пятница, 21 января 2011 г.

Когда нужны макросы


В 8-й главе книги «On Lisp» Пол Грэм рассуждает о том, когда и почему нужно использовать макросы, а не функции. Два пункта интуитивно понятны: вычисление условных выражений и множественные вычисления. Они позволяют создавать конструкции условных переходов и циклы.
Но для меня наиболее интересными представляются следующие случаи: создание лексического окружения и трансформация кода. Рассмотрим их на примере простого DSL.
Предположим, необходимо дополнить язык Clojure синтаксическим конструкциями для описания интерфейса пользователя. То, что обычно делают графические рисовалки интерфейсов в монстрозных IDE, нужно выразить в виде такого кода:

(defn pushme [] (println "You have pushed the button!")) 

(create-window mywin
  (label mylabel "Test label")
  (button mybutton "Pushme!" pushme))

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

(defmacro create-window [name & funcs]
   `(def ~name
      (let [my-win# {:name ~(str name)}]
         (-> my-win# ~@funcs)))) 

(defmacro label [win name title]
  `(assoc (assoc ~win
     ~(keyword (str name "-type")) "label")
     ~(keyword (str name "-title")) ~title))

(defmacro button [win name title action]
  `(assoc (assoc (assoc ~win
    ~(keyword (str name "-type")) "button")
    ~(keyword (str name "-title")) ~title)
    ~(keyword (str name "-action")) ~action))

Как видно из кода, здесь активно используется возможность создания ключевых слов для map-а при помощи конструкции (keyword (str name «-something»)). Параметр name передается в виде обыкновенного символа Clojure; перед ним не надо ставить двоеточие или заключать его в кавычки. Такая трансформация name-а возможна потому, что макрос принимает свои аргументы «как есть», не вычисляя их. Поэтому неважно, что символ name не только ни к чему не привязан, но даже и не объявлен.
Удобным инструментом для создания фрагментов DSL является функция ->. Она позволяет перенаправлять выход одной функции на вход другой первым аргументом. Таким образом, легко строить декораторы функций. Свойства окна удобно перечислить «в столбик», а декоратор вызовет последовательно все заданные функции. Например:

(defn ok-pushed [] (println "You have pushed the OK button!"))
(defn cancel-pushed [] (println "You have pushed the Cancel button!"))

(create-window mywin
   (label confirmation "Do you really want to delete the selected files?")
   (button ok-button "OK" ok-pushed)
   (button cancel-button "Cancel" cancel-pushed))

Созданный map выглядит так.

user=> mywin
{:cancel-button-action #<user$cancel_pushed user$cancel_pushed@2c17f7>, :cancel-button-title "Cancel", :cancel-button-type "button", :ok-button-action #<user$ok_pushed user$ok_pushed@33788d>, :ok-button-title "OK", :ok-button-type "button", :confirmation-title "Do you really want to delete selected files?", :confirmation-type "label", :name "mywin"}

Теперь можно даже «нажать» на кнопку.

user=> ((:cancel-button-action mywin))
You have pushed the Cancel button!

Подводя итоги, могу сказать, что макрос – незаменимый инструмент для трансформации кода. Особенно полезным считаю то, что эта трансформация происходит во время компиляции программы, а не во время исполнения. Тормоза во втором случае известны всем любителям ruby и python.

Комментариев нет:

Отправить комментарий