Базовые подсистемы языка
В языке Common Lisp есть как минимум 3 инфраструктурных технологии, во многом формирующие подходы к его применению, которые в других языках либо отсутствуют вовсе, либо реализованы в очень ограниченном варианте. Для компенсации их отсутствия пользователи других языков часто вынуждены использовать Шаблоны проектирования, а порой и вообще не имеют возможности применять некоторые более эффективные подходы к решению типичных задач.
Что это за технологии и какие возможности дает их использование?
Макросистема
- Это основная отличительная особенность Common Lisp, выделяющая его среди других языков. Ее реализация возможна благодаря использованию для записи Lisp-програм s-нотации (представления программы непосредственно в виде ее абстрактного синтаксического дерева). Позволяет программировать компилятор языка.
- Позволяет полностью соблюдать один из основополагающих принципов хорошего стиля программирования DRY (не-повторяй-себя).
- В отличие от обычных функций, аргументы, передаваемые макросам, не вычисляются, поэтому с их помощью можно создавать любые управляющие конструкции языка.
-
Определение управляющих конструкций языка, которые могут использоваться на равне со стандартными (на самом деле практически все стандартные управляющие конструкции также являются макросами. Основу языка — “аксиомы”, которые невозможно определить через другие конструкции — составляют специальные операторы). В качестве примера можно привести анафорические управляющие конструкции (см. библиотеку Anaphora), которые, используя принцип “convention over configuration”, скрывают реализацию некоторых типичных шаблонов.
Самый простой пример — макро AIF (или IF-IT), которое тестирует первый аргумент на истинность и одновременно привязывает его значение к переменной IT, которую, соответственно, можно использовать в THEN-clause:
Учитывая то, что в 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)* Простейший макрос-обертка для задания аттрибутов объекта, полученного с помощью селектора jQuery
`(.attr ($ (+ "#" ,id)) ,attr ,val))
- В форме локальных макросов (MACROLET) для модуляризации и разделения потоков вычислений внутри сложных функций, а также для соблюдения принципа DRY при написании лишь слегка отличающегося кода в различных местах одной функции.
- Наконец, создание инфраструктурных систем языка. Например, с помощью макросов можно реализовать продления (библиотека CL-CONT), ленивые вычисления (библиотека SERIES) и т.д.
- …ну и для многих других целей.
Мета-объектный протокол и CLOS
- Основа объектной системы языка. Позволяет манипулировать представлением классов.
- Методы не принадлежат классам, а специализируются на них, что дает возможность элегантной реализации множественной диспетчиризации. Также возможна специализация не по классу, а по ключу.
- Уникальной является технология комбинации методов, позволяющая использовать стандартные способы комбинации: перед, после, вокруг,— а также определенные пользователем.
- object-persisance: Elephant, AllegroCache
- работа с БД: CLSQL
- интерфейс пользователя: Cells
Система обработки ошибок / сигнальный протокол
Система обработки ошибок есть в любом современном языке, однако в 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”
Вспомогательные технологии
Протокол множественных возвращаемых значений
Дает возможность возвращать из функции несколько значений и по желанию принимать все их (и привязывать к каким-то переменным) или только часть. По-умолчанию для кода, не использующего эту функциональность, передается только 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):
(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”