Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

книги / Функциональное программирование

..pdf
Скачиваний:
0
Добавлен:
12.11.2023
Размер:
14.84 Mб
Скачать

Синтаксис определения макроса выглядит так же, как синтаксис используемой при определении функций формы DEFUN:

(DEFMACRO имя лямбда-список тело).

Вызов макроса совпадает по форме с вызовом функции, но его вычисление отличается от вычисления вызова функции. Первое отличие состоит в том, что в макросе не вычисляются аргументы. Тело макроса вычисляется с аргументами в том виде, как они записаны. Такая трактовка аргументов соответствует применяемому в некоторых лисп-системах вычислению NLAMBDA. В «Коммон Лиспе» нет функций, не вычисляющих предварительно свои аргументы, и при необходимости такие формы определяются с использованием макросов.

Второе отличие макроса от функции связано со способом вычисления тела макроса. Вычисление вызова макроса состоит из двух последовательных этапов.

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

На втором этапе эта программно созданная форма вычисляется обычным образом и ее значение возвращается в качестве значения вызова макроса. В этом случае у возникшей формы есть побочный эффект.

Макрос – это форма, которая во время вычисления заменяется на новую, обычно более сложную, форму, которая затем вычисляется обычным образом.

Макросы отличаются от функций и в отношении контекста вычислений. В этом смысле этап расширения макроса ана-

111

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

Рекурсивные макросы и продолжающиеся вычисления

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

Функция тестирования макросов

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

(MACROEXPAND макровызов).

Как при определении и вызове функции в случае макросов в лямбда-списке можно использовать те же ключевые слова

&OPTIONAL, &REST, &KEY и &AUX.

112

Параметр &REST использовался для указания на заранее не определенное количество аргументов. Этот механизм применяется и для определения форм, не все аргументы которых нужно вычислять или аргументы которых желательно обрабатывать нестандартным образом. Например, в обычной форме COND предикаты вычисляются лишь до тех пор, пока не будет получено первое значение, отличное от NIL. Для определения таких форм функции не подходят.

Влямбда-списке макроса кроме ключевых слов, входящих

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

Например, &WHOLE описывает параметр, который связывается со всей формой макровызова. Таким образом, можно из тела макроса кроме аргументов получить и имя самого макроса. Использование параметра &WHOLE в «Коммон Лиспе» в более старых лисп-системах соответствует случаю, когда на месте лямбда-списка был один символьный параметр, который связывался со всем выражением вызова.

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

2.16. Обратная блокировка разрешает промежуточные вычисления

При раскрытии макроса обычно используется большое количество вложенных друг в друга вызовов функций CONS, CAR, CDR, LIST, APPEND и др. Поэтому при построении расширения можно легко ошибиться, а само макроопределение становится менее прозрачным. Для облегчения написания макросов в «Лиспе» принят специальный механизм блокировки вычислений, который называют обратной блокировкой и который

113

; обыкновенный «’» не отменяется
;«`»действуеткакобычная блокировка

помечается в отличие от обычной блокировки (quote) наклоненным в другую сторону (обратным) апострофом «`» (backquote).

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

Пример:

>’(не вычисляется (+ 3 4)

(невычисляется(+34)) >`(можновычислить(+3 4)

можновычислить(+34)

>`(желательновычислить,(+34) ;«,»передвыражениемприводиткеговычислению

(желательновычислить7)

В расширяемом выражении при обратной блокировке выражения с предваряющей запятой заменяются на их значения. Использование предваряющей запятой можно назвать замещающей отменой блокировки. В «Коммон Лиспе» кроме запятой можно использовать запятую вместе со знаком @ (at-sign) присоединения подвыражения. Выражение, перед которым стоит признак присоединения «,@», вычисляется обычным образом, но полученное выражение присоединяется к конечному выражению таким же образом, как это делает функция APPEND. Так внешние скобки списочного значения пропадут, а элементы станут элементами списка верхнего уровня. Такую форму отмены блокировки называют присоединяющей. Приведем пример:

>(setq х `(новые элементы)) (НОВЫЕ ЭЛЕМЕНТЫ)

114

>’(включить,х в список)

; замещающая

(ВКЛЮЧИТЬ (НОВЫЕ ЭЛЕМЕНТЫ) В СПИСОК)

>’(включить,@х в список)

; присоединяющая

(ВКЛЮЧИТЬ НОВЫЕ ЭЛЕМЕНТЫ В СПИСОК) ; отмена Блокировка вычислений и знаки отмены определены в «Лиспе» как макросы чтения. Способы блокировки и ее отмены в конце концов сводятся к иерархическим вызовам функции CONS. Изучить способы формирования выражений можно, задавая интерпретатору выражения, в которых внутри QUOTE содержится

обратная блокировка. Например:

>’`(а (b,c)) ; сначала прямой, потом обратный апостроф В качестве значения получается иерархия вызовов системных функций, вычислению значения которой препятствует

апостроф.

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

Обратная блокировка дает возможность определить раскрытое выражение в виде образца, в котором динамически заполняемые формы помечены запятой. Это помогает избежать сложной комбинации вызовов функций CONS, LIST и др.

Обратная блокировка используется не только в макроопределениях. Например, построение результатов для функции PRINT часто приводит к использованию вызовов CONS, LIST и других функций. Обратной блокировкой мы вновь приближаемся к окончательно выводимому виду:

>(defun добавь-и (х у z) (print `(,x ,y и ,z)))

ДОБАВЬ-И >(добавь-и ‘ниф ‘наф ‘нуф) (НИФ НАФ И НУФ) (НИФ НАФ И НУФ)

ДОБАВЬ-И можно было бы определить и следующим макросом, для которого не нужны апострофы перед аргументами:

115

>(defmacro добавь-и1 (x y z) `(print ‘(,x ,y и ,z)))

ДОБАВЬ-И1 >(добавь-и ниф наф нуф) (НИФ НАФ И НУФ) (НИФ НАФ И НУФ)

Иногда возникает необходимость вычислить только часть выражения. Если (setq b '(x y z)), то запишем

>`(a ,b c) (a (x y z) c)

2.17. Макросы с побочным эффектом

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

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

Обычно интерпретатор «Лиспа» расширяет макровызовы при каждом обращении заново, что в некоторых ситуациях может быть слишком неэффективным. Альтернативным решением может стать программирование такого макроса с привлечением побочного эффекта, который заменяет с помощью псевдофункций (RPLACA, RPLACD, SETF и др.) макровызов на его расширение. Такие макросы нет надобности расширять каждый раз, так как при следующем вызове макроса на месте макровызова будет полученное при первом вызове расширение. Например:

116

>(defmacro первый (&rest x) (cons 'car x))

ПЕРВЫЙ

>(первый '(a b c)) A

>(defun выведи-первый (х) (print (первый x)))

ВЫВЕДИ ПЕРВЫЙ По этим определениям при каждом вызове функции ВЫВЕ-

ДИ-ПЕРВЫЙ производится расширение макроса ПЕРВЫЙ: >(выведи-первый '(a b с))

А

Превратим теперь макрос ПЕРВЫЙ в структуроразрушающий макрос:

(defmacro первый (arg &whole вызов) (rplaca вызов 'car))

Расширение макроса происходит теперь только при первом вызове функции ВЫВЕДИ-ПЕРВЫЙ, которое в качестве побочного эффекта преобразует форму (ПЕРВЫЙ х) в теле ВЫВЕДИПЕРВЫЙ в форму (CAR х). Вычисления соответствуют ситуации, в которой ВЫВЕДИ-ПЕРВЫЙ с самого начала была бы определена в виде

(defun выведи-первый (х) (print (car х)))

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

Макрос ПЕРВЫЙ является примером физически изменяющего вызов макроса. Макрос может физически менять и такие внешние структуры, как функции. Примером такого мак-

117

роса является обобщенная форма присваивания «Коммон Лиспа» SETF. Ее можно определить в виде макроса, который на основе своего первого аргумента определяет, над каким типом данных (или над какой структурой) осуществляется операция присваивания, и расширяется в зависимости от этого в действия, необходимые для данного случая. Поэтому пользователь не должен помнить об особенностях форм присваивания значений различным видам данных.

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

Определения новых форм предложений осуществляются как макросы, которые транслируют их в известные интерпретатору или ранее определенные формы. Например, предложение IF на русском языке

(ЕСЛИ условие ТО р ИНАЧЕ q)

можно было бы определить следующим образом: (defmacro если (условие то р иначе q)

`(if ,условие ,р ,q))

2.18. Представление данных через массивы

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

118

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

(MAKE-ARRAY (n1 n2 … nN) режимы).

Функция возвращает в качестве значения новый объект – массив, n1, п2, …, nN – целые числа, их количество N отражает размерность массива, а значения – размер по каждой размерности. Необязательными аргументами (режимами) можно задать тип элементов массива, указать их начальные значения или придать самому массиву динамический размер. Общий размер массива в этом случае заранее знать и закреплять необязательно.

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

Массив может иметь размерность 0, 1, 2, …. 0-мерный массив состоит ровно из одного элемента.

Для вычислений, осуществляемых с массивами, наряду с функцией создания массива, используются функции для выборки и изменения элементов массива. Ссылка на элемент N-мерного массива осуществляется с помощью вызова

(AREF массив nl n2 …nN),

где n1, n2, …, nN – координаты, или индексы, элемента, на который ссылаются. В качестве функции присваивания используется обобщенная функция присваивания SETF.

Например, следующая директива присваивает 8-му элементу массива data число 12.0:

(setq data (make-array 10)) (setf (aref data 8) 12.0)

119

Доступ производится с помощью функции aref: (AREF <имя> <индекс ячейки>)

>(aref data 8)

Рассмотрим массив testdata: >(setq testdata (make-array 4)) >(setf (aref testdata 1) 'dog)

>(setf (aref testdata 0) 18) >(setf (aref testdata 2) '(a b)) >(setf (aref testdata 3) 4)

# (18 DOG (A B) 4)

>(setq testdata (vector 18 'dog '(a b) 0))

# (18 DOG (A B) 0)

Можно использовать эти данные:

>(cons (aref testdata 1) (list (aref testdata 3) (aref testdata 2))) (dog 0 (a b))

>(aref testdata (aref testdata 3)) 18

Кроме того, в «Коммон Лиспе» существует множество функций для проверки типа и параметров массивов для их преобразования, например для увеличения общего размера.

Кроме привычных массивов с численными индексами можно также работать с хэш-массивами. Хэш-массивы родственны обыкновенному одномерному массиву и ассоциативному списку. Если с помощью массива можно связать лисповские объекты с числами, являющимися индексами, а с помощью ассоциативного списка – с символами, являющимися ключами, то хэш-массив позволяет связать два произвольных лисповских объекта (атомы, списки, строки, массивы, хэш-массивы и т.д.).

Работа с хэш-массивом напоминает работу с обычным (одномерным) массивом с тем лишь отличием, что в позиции индекса при выборке и записи в массив в качестве индекса используется указатель на лисповский объект.

Преимуществом хэш-массивов является быстрота вычислений. Поиск данных, соответствующих ключу, например в ас-

120