tools.macroを使ってLet Over Lambdaのパンドリックマクロを実装する

(このエントリは Clojure Contrib Library Advent Calendar 18日目の記事です。)

はじめに

今回のネタはtools.macroです。
tools.macroは,ライブラリのREADMEによれば「マクロを書くためのツール」という位置づけで,現在提供している主要な機能はmacroletとsymbol-macroletです。これらは,Common Lispの同名の特殊形式と同じように,それぞれローカルマクロとシンボルマクロを定義するのに使うことができます。Clojureは,(少なくとも現時点では)ローカルマクロやシンボルマクロを言語としてサポートしていないため,そういった機能が必要な複雑なマクロを定義する場合等にはtools.macroのマクロを使うのが便利です。

インストール

2013年12月現在のtools.macroの最新版は0.1.2です。tools.macroをインストールするには,Leiningenを使用している場合はproject.cljの:dependenciesに以下を追加します。

[org.clojure/tools.macro "0.1.2"]

使い方

macroletはletfnと同じ形式でローカルマクロを定義します。

(defn foo [x flag]
  (macrolet [(f [x]
               `(if ~'flag (* ~x ~x) ~x))]
    (+ x
       (f x)
       (f (+ x 1)))))

これは以下と等価です。

(defn foo [x flag]
  (+ x
     (if flag (* x x) x)
     (if flag (* (+ x 1) (+ x 1)) (+ x 1))))

symbol-macroletはletと同じ形式でシンボルマクロを定義します。

(symbol-macrolet [x 'foo]
  (list x (let [x 'bar] x)))

これは以下のように展開されます。

(list 'foo (let [x 'bar] x))

この展開結果を見ても分かるように,macroletやsymbol-macroletはマクロと同名のシンボルが出現する箇所を盲目的に置き換えるのではなく,Clojureの構文を理解してマクロの展開が可能な位置を認識します。コードウォークが必要なマクロを定義する場合は,この観点から,むやみにclojure.waok/postwalkなどの関数を使うのではなく,macroletやsymbol-macroletを使う方が安全です。

ここでは,シンボルマクロの応用例として,Let Over Lambdaに登場するパンドリックマクロを実装してみます。

パンドリックマクロ

パンドリックマクロ(pandoric macros)とは,クロージャが捕捉しているローカルな環境に,その環境の外側から(見かけ上)アクセスできるようにするマクロです。本来,外側からはアクセスできないはずのローカル環境に無理矢理アクセスする禁忌を,「パンドラの箱」を開けることになぞらえてこの名前がつけられています。

外側からローカル環境にアクセス可能なクロージャを作るためにはpfnマクロを使います。

user=> (def f (let [x 42] (pfn [y] [x] (* x y))))
#'user/f
user=> (f 2)
84
user=>

pfnの第1引数は通常の引数リストです。第2引数には外からアクセス可能にする変数を指定します。上の例ではクロージャが捕捉するローカル変数xをアクセスできるようにしています。pfnで生成されたクロージャはもちろん通常のクロージャと同じように関数適用できます。

pfnでアクセス可能にした変数の値を取得するにはwith-pandoricマクロを使います。

user=> (with-pandoric [x] f
  #_=>   (println "the value of closed variable x is:" x))
the value of closed variable x is: 42
nil
user=> 

with-pandoricマクロの第1引数では,pfnで作ったクロージャでアクセス可能にした変数のうち,with-pandoricのボディで実際にアクセスする変数を指定します。第2引数にはpfnで作ったクロージャを渡します。上の例では,fで捕捉された変数xの値42がwith-pandoric内で取得できています。

実装

このようなマクロpfnおよびwith-pandoricをシンボルマクロを使って作ってみましょう。実装は以下のようになります:

(ns contrib-calendar.tools.macro
  (:require [clojure.tools.macro :refer [symbol-macrolet]]))

(defmacro pfn [args pargs & body]
  `(let [f# (fn ~args ~@body)]
     (fn [& args#]
       (if (= (first args#) ::pandoric-get)
         (case (second args#)
           ~@(mapcat #(list % %) pargs)
           (throw (Exception. (str "Unknown pandoric get: " (second args#)))))
         (apply f# args#)))))

(defmacro with-pandoric [syms f & body]
  (let [gf (gensym)]
    `(let [~gf ~f]
       (symbol-macrolet [~@(mapcat #(list % `(~gf ::pandoric-get '~%)) syms)]                                                                                                    
         ~@body))))

パンドリックマクロの使用例

pfnとwith-pandoricを使って,リセット可能なカウンタを作ってみましょう。

(defn make-counter []
  (let [x (atom 0)]
    (pfn [] [x]
      (swap! x inc)
      @x)))

これを以下のように使います。

user=> (def c (make-counter))
#'user/c
user=> (c)
1
user=> (c)
2
user=> (c)
3
user=> (with-pandoric [x] c
  #_=>   (reset! x 0))
0
user=> (c)
1
user=> (c)
2
user=> 

おわりに

今回はtools.macroが提供するmacroletとsymbol-macroletを紹介しました。また,シンボルマクロの使用例としてLet Over Lambdaのパンドリックマクロを実装しました。

ここではmacroletの実用例については詳しく見ていませんが,(手前味噌ですが)たとえば以下のようなマクロが参考になるかもしれません。

trampolineを使わないで相互末尾再帰を末尾再帰最適化するマクロ
optimizing mutual tail recursion without trampoline
ローカル関数をメモ化再帰できるようにするマクロ
ローカルな関数でメモ化再帰できるようにするマクロをClojureで - Qiita [キータ]