воскресенье, 30 января 2011 г.

Анафорические макросы на Clojure


Во все той же незабвенной «On Lisp» Пола Грэма в главе 14 рассматривается такое понятие, как анафорический макрос. Анафора в естественном языке – термин, означающий ссылку на что-то в предшествующем разговоре. В языке программирования анафора – отсылка к предыдущим результатам вычислений. Например.

(defn my-long-calculation [param] param)

(aif (my-long-calculation 15)
   (println result)
   (println "The result is nil!")

Здесь aif – анафорический if, который результат вычисления функции my-long-calculation присвоит переменной result. Соответственно, result здесь – это анафора, которая ссылается на результат вычислений. Кроме того, ветка then здесь будет выполнена только в случае, когда result не равен nil, иначе управление перейдет ветке else.
Забавный, в общем макрос. Тем более, что, обратите внимание, пользователь переменную result не создает, это делает за пользователя макрос aif. Но в Clojure его нет, и нам предстоит его написать.
Проблема в том, что в Clojure захват лексической привязки result – очень неидиоматичное решение. Мне даже не удалось найти стандартного способа это сделать. Например.

(defmacro a [body]
   `(let [b 15]
      ~@body))

(a (println b))

Здесь Clojure создаст исключение «Can't let qualified name: user/b». Можно, конечно, воспользоваться gensym:

(defmacro a [body]
   (let [b (gensym "b")]
     `(let [~b 15]
       ~@body)))

(a (println b))

Здесь ошибка уже «Unable to resolve symbol: b in this context», потому что gensym генерирует уникальное имя во избежание захвата переменной (variable capture).
Создадим свою версию gensym, результатом которой было бы предсказуемое имя переменной.

(defn make-sym [name] (. clojure.lang.Symbol (intern (str name))))

Теперь можно написать анафорический макрос aif.

(defmacro aif
  ([expr then]
     (let [result (make-sym "result")]
       `(let [~result ~expr]
           (if (not (nil? ~result))
            ~then))))
  ([expr then else]
     (let [result (make-sym "result")]
      `(let [~result ~expr]
          (if (not (nil? ~result))
           ~then
           ~else)))))

Одним словом, анафорические макросы в Clojure писать так же легко, как и любые другие.

5 комментариев:

  1. Если передать в макрос имя требуемой переменной,
    то он (макрос) сокращается до неприлично малого.

    (defmacro aif-2 [var expr then else]
    `(if-let [~var ~expr] ~then ~else))

    (aif-2 x (* 2 2)
    (println x)
    (println "NULL"))

    ОтветитьУдалить
  2. Точно!
    Ну а в моем варианте фишка в том, что имя переменной не передается, а подразумевается. Convention over configuration, так сказать :-)

    ОтветитьУдалить
  3. ;; -- why only lexical ?
    ;; (macroexpand-1 '(aif (+ 2 1) (* 2 it) 'nok))
    ;; (aif (+ 2 1) (* 2 it) 'nok)
    ;; (aif nil (* 2 it) 'nok)
    (def it nil)
    (defmacro aif [test body else]
    `(binding [it ~test] (if it ~body ~else)) )

    ;; -- symbol is the function you want !
    ;; (macroexpand-1 '(aif2 (+ 2 1) (* 2 it) 'nok))
    ;; (aif2 (+ 2 1) (* 2 it) 'nok)
    ;; (aif2 nil (* 2 it) 'nok)
    (defmacro aif2 [test body else]
    (let [it (symbol "it")]
    `(let [~it ~test] (if ~it ~body ~else)) ))

    my 2 cents ...

    ОтветитьУдалить
  4. Yep, that really works, thanks for simplifying the macro.
    The only question is: how did you manage to read my blog? It's totally in russian and you (according to your profile) speak French. I'm totally sure that google translate makes a crappy translation from russian to English or French.
    Actually I'm pleased that my post has attracted your attention. I'm also interested in your blog about clojure (http://clojadventure.blogspot.com/), trying to read it in translation to English.

    ОтветитьУдалить
  5. Yes i'm using google translate to read your blog. It's not perfect but enough to understand main ideas :).

    ОтветитьУдалить