Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Lisp: Слезы радости, часть 7

Оригинал: "Lisp: Tears of Joy, Part 7 "
Автор: Vivek Shangari
Дата публикации: December 30, 2011
Перевод: Н.Ромоданов
Дата публикации перевода: 27 октября 2012 г.
Первую статью серии читайте здесь.
Предыдущую статью серии читайте здесь.

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

Макросы — их всегда не хватает!

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

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

Чтобы освежить нашу память, давайте рассмотрим механизм, с помощью которого система Lisp преобразует код, сгенерированный макросом. Вы определяете макрос с помощью формы defmacro; форма defmacro похожа на форму defun, но вместо того, чтобы возвращать значения, форма defmacro возвращает формы на Lisp-е. Ваша программа "вызывает" макрос точно так же, как она вызывает функцию, но поведение совершенно иное.

Во-первых, ни один из параметров макроса никогда не оценивается. Макро параметры присваиваются соответствующим аргументам в макроопределении как символьные значения. Если вы в макрос передаете (* 7 (+ 3 2)), аргументу в теле макроопределения присваивается список символьных значений (* 7 (+ 3 2)), а не значение 35.

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

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

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

Допустим, вы работаете над интерактивной игрой, в которой часто используется тригонометрическая функция sin r в вычислениях движения и взаимодествия игроков. Вы уже определили, что на вызов функции sin в Lisp затрачивается слишком много времени; вы также знаете, что ваша программа будет прекрасно работать с приблизительными результатами вычисления sin r. Вы хотите определить макрос поиска значения lookup-sin, в котором будет находиться таблица, используемая на этапе выполнения программы, и вы также хотите скрыть детали генерации таблицы, которые будут просто загромождать исходный код вашей программы.

Обращение к вашему макросу всегда будет иметь вид (lookup-sin radians divisions), где значение radians всегда находится в диапазоне от нуля до pi, деленного по-полам, а divisions является количеством дискретных значений, которые будут храниться в таблице lookup-sin. Во время выполнения программы макроподстановщик просто преобразует индекс в таблице поиска и возвращает значение, взятое из таблицы. Таблица будет создана на этапе компиляции (в большинстве систем Lisp). Кроме того, для заданного значения divisions в макровызове будет создана только одна таблица.

;; This is where we cache all of the sine tables generated during compilation.
;; The tables stay around at runtime so they can be used for lookups.
(defvar *sin-tables* (make-hash-table)
    "A has table of tables of sine values. The hash is keyed by the number of entries in each sine table.")
 
;; This is a helper function for the lookup-sin macro. It is used only at compile
;; time.
(defun get-sin-table-and-increment (divisions)
    "Returns a sine lookup table and the number of radians quantised by each entry in the table. Tables of a given size are reused. A table covers angles from zero to pi/2 radians."
(let ((table (gethash divisions *sin-tables* :none))
      (increment (/ pi 2 divisions)))
    (when (eq table :none)
        ;; Uncomment the next line to see when a table gets created.
 
        ;; (print `|Making new table|)
        (setq table
            (setf (gethash divisions *sin-tables*)
                  (make-array
                    (1+ divisions)
                    :initial-element 1.0)))
        (dotimes (i divisions)
            (setf (aref table i)
                  (sin (* increment i)))))
     (values table increment)))
 
;; Macro calls the helper at compile time, and returns an AREF form to do the
;; lookup at runtime.
(defmacro lookup-sin (radians divisions)
    "Return a sine value via table lookup."
    (multiple-value-bind (table increment)
                         (get-sin-table-and-increment divisions)
        `(aref, table (round, radians, increment))))

Давайте рассмотрим, что здесь происходит. Когда эта программа работает, она для того, чтобы найти значение sin r, просто выполняет aref (и соответствующее округление).

> (pprint (macroexpand-1 `(lookup-sin (/ pi 4) 50)))
 
(AREF #(0.0D0 0.03141075907812829D0 0.06279051952931338D0 0.09410831331851433D0 0.12533323356430426D0 0.15643446504023087D0 0.18738131458572463D0 0.21814324139654257D0 0.2486898871648548D0 0.2789911060392293D0
        0.3090169943749474D0 0.3387379202452914D0 0.368124552684678D0 0.3971478906347806D0 0.4257792915650727D0 0.4539904997395468D0 0.4817536741017153D0 0.5090414157503713D0 0.5358267949789967D0 0.5620833778521306D0
        0.5877852522924731D0 0.6129070536529765D0 0.6374239897486898D0 0.6613118653236518D0 0.6845471059286887D0 0.7071067811865476D0 0.7289686274214116D0 0.7501110696304596D0 0.7705132427757893D0 0.7901550123756904D0
        0.8090169943749475D0 0.8270805742745618D0 0.8443279255020151D0 0.8607420270039436D0 0.8763066800438637D0 0.8910065241883678D0 0.9048270524660196D0 0.9177546256839811D0 0.9297764858882515D0 0.9408807689542256D0
        ...)
      (ROUND (/ PI 4) 0.031415926535897934D0))

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

> (lookup-sin (/ pi 4) 50)
0.7071067811865476D0

В макрорасширении #(...) используется для выдачи варианта таблицы для 50 дискретных значений первой четверти круга. Эта таблица запоминается в хеш-таблице *sin-tables*, где она используется каждый раз, когда выполняется вызов (lookup-sin angle 50). Нам даже не потребуется осуществлять хэш-поиск во время выполнения программы, поскольку макроподстановшик создает таблицу свободных переменных из формы с несколькими переменными, имеющимися в lookup-sin.

Макросы, которые определяют макросы

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

> (defmacro defsynonym (old-name new name)
"Define OLD-NAME to be equivalent to NEW-NAME when used in the first position of a Lisp form."
 `(defmacro, new-name (&rest args)
                    `(,`,old-name, @args)))
DEFSYNONYM
 
> (defsynonym make-pair cons)
MAKE-PAIR
 
> (make-pair `a `b)
(A . B)

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

В конечном счете, вероятность того, что вы случайно столкнетесь с кодом, который был составлен с использованием старой версии макроса, прямо пропорциональна тому, как часто вы изменяете макрос. Такой макрос, как defsynonym, употребляется снова и снова каждый раз, когда вы создаете новый код. Если вы решите изменить некоторое старое название на новое, весь ранее откомпилированный код будет продолжать использовать старое имя, которое вы выбрали ранее.

Здесь я оставлю вас и позволю получить удовольствие от использования собственных макросов.

В следующей статье мы рассмотрим систему Common Lisp Object System (CLOS), которая позволяет создавать очень сложные объектно-ориентированные решения. Если вам нужен код, в котором строго соблюдаются все требования объектно-ориентированной разработки, вы, вероятно, найдете в Common Lisp все функциональные возможности объектно-ориентированного языка. В CLOS есть много дополнительных возможностей объектно-ориентированного программирования, которые вы не сможете найти в других местах. Поэтому CLOS часто используется как инструмент изучения принципов ООП.

Продолжение следует...