Let Over Lambda - 50 Years of Lisp / Let Over Lambda - 50 лет Lisp-у: Глава 4 - Считывающие Макросы

← →
Английский оригинал Перевод на русский

Сожалеем, но текст оригинала доступен только зарегистрированным пользователям.

4.1 Время-Работы и Время-Чтения

Charlz_Klug 24.08.13 в 04:46

0

Синтаксический сахар вызывает рак точек с запятой
- Алан Перлис

Charlz_Klug 24.08.13 в 05:04

0

Не один только Лисп даёт прямой доступ к коду, который может разбираться на структуры из cons ячеек, но, кроме этого Лисп предоставляет доступ к символам, составляющим ваши программы, ещё до того, как программы будут представлять из себя структуру. Обычный макрос работает с программой в форме дерева. Кроме обычного макроса существует специальный тип макросов, называемых считывающими макросами (read macro), оперирующими с сырыми символами, из которых состоит ваша программа.

Charlz_Klug 24.08.13 в 06:01

0

Если нам понадобиться определить не-Лисповский синтаксис в Лиспе, то в этом случае не имеет смысла использовать Лисповский считыватель - он предназначен только для чтения Лиспа. Считывающий макрос - это устройство, используемое для обработки не-Лисп синтаксиса, после считывающего макроса в дело вступает Лисп считыватель. Причина, по которой Лисп считыватель является более мощным чем считыватели в других языках заключается в том, что Лисп даёт нам возможность контролировать (hooks) каждый аспект поведения считывателя. В частности, Лисп позволяет вам расширять считыватель, таким образом, что не-Лисп объекты будут прочитываться и преобразовываться в Лисп объекты. Также, как вы строите ваши приложения поверх Лиспа, расширяя его с помощью макросов и функций, также и Лисп приложения могут, а чаще всего и просачиваются в это измерение расширяемости. Когда это происходит, то становится возможным чтение любого символа на основе синтаксиса с помощью Лисп считывателя, это означает что вы добавили этот синтаксис в Лисп.

Charlz_Klug 24.08.13 в 06:29

0

В то время, когда трансформация кода, выполняемая обычными макросами используется для вставки Лисп кода в новый Лисп код, считывающие макросы могут быть написаны для вставки не-Лисп кода в Лисп код. Подобно обычным макросам, считывающие макросы реализуются с помощью функций, благодаря этому мы имеем доступ ко всей мощи Лисп окружения. Подобно макросам, увеличивающим продуктивность через создание более удобного предметно ориентированного языка, считывающие макросы увеличивают продуктивность позволяя сокращать выражения в точку где они больше не являются выражениями.

Charlz_Klug 26.08.13 в 04:19

0

Если всё что нам нужно для того, чтобы разобрать эти не-Лисповские предметно-ориентированные языки - это написать короткий считывающий макрос, то может быть эти не-Лисп языки на самом деле являются умно замаскированным Лиспом. Если XML может прямо считываться Лисп считывателем [XML-AS-READ-MACRO], то может быть XML - это просто видоизменённый Лисп. Подобным образом считывающие макросы могут использоваться для чтения регулярных выражений и SQL запросов прямо в Лисп, то может быть эти языки на самом деле являются Лиспом. Это нечёткое различие между кодом и данными, Лиспом и не-Лиспом, являются источником многих интересных философских проблем, возникающих перед Лисп программистами с самого начала зарождения Лиспа.

Charlz_Klug 26.08.13 в 04:53

0

Базовый считывающий макрос, встроенный в Common Lisp - это #. вычисляющий макрос в момент чтения. Этот считывающий макрос позволяет вам встраивать объекты в читаемые, не сериализируемые формы, но создаваемые с помощью Лисп кода. Один забавный пример - это создание формы, содержащей изменяющиеся значения при каждом вызове.

Charlz_Klug 26.08.13 в 05:11

0

* ’(football-game
(game-started-at
#.(get-internal-real-time))
(coin-flip
#.(if (zerop (random 2)) ‘heads ’tails)))


(FOOTBALL-GAME
(GAME-STARTED-AT 187)
(COIN-FLIP HEADS))

Charlz_Klug 26.08.13 в 05:12

0

Несмотря на то, что это то-же самое выражение, эта форма считывается по разному раз за разом:

Charlz_Klug 26.08.13 в 05:14

0

* ’(football-game
(game-started-at
#.(get-internal-real-time))
(coin-flip
#.(if (zerop (random 2)) ’heads ’tails)))


(FOOTBALL-GAME
(GAME-STARTED-AT 309)
(COIN-FLIP TAILS))


Charlz_Klug 26.08.13 в 09:54

0

Заметьте, что две формы окружённые #. вычисляются в момент чтения, а не тогда, когда вычисляется форма. Полный список был сформирован после того, как они были вычислены, а предыдущая и последующая эквивалентность (определённая equal'ом) может быть обнаружена через повторное вычисление последней прочитанной формы и сравнения её с предыдущими результатами, с помощью вспомогательных переменных *, + в REPL 1:

Charlz_Klug 26.08.13 в 10:08

0

1 Переменная * содержит значение, являющееся результатом предыдущей формы, а переменная + содержит значение последней формы.

Charlz_Klug 26.08.13 в 10:12

0

* (equal * (eval +))
T

Charlz_Klug 26.08.13 в 10:12

0

Следует обратить внимание на то, что эти формы вычисляются в момент чтения, этим они отличаются от использования обратной кавычки, более подробно мы рассмотрим обратную кавычку в следующем разделе. Мы можем вычислить подобную форму с помощью использования обратных кавычек:

Charlz_Klug 26.08.13 в 10:16

0

* ‘(football-game
(game-started-at
,(get-internal-real-time))
(coin-flip
,(if (zerop (random 2)) ’heads ’tails)))


(FOOTBALL-GAME
(GAME-STARTED-AT 791)
(COIN-FLIP HEADS))

Charlz_Klug 26.08.13 в 10:18

0

но в этом случае будет происходить вычисление в различные результаты, поскольку обратная кавычка производит считывание в виде кода для вычисления:

Charlz_Klug 26.08.13 в 10:41

0

* (equal * (eval +))


NIL
; если вы не очень быстрый и удачливый

Charlz_Klug 26.08.13 в 10:48

0

* (equal * (eval +))


NIL
; если вы не очень быстрый и удачливый

Charlz_Klug 26.08.13 в 10:49

0

* (equal * (eval +))


NIL
; если вы не очень быстрый и удачливый

Charlz_Klug 26.08.13 в 10:50

0

* (equal * (eval +))


NIL

если вы не очень быстрый и удачливый

Charlz_Klug 28.08.13 в 03:16

0

4.2 Обратная Кавычка

Charlz_Klug 28.08.13 в 08:45

0

Обратная кавычка, иногда называемая как квазикавычка (quasiquote) 2 и отображаемая как ` - это относительно новый элемент в промышленных диалектах Лиспа. Эта концепция по-прежнему абсолютно чужда для не-Лисп языков программирования.

Charlz_Klug 28.08.13 в 08:53

0

2 Scheme программисты называют её квазикавычкой а Common Lisp программисты - обратной кавычкой.

Charlz_Klug 28.08.13 в 08:54

0

У обратной кавычки странная история развития. Обратная кавычка развивалась параллельно с Лиспом. Есть сообщения [QUASIQUOTATION] о том, что раньше никто не верил в то, что вложенные обратные кавычки будут работать правильно, пока умные программисты не выяснили что обратные кавычки правильно выполняют свою работу - идеи людей о том, что было верным оказались неверными. Известно, что трудно понять работу вложенной обратной кавычки. Даже Стил (Steele), отец Common Lisp'а, жалуется на это [CLTL2—P530].

Charlz_Klug 28.08.13 в 09:09

0

В принципе Лисп не нуждается в обратной кавычке. Всё, что можно выполнить с помощью обратной кавычки - можно выполнять с помощью других функций, предназначенных для постройки списков. Однако, обратная кавычка настолько полезна при программировании макросов, а в Лиспе под программированием понимается программирование макросов, что Лисп профессионалы очень активно ею пользуются.

Charlz_Klug 28.08.13 в 09:15

0

Вначале мы должны разобраться с обычным закавычиванием. В Лиспе, мы используем префикс формы в виде символа кавычки (') для того, чтобы информировать Лисп о том, что следующая форма должна рассматриваться как сырые данные, а не как код, который нужно вычислить. Или, если выразиться иначе, результатом вычисления кавычки в коде будет возвращение формы. Иногда мы говорим что кавычка останавливает или отключает вычисление формы.

Charlz_Klug 28.08.13 в 09:26

0

В Лиспе обратная кавычка может использоваться как замена кавычки. Исключая некоторые специальные символы, называемые символами раскавычивания (unquote) и могущие появиться в форме, обратная кавычка останавливает вычисление тем-же способом, что и кавычка. Как следует из названия символы раскавычивания изменяют семантику вычисления. Иногда мы говорим что раскавычивание перезапускает или включает вычисление формы.

Charlz_Klug 28.08.13 в 09:46

0

Есть три главных типа раскавычивания: обычное раскавычивание, объединяющее раскавычивание и деструктивное, объединяющее раскавычивание.

Charlz_Klug 29.08.13 в 05:39

0

Для выполнения обычного раскавычивания, мы используем оператор запятую:

Charlz_Klug 29.08.13 в 05:40

0

* (let ((s ’hello))
‘(,s world))


(HELLO WORLD)

Charlz_Klug 29.08.13 в 05:41

0

Хотя выражение, которое мы раскавычили является простым вычислением символа, s, на месте этого символа может быть любое Лисп выражение, вычисляемое в нечто значимое для любого контекста, в котором оно появляется в шаблоне обратной кавычки. Каким бы ни был результат, он будет вставлен в результирующий список в car позиции того места в котором он появился в шаблоне обратной кавычки.

Charlz_Klug 29.08.13 в 05:58

0

В нотации Лисповской формы мы можем использовать . если мы хотим явно вставить что-либо cdr'ное в создаваемую списковую структуру. Если здесь мы вставим список, то результирующей формой обратной кавычки будет корректный список. Но, если здесь мы вставим что-нибудь другое, то мы получим новую не-списковую структуру.

Charlz_Klug 29.08.13 в 06:11

0

Такое-же поведение доступно нам везде, в том числе внутри обратной кавычки 3. Благодаря архитектуре обратной кавычки мы можем раскавычивать элементы даже в такой позиции:

Charlz_Klug 29.08.13 в 06:14

0

3 Поскольку обратная кавычка использует (почти ту же) стандартную функцию read что и везде.

Charlz_Klug 29.08.13 в 06:17

0

* (let ((s ’(b c d)))
‘(a . ,s))


(ABCD)

Charlz_Klug 29.08.13 в 06:17

0

Вставка списков в cdr позиции создаваемого списка из шаблона обратной кавычки оказалось весьма универсальным, поэтому был сделан следующий шаг названный как объединяющее раскавычивание. Вышеприведённая комбинация ., полезна, но не способна вставлять элементы в середину списка. Для этого у нас есть оператор объединяющее раскавычивание:

Charlz_Klug 29.08.13 в 06:24

0

* (let ((s ’(b c d)))
‘(a ,@s e))


(ABCDE)

Charlz_Klug 29.08.13 в 10:29

0

Ни ., ни ,@ не модифицируют сращиваемый список. Для примера, после вычисления обратной кавычки в обеих предыдущих формах, s по прежнему будет привязан к трёх элементному списку (B C D). Хотя это не является строго определяемым стандартом, в форме (A B C D), (B C D) может являться общей структурой с объединённым списком s. Однако, структура списка (A B C D E) гарантирует, что она будет свежесозданной при вычислении обратной кавычки, поскольку ,@ запрещено модифицировать сращенные списки. Объединяющее раскавычивание - не деструктивная операция, поскольку в целом нам нужно думать об обратной кавычке как о компонуемом шаблоне для создания списков. Деструктивная модификация списковой структуры не свежесозданных данных при каждом вычислении обратно закавыченного кода может иметь нежелательные эффекты в плане будущих расширений.

Charlz_Klug 29.08.13 в 10:46

0

Однако, в Common Lisp есть деструктивная версия сращиваемого раскавычивания, которое вы можете использовать везде, где можно использовать сращивающее раскавычивание. Для деструктивного раскавычивания используйте ,. . Деструктивное сращивание работает также, как и обычное сращивание, за исключением того, что сращиваемый список может быть модифицирован в процессе вычисления шаблона обратной кавычки. И хотя отличие от обычного раскавычивания выражается всего лишь в одном символе, эта нотация более умно используем символ . из cdr позиционного раскавычивания ., рассмотренного выше.

Charlz_Klug 29.08.13 в 10:56

0

Для того, чтобы увидеть это в действии в этом примере мы деструктивно модифицируем список указанный в to-splice:

Charlz_Klug 29.08.13 в 10:58

0

* (defvar to-splice ’(B C D))


TO-SPLICE
* ‘(A ,.to-splice E)


(A B C D E)
* to-splice


(BCDE)

Charlz_Klug 29.08.13 в 10:59

0

Деструктивная модификация сращиваемых списков может быть опасной. Рассмотрим следующее применение деструктивного сращивания:

Charlz_Klug 30.08.13 в 09:12

0

(defun dangerous-use-of-bq ()
‘(a ,. ’(b c d) e))

Charlz_Klug 30.08.13 в 09:12

0

При первом вызове dangerous-use-of-bq возвращается ожидаемый ответ: (A B C D E). Но, поскольку эта функция использует деструктивное сращивание и модификацию не свежесгенерированных списков - закавыченный список - то мы можем ожидать возникновения различных нежелательных последствий. В этом случае при втором вычислении dangerous-use-of-bq форма (B C D) будет представлять из себя форму (B C D E) и в момент, когда обратная кавычка попытается деструктивно срастить этот список с остатком шаблона обратной кавычки (E) - его собственный хвост - будет создан список, содержащий цикл (cycle). Более детально мы обсудим циклы в разделе 4.5, Циклические Выражения.

Charlz_Klug 30.08.13 в 09:23

0

Однако, есть много случаев в которых деструктивное сращивание является чрезвычайно безопасным. Не позволяйте dangerous-use-of-bq напугать вас если вам нужна повышенная эффективность в ваших формах обратной кавычки. Есть множество операций, создающих свежие списковые структуры, которые вам так или иначе могут не понадобиться. Например, сращивание результатов mapcar настолько распространено и безопасно, что вполне может претендовать на программную идиому:

Charlz_Klug 30.08.13 в 09:31

0

(defun safer-use-of-bq ()
`(a
,.(mapcar #’identity ’(b c d))
e))

Charlz_Klug 30.08.13 в 09:32

0

Но, есть деталь, которая мешает этому. Чаще всего обратная кавычка используется для создания макросов, часть программирования на Лиспе, где менее важна скорость и более важна ясность. Если думать о побочных эффектах в ваших операциях сращивания, то при создании и интерпретации макросов вам придётся часто отвлекаться на них, а это не стоит свеч. Эта книга придерживается обычных сращиваний. Чаще всего обратная кавычка используется для конструирования макросов, но, это не единственное их использование. Обратная кавычка - это удобный предметно ориентированный язык для смешивания списков, и ещё более удобным он становится с возможностью деструктивного сращивания.

Charlz_Klug 30.08.13 в 09:45

0

Как работает обратная кавычка? Обратная кавычка - это считывающий макрос. Формы обратной кавычки читаются как код, который при вычислении, становится желаемым списком. Возвращаясь к примеру из предыдущего раздела о вычислении во время выполнения, мы можем отключить красивую печать (pretty printing), закавычить значение формы обратной кавычки и вывести его на печать для того, чтобы увидеть как читаются формы с обратной кавычкой 4:

Charlz_Klug 30.08.13 в 09:53

0

4 Мы возвращаем t и поэтому мы не видим значения, возвращаемого от print. (values) также универсальны.

Charlz_Klug 30.08.13 в 09:54

0

* (let (*print-prettyt) ; привязано к nil
(print
'`footba11-game
(game-started-at
,(get-internal-real-time))
(coin-flip
,(if (zerop (random 2))
’heads
’tails))))
t)


(LISP::BACKQ-LIST
(QUOTE FOOTBALL-GAME)
(LISP::BACKQ-LIST
(QUOTE GAME-STARTED-AT)
(GET-INTERNAL—REAL-TIME))
(LISP::BACKQ-LIST
(QUOTE COIN-FLIP)
(IF (ZERO? (RANDOM 2))
(QUOTE HEADS)
(QUOTE TAILS))))
T

Charlz_Klug 30.08.13 в 09:56

0

В предыдущей некрасиво напечатанной форму, функция LISP: :BACKQ-LIST идентична list, за исключением поведения, связанного с красивой печатью. Заметьте, что операторы запятая исчезли. Common Lisp очень либерален и поэтому позволяет читать обратные кавычки, также, как и допускает операции с использованием общих структур.

Charlz_Klug 30.08.13 в 10:02

0

Кроме того обратная кавычка предоставляет много интересных решений забавной проблемы написания Лисп выражений, вычисляющихся в самих себя. Эти выражения часто называются куайнами в честь Уилларда Куайна, который занимался их широким изучением и кто, по сути, ввёл термин квазицитирования - альтернативное название обратной кавычки [FOUNDATIONS-P31-FOOTNOTE3]. Вот забавный пример куайна, связанный с Майком МкМахоном (Mike McMahon) [QUASIQUOTATION]:

Charlz_Klug 02.09.13 в 09:40

0

* (let ((let `'(let ((let ’,let))
,let)))
‘(let ((let ’,let)) ,let))


(LET ((LET `'(LET ((LET ’,LET))
,LET)))
‘(LET ((LET ’,LET)) ,LET))

Charlz_Klug 04.09.13 в 07:56

0
Минутку...

Следующая страница →