Let Over Lambda - 50 Years of Lisp / Let Over Lambda - 50 лет Lisp-у: Глава 3 - Основы Макросов

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

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

3.1 Итеративная Разработка

Charlz_Klug 11.07.13 в 15:04

0

Лисп помог целому ряду одарённых людей размышлять таким образом, какой ранее не был им доступен.
- Эдсгер Дейкстра

Charlz_Klug 13.07.13 в 17:43

0

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

Charlz_Klug 14.07.13 в 14:12

0

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

Charlz_Klug 15.07.13 в 05:28

0

В этой главе мы напишем несколько базовых макросов и ознакомимся с двумя основными концепциями макросов: предметно-ориентированные языки (domain specific languages) и структуры управления (control structures). После того, как мы изучим эти основные понятия о макросах, мы вернёмся назад и обсудим процесс создания макросов с помощью макросов. На протяжении всей книги мы будем использовать такие техники как захват переменной и введение свободной переменной, кроме этого мы ознакомимся с новым, более удобным синтаксисом определения Лисп макросов.

Charlz_Klug 15.07.13 в 05:50

0

3.2 Предметно-Ориентированные Языки

Charlz_Klug 15.07.13 в 08:08

0

Common Lisp, наряду с большинством программных сред, предоставляет функцию sleep, которая приостанавливает исполнение процесса на n секунд, где n - не отрицательный, не комплексный, числовой аргумент. Например, мы можем захотеть уснуть на 3 минуты (180 секунд), в этом случае мы можем вычислить эту форму:

Charlz_Klug 16.07.13 в 18:20

0

(sleep 180)

Charlz_Klug 16.07.13 в 18:20

0

Или, если мы предпочитаем думать о засыпании в терминах минут, то взамен последнего выражения мы можем использовать

Charlz_Klug 16.07.13 в 18:22

0

(sleep (* 3 60))

Charlz_Klug 16.07.13 в 18:22

0

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

Charlz_Klug 16.07.13 в 18:26

0

(defun sleep-minutes (m)
(sleep (* m 60)))

Charlz_Klug 16.07.13 в 18:27

0

Подход с определением новой функции для каждой единицы времени оказывается неуклюжим и неудобным. Что нам нужно - так это некая абстракция, которая позволяла бы использовать единицу времени вместе со значением. Нам нужен предметно-ориентированный язык (domain specific language).

Charlz_Klug 16.07.13 в 18:31

0

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

Charlz_Klug 16.07.13 в 18:42

0

#define UNIT_SECONDS 1
#define UNIT_MINUTES 2
#define UNIT_HOURS 3
int sleep_units(int value, int unit) {
switch(value) {
case UNIT_SECONDS: return value;
case UNIT_MINUTES: return value*60;
case UNIT_HOURS: return value*3600;
}
}

Charlz_Klug 16.07.13 в 18:44

0

Листинг 3.1: SLEEP-UNITS-1
(defun sleep-units% (value unit)
(sleep
(* value
(case unit
((3) 1)
((m) 60)
((h) 3600)
((d) 86400)
((ms) 1/1000)
((us) 1/1000000)))))

Charlz_Klug 16.07.13 в 18:47

0

Но в Лиспе самый очевидный путь обозначения желаемой единицы - это использование символа. Символ в Лиспе - это нечто, не eq'альное другим символам. Eq - это самый быстрый оператор сравнения в Лиспе и примерно соответствует сравнению указателей. Поскольку сравнение указателей производится очень быстро, то символы предоставляют нам очень быстрый и удобный способ определения равенства двух и более различных Лисп выражений. В Лиспе мы должны определить функцию sleep-units%, после этого мы можем использовать единицы времени в наших формах:

Charlz_Klug 17.07.13 в 10:04

0

(sleep-units% 2 ’m)
(sleep-units% 500 ’us)

Charlz_Klug 17.07.13 в 10:05

0

Поскольку сравнение символов требует сравнения только одного указателя, sleep-units% будет скомпилирован в очень быструю диспетчеризацию во времени выполнения:

Charlz_Klug 17.07.13 в 10:08

0

...
524: CMP ESI, [#x586FC4D0] ; ’S
52A: JEQ L11
530: CMP ESI, [#x586FC4D4] ; ’M
536: JEQ L10
538: CMP ESI, [#x586FC4D8] ; ’H
53E: JEQ L9
540: CMP ESI, [#x586FC4DC] ; ’D
546:
...

Charlz_Klug 17.07.13 в 10:09

0

Заметьте, как должна закавычиваться единица времени, указанная в sleep-units%. Это происходит потому, что Лисп начинает вычисление функции с вычисления всех аргументов функции и привязывает результаты к переменным, использующимся внутри функции. Числа, строки и некоторые другие примитивы вычисляются в самих себя, поэтому нам не нужно закавычивать числовые значения, переданные в sleep-units%. Если же вы хотите чтобы и эти значения не вычислялись, то вы можете закавычить и их:

Charlz_Klug 17.07.13 в 10:19

0

(sleep-units% ’.5 ’h)

Charlz_Klug 17.07.13 в 10:21

0

Однако, как правило, символы не вычисляются в самих себя1. Когда Лисп вычисляет символ, то он предполагает, что вы ссылаетесь на переменную и пытается найти значение, ассоциированное с этой переменной в заданном лексическом контексте (если же переменная объявлена как специальная, то в этом случае поиск происходит в динамическом окружении).

Charlz_Klug 17.07.13 в 12:12

0

1 Как правило, нет правил без исключения. Некоторые символы вычисляются в самих себя, например: t, nil и ключевые слова.

Charlz_Klug 17.07.13 в 12:14

0

Если мы хотим избежать закавычивания единицы времени, то нам нужно использовать макрос. В отличие от функции макрос не вычисляет свои аргументы. Для того, чтобы воспользоваться этим фактом, мы заменим функцию sleep-units% макросом sleep-units. Теперь нам не нужно закавычивать единицу времени:

Charlz_Klug 17.07.13 в 15:10

0

Листинг 3.2: SLEEP-UNITS
(defmacro sleep-units (value unit)
‘(sleep
(* ,value
,(case unit
((3) 1)
((m) 60)
((h) 3600)
((d) 86400)
((ms) 1/1000)
((us) 1/1000000)))))

Charlz_Klug 17.07.13 в 15:12

0

(sleep-units .5 h)

Charlz_Klug 17.07.13 в 15:12

0

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

Charlz_Klug 17.07.13 в 15:29

0

(sleep-units 1 (if super-slow-mode ’d ’h))

Charlz_Klug 17.07.13 в 15:30

0

Это не сработает, поскольку sleep-units ожидает что второй аргумент будет одним из символов, в нашем случае вторым аргументом выступает список в котором первым элементом является if.

Charlz_Klug 18.07.13 в 08:47

0

Напомним, что большинство макросов писались для того, чтобы создать более удобные и полезные программные абстракции, и не предназначались для улучшения эффективности нижележащего кода. Возможно ли извлечь некоторые идиомы из этого кода, с тем, чтобы сделать его более полезным для остальной части нашей программы (и, возможно, для наших будущих программ)? Уже сейчас можно предсказать, что мы можем захотеть выполнять разные операции с временными значениями, чем простой вызов sleep. Макрос unit-of-time абстрагирует функциональность из макроса sleep-units, возвращая значение вместо вызова sleep. Параметр value может определяться во время выполнения программы, поскольку он является вычисляемым, но unit не является вычисляемым, поскольку нам нужна информация во время компиляции, также, как и при sleep-units. Вот пример:

Charlz_Klug 18.07.13 в 09:03

0

* (unit-of-time 1 d)
86400

Charlz_Klug 18.07.13 в 09:04

0

Листинг 3.3: UNIT-OF-TIME
(defmacro unit-of-time (value unit)
‘(* ,value
,(case unit
((5) 1)
((m) 60)
((h) 3600)
(Cd) 86400)
((ms) 1/1000)
((us) 1/1000000))))

Charlz_Klug 18.07.13 в 09:04

0

Такой простой макрос как unit-of-time даёт нам более лучший синтаксис для решения конкретных областей некоторых проблем и может значительно улучшить продуктивность работы и корректность решения задачи. В разделе 5.2, Программирование Сверху-Вниз мы продолжим разработку языка единиц. В отличие от большинства языков программирования, в Лиспе вам доступны те же инструменты, что и у людей создавших вашу программную среду. Для того, чтобы реализовать язык Common Lisp, достаточно использовать макросы, этих же макросов достаточно, чтобы реализовать ваш собственный предметно-ориентированный язык.

Charlz_Klug 18.07.13 в 09:36

0

3.3 Управляющие Структуры

Charlz_Klug 19.07.13 в 10:56

0

Хотя эта книга ориентирована на Common Lisp, идеи, описанные здесь можно применить и для языка программирования Scheme. Scheme - это замечательный язык, и хотя в Scheme отсутствуют многие особенности воспринимаемые Лисп программистами как должное, в нём есть достаточно гибкое ядро, которое при необходимости могут расширить профессиональные Лисп программисты 2. Аналогичным образом, Scheme программисты приводят, в качестве преимущества, особенности на которых не делается ставка в Common Lisp. Но, сравнение особенностей всех языков, кроме некоторых особенностей, бессмысленно. Часто мы в состоянии перебросить мост между двумя языками. Само собой разумеется, что в роли моста, с помощью которых мы объединяем два языка выступают макросы.

Charlz_Klug 19.07.13 в 14:53

0

2 В основном, Scheme и Common Lisp различаются в своих сообществах. Scheme программисты любят говорить о том, как здорово иметь короткую спецификацию языка; Common Lisp программисты любят писать программы.

Charlz_Klug 19.07.13 в 14:57

0

Листинг 3.4: NLET
(defmacro nlet (n letargs &rest body)
‘(labels ((,n ,(mapcar #’car letargs)
,@body))
(,n ,@(mapcar #’cadr letargs))))

Charlz_Klug 19.07.13 в 14:58

0

Scheme'овская форма let является более мощной чем её коллега в Common Lisp. Scheme'овская форма let поддерживает нечто, под названием именованный let (named let). В Scheme вы можете вставить символ перед списком привязок let формы и Scheme привяжет функцию под указанным символом вокруг тела let 3. Эта функция принимает новые аргументы для значений, указанных в привязках let, предоставляя очень удобный способ выражения циклов.

Charlz_Klug 20.07.13 в 09:41

0

3 В Scheme есть только одно пространство имён, поэтому функция привязывается именно здесь.

Charlz_Klug 20.07.13 в 09:42

0

К счастью, мы можем построить мост между Scheme и Common Lisp с помощью макроса nlet. Nlet даёт нам возможность писать код в Scheme стиле эмулируя Scheme'овские именованные let'ы. В nlet-fact, nlet используется для определения функции факториала с помощью именованного let:

Charlz_Klug 21.07.13 в 15:31

0

(defun nlet-fact (n)
(nlet fact ((n n))
(if (zerop n)
1
(* n (fact (- n 1))))))

Charlz_Klug 21.07.13 в 15:32

0

Поскольку nlet - это наш первый макрос, сделаем паузу и изучим этот макрос глубже. Иногда для того чтобы понять макрос полезно применить macroexpand к примеру, показывающему использование этого макроса 4. Для этого функции macroexpand мы передадим список, представляющий вызов макроса. Учтите, что macroexpand будет расширять макросы, в которых первый символ списка является макрос символом и не расширяет вложенные макро вызовы 5. Теперь мы скопируем вызов nlet прямо из nlet-fact, закавычим его и передадим в macroexpand:

Charlz_Klug 21.07.13 в 15:46

0

4 Терминология расширения на самом деле довольно неудачна. Никто не говорит что при макрорасширении чего-либо результатом будет больший, расширенный код. Иногда формы расширяются даже в ничто (т.е. nil).

Charlz_Klug 22.07.13 в 06:32

0

5 Но, macroexpand будет продолжать расширение макроса до тех пор, пока первый элемент не будет представлять макрос. Macroexpand-1 полезен для наблюдения первого шага в этом процессе.

Charlz_Klug 22.07.13 в 06:35

0

* (macroexpand
’(nlet fact ((n n))
(if (zerop n)
1
(* n (fact (- n 1))))))
(LABELS ((FACT (N)
(IF (ZEROP N)
1
(* N (FACT (- N 1))))))
(FACT N))
T

Charlz_Klug 22.07.13 в 13:28

0

Расширение использует специальную форму labels, предназначенную для привязки функции вокруг указанного тела. Именованная функция соответствует символу, используемому в форме именованного let. Он получает аргументы значений, привязанных с помощью nlet, в данному случае только n. Поскольку эта функция может быть рекурсивной, nlet реализует полезную итерационную конструкцию.

Charlz_Klug 22.07.13 в 13:34

0

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

Charlz_Klug 22.07.13 в 14:10

0

Но что даёт нам право сказать что nlet - это новая управляющая структура? Управляющая структура - это всего лишь забавный способ определения некоторой конструкции, которая не следует поведению функции. Функция вычисляет каждый аргумент слева направо, привязывает результаты в среде и выполняет машинный код, определённый в некоторой lambda форме. Поскольку nlet не вычисляет напрямую свои аргументы, а вместо этого объединяет их в некоторый кусок Лисп кода, мы изменили выполнение вычисления для nlet форм и тем самым создали новую управляющую структуру.

Charlz_Klug 22.07.13 в 16:37

0

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

Charlz_Klug 23.07.13 в 07:18

0
Минутку...

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