Уникальные технологии Common Lisp (с примерами использования)
Базовые подсистемы языка
В языке 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) и т. д. - ... ну и для многих других целей.
Больше по теме: Paul Graham, On Lisp
Мета-объектный протокол и 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 есть ряд технологий менее значительных, которые нельзя назвать в полной мере уникальными, но которые существенно упрощают его применение и делают программы более ясными, а также дают дополнительные возможности для расширения языка:
Протокол множественных возвращаемых значений
Дает возможность возвращать из функции несколько значений и по желанию принимать все их (и привязывать к каким-то переменным) или только часть. По-умолчанию для кода, не использующего эту функциональность, передается только
Казалось бы, это простая возможность, однако, на поверку, она требует обширной поддержки на языковом уровне (учитывая необходимость поддержки возврата из блоков и т. п.).
Протокол обобщенных переменных
Это аналог свойств в некоторых ОО-языках. Концептуально, оперирует понятием места (place) — по сути дела ячейки памяти, однако не физической (без манипуляции указателями) — это может быть просто объект или же элемент какой-то структуры (будь-то опять же объект, список, массив и т. д.) Таким образом, имеются намного большие возможности, чем при использовании обычных свойств, поскольку для любой функции, которая читает значения какого-либо места, можно указать функцию которая его значение задает.
Больше по теме: Paul Graham, On Lisp, Ch.12 «Generalized Variables»
Макросы чтения
Это инструмент модификации синтаксиса языка за пределы s-выражений, который дает программисту возможность, используя компилятор Lisp, создать свой собственный синтаксис. Его работа основана на фундаментальном принципе Lisp-систем: разделении времени чтения, времени компиляции и времени выполнения — REPL (Read-Eval-Print Loop). Обычные макросы вычисляются (раскрываются, expand) во время компиляции, и полученный код компилируется вместе с написанным вручную. А вот макросы чтения выполняются еще на этапе обработки программы парсером при обнаружении специальных символов (dispatch characters). Механизм макросов чтения является возможностью получить прямой доступ к Reader’у и влиять на то, как он формирует абстрактное синтаксическое дерево из «сырого» программного кода. Таким образом, можно на поверхности Lisp использовать любой синтаксис, вплоть до, например,
Пример такого использования — буквальный синтаксис для чтения hash-таблиц, который почему-то отсутствует в спецификации языка. Это, кстати, еще один пример того, каким образом CL дает возможность изменить себя и использовать новые базовые синтаксические конструкции наравне с определенными в стандарте. Основывается на буквальном синтаксисе для ассоциативных списков (ALIST):
<code class="lisp">; 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)))))</code>
Больше по теме: Doug Hoyte, Let Over Lambda, Ch.4 «Read Macros»
Послесловие
В заключение хотелось бы коснуться понятия высокоуровневого языка программирования. Оно, конечно, является философским, поэтому выскажу свое мнение на этот счет: по-настоящему высокоуровневый язык должен давать программисту возможность выражать свои мысли, концепции и модели в программном коде напрямую, а не через другие концепции, если только те не являются более общими. Это значит, например, что высокоуровневый язык должен позволять напрямую оперировать такой сущностью, как функция, а не требовать для этого задействовать другие сущности такого же уровня абстракции, скажем, классы. Подход к созданию высокоуровневого языка можно увидеть на примере Common Lisp, в котором для каждой задачи выбирается подходящая концепция, будь то объект, сигнал или место. А что дает нам использование по-настоящему высокоуровневых языков? Большую расширяемость, краткость и адаптируемость программы к изменениям, и, в конце концов, настоящую свободу при программировании!
48 коментарів
Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.