Время написания: октябрь 2008
Базовые подсистемы языка
В языке Common Lisp есть как минимум 3 инфраструктурных технологии, во многом формирующие подходы к его применению, которые в других языках либо отсутствуют вовсе, либо реализованы в очень ограниченном варианте. Для компенсации их отсутствия пользователи других языков часто вынуждены использовать Шаблоны проектирования, а порой и вообще не имеют возможности применять некоторые более эффективные подходы к решению типичных задач.
Что это за технологии и какие возможности дает их использование?
Макросистема
- Это основная отличительная особенность Common Lisp, выделяющая его среди других языков. Ее реализация возможна благодаря использованию для записи Lisp-програм s-нотации (представления программы непосредственно в виде ее абстрактного синтаксического дерева). Позволяет программировать компилятор языка.
- Позволяет полностью соблюдать один из основополагающих принципов хорошего стиля программирования DRY (не-повторяй-себя).
- В отличие от обычных функций, аргументы, передаваемые макросам, не вычисляются, поэтому с их помощью можно создавать любые управляющие конструкции языка.
- Определение управляющих конструкций языка, которые могут использоваться на равне со стандартными (на самом деле практически все стандартные управляющие конструкции также являются макросами. Основу языка — “аксиомы”, которые невозможно определить через другие конструкции — составляют специальные операторы). В качестве примера можно привести анафорические управляющие конструкции (см. библиотеку Anaphora), которые, используя принцип “convention over configuration”, скрывают реализацию некоторых типичных шаблонов.
Самый простой пример — макро AIF (или IF-IT), которое тестирует первый аргумент на истинность и одновременно привязывает его значение к переменной IT, которую, соответственно, можно использовать в THEN-clause:
(defmacro aif (var then &optional else)
`(let ((it ,var))
(if it ,then ,else)))Учитывая то, что в CL ложность представляется константой NIL, которая также соответствует пустому списку, такая конструкция, например, часто применяется в коде, где сначала какие-то данные аккумулируются в список, а потом, если список не пуст, над ними производятся какие-то действия. Другой вариант, это проверить, заданно ли какое-то значение и потом использовать его:
(defun determine-fit-xture-type (table-str)
"Determine a type of Fit fixture, specified with TABLE-STR"
(handler-case
(aif (find (string-trim *spacers* (strip-tags (get-tag "td" (get-tag "tr" table-str 0) 1)))
*fit-xture-rules* :test #'string-equal :key #'car)
(cdr it)
'row-fit-xture)
(tag-not-found () 'column-fit-xture)))* В этой функции проверяется, есть ли во второй ячейке первой строки HTML таблицы какие-то данные и в соответствии с этим определяется тип привязки для Fit-теста. Переменной it присвоены найденные данные.
- Создание DSL‘ей для любой предметной области, которые могут иметь в распоряжении все возможности компилятора Common Lisp. Ярким примером такого DSL’я может служить библиотека Parenscript, которая реализует кодогенерацию JavaScript из Common Lisp. Используя ее, можно писать макросы для Javascript!
(js:defpsmacro set-attr (id attr val)
`(.attr ($ (+ "#" ,id)) ,attr ,val))* Простейший макрос-обертка для задания аттрибутов объекта, полученного с помощью селектора jQuery
- В форме локальных макросов (MACROLET) для модуляризации и разделения потоков вычислений внутри сложных функций, а также для соблюдения принципа DRY при написании лишь слегка отличающегося кода в различных местах одной функции.
- Наконец, создание инфраструктурных систем языка. Например, с помощью макросов можно реализовать продления (библиотека CL-CONT), ленивые вычисления (библиотека SERIES) и т.д.
- …ну и для многих других целей.
Мета-объектный протокол и CLOS
- Основа объектной системы языка. Позволяет манипулировать представлением классов.
- Методы не принадлежат классам, а специализируются на них, что дает возможность элегантной реализации множественной диспетчиризации. Также возможна специализация не по классу, а по ключу.
- Уникальной является технология комбинации методов, позволяющая использовать стандартные способы комбинации: перед, после, вокруг,— а также определенные пользователем.
Примерами использования мета-объектного протокола также являются инфраструктурные системы языка, реализованные в виде библиотек:
- object-persisance: Elephant, AllegroCache
- работа с БД: CLSQL
- интерфейс пользователя: Cells
Библиотека CLSQL создана для унификации работы с различными SQL базами данных. Кстати, на ее примере можно увидеть проявление мультипарадигменности Common Lisp: у библиотеки есть как объектно-ориентированный интерфейс (ORM), реализованный на основе CLOS, так и функциональный (на основе функций и макросов чтения).
С помощью мета-объектного протокола стандартный класс языка расширяется специальным параметром — ссылкой на таблицу БД, к которой он привязан, а описания его полей (в терминологии Lisp: слотов) — дополнительными опциональными параметрами, такими как: ограничение уникальности, ключа, функция-преобразователь при записи и извлечении значения из БД и т.д.
Больше по теме: Gregor Kiczales et al. The Art of Metaobject ProtocolСистема обработки ошибок / сигнальный протокол
Система обработки ошибок есть в любом современном языке, однако в CL она все еще остается в определенном смысле уникальной (разве что в C# сейчас вводится нечто подобное). Преимущество этой системы заключается опять же в ее большей абстрактности: хотя основная ее задача — обработка ошибок, точнее исключительных ситуаций,— она построена на более общей концепции передачи управления потоком выполнения программы по стеку. …Как и системы в других языках. Но в других языках есть единственный предопределенный вариант передачи управления: после возникновения исключительной ситуации стек отматывается вплоть до уровня, где находится ее обработчик (или до верхнего уровня). В CL же стек не отматывается сразу, а сперва ищется соответствующий обработчик (причем это может делаться как в динамическом, так и в лексическом окружении), а затем обработчик выполняется на том уровне, где это определенно программистом. Таким образом, исключительные ситуации не несут безусловно катастрофических последствий для текущего состояния выполнения программы, т.е. с их помощью можно реализовать различные виды нелокальной передачи управления (а это приводит к сопроцедурам и т.п.) Хорошие примеры использования сигнального протокола приведены в книге Practical Common Lisp (см. ниже).
Больше по теме:- Kent Pitman, Condition Handling in the Lisp Language Family
- Peter Siebel, Practical Common Lisp, Ch.19 “Beyond Exception Handling: Conditions and Restarts”
Вспомогательные технологии
Кроме того в CL есть ряд технологий менее значительных, которые нельзя назвать в полной мере уникальными, но которые существенно упрощают его применение и делают программы более ясными, а также дают дополнительные возможности для расширения языка:
Протокол множественных возвращаемых значений
Дает возможность возвращать из функции несколько значений и по желанию принимать все их (и привязывать к каким-то переменным) или только часть. По-умолчанию для кода, не использующего эту функциональность, передается только 1-е значение.
Казалось бы, это простая возможность, однако, на поверку, она требует обширной поддержки на языковом уровне (учитывая необходимость поддержки возврата из блоков и т.п.).
Протокол обобщенных переменных
Это аналог свойств в некоторых ОО-языках. Концептуально, оперирует понятием места (place) — по сути дела ячейки памяти, однако не физической (без манипуляции указателями) — это может быть просто объект или же элемент какой-то структуры (будь-то опять же объект, список, массив и т.д.) Таким образом, имеются намного большие возможности, чем при использовании обычных свойств, поскольку для любой функции, которая читает значения какого-либо места, можно указать функцию которая его значение задает.
Больше по теме: Paul Graham, On Lisp, Ch.12 “Generalized Variables”Макросы чтения
Это инструмент модификации синтаксиса языка за пределы s-выражений, который дает программисту возможность, используя компилятор Lisp, создать свой собственный синтаксис. Его работа основана на фундаментальном принципе Lisp-систем: разделении времени чтения, времени компиляции и времени выполнения — REPL (Read-Eval-Print Loop). Обычные макросы вычисляются (раскрываются, expand) во время компиляции, и полученный код компилируется вместе с написанным вручную. А вот макросы чтения выполняются еще на этапе обработки программы парсером при обнаружении специальных символов (dispatch characters). Механизм макросов чтения является возможностью получить прямой доступ к Reader’у и влиять на то, как он формирует абстрактное синтаксическое дерево из “сырого” программного кода. Таким образом, можно на поверхности Lisp использовать любой синтаксис, вплоть до, например, C-подобного. Впрочем, Lisp-программисты предпочитают все-таки префиксный унифицированный синтаксис со скобками, а Reader-макросы используют для специфических задач.
Пример такого использования — буквальный синтаксис для чтения hash-таблиц, который почему-то отсутствует в спецификации языка. Это, кстати, еще один пример того, каким образом CL дает возможность изменить себя и использовать новые базовые синтаксические конструкции наравне с определенными в стандарте. Основывается на буквальном синтаксисе для ассоциативных списков (ALIST):
;; a reader syntax for hash tables like alists: #h([:test (test 'eql)] (key . val)*)
(set-dispatch-macro-character #\# #\h
(lambda (stream subchar arg)
(declare (ignore subchar)
(ignore arg))
(let* ((sexp (read stream t nil t))
(test (when (eql (car sexp) :test) (cadr sexp)))
(kv-pairs (if test (cddr sexp) sexp))
(table (gensym)))
`(let ((,table (make-hash-table :test (or ,test 'eql))))
(mapcar #'(lambda (cons)
(setf (gethash (car cons) ,table)
(cdr cons)))
',kv-pairs)
,table)))))
Больше по теме: Doug Hoyte, Let Over Lambda, Ch.4 “Read Macros”
Послесловие
В заключение хотелось бы коснуться понятия высокоуровневого языка программирования. Оно, конечно, является философским, поэтому выскажу свое мнение на этот счет: по-настоящему высокоуровневый язык должен давать программисту возможность выражать свои мысли, концепции и модели в программном коде напрямую, а не через другие концепции, если только те не являются более общими. Это значит, например, что высокоуровневый язык должен позволять напрямую оперировать такой сущностью, как функция, а не требовать для этого задействовать другие сущности такого же уровня абстракции, скажем, классы. Подход к созданию высокоуровневого языка можно увидеть на примере Common Lisp, в котором для каждой задачи выбирается подходящая концепция, будь то объект, сигнал или место. А что дает нам использование по-настоящему высокоуровневых языков? Большую расширяемость, краткость и адаптируемость программы к изменениям, и, в конце концов, настоящую свободу при программировании!
так а чего на ДОУ-то не разместил?
ReplyDelete@COTOHA оно на доу было в 2008-м, как свидетельствует ссылка (http://www.developers.org.ua/archives/vseloved/2008/10/22/common-lisp-technologies/). Сейчас, просто, решил пособирать все статьи в одном месте.
ReplyDeleteага. уже нашёл - пошёл удалять свой комент а тут уже меня попалили :)
ReplyDeleteподумалось, что надо как-нить на доу делать обхоры древних статей. чтоб новых людей туда загонять.