今回は本当に簡単なDSL(もどきですが…)を書くということを行ってみます。
(defdsl :foo "foo" :bar "bar" :num 3 :data ["a" "b" "c"])
のようにファイルに書かれたDSLを読みこんで処理することを想定します。
(もちろんこれはClojureのコードになっていますが、用途を限定してデータ入力に特化させてあります。
もっと複雑なことも可能ですが、ここでは行いません。というより私の力ではまだそんなことはできません。)
上のようなファイルを読み込んで処理を行うコードは
(ns sample.core (:use [clojure.contrib.def])) (def ^{:private true} f nil) ;DSL(もどき)が書かれたファイルを読み込む (defn read-file ([file] (try (binding [*ns* (the-ns 'sample.core)] (load-file file) f) (catch java.io.FileNotFoundException _)))) ;DSLの処理を行う (defmacro- defdsl [& args] `(let [m# (hash-map ~@args)] (alter-var-root #'f (fn [_#] ;引数の処理 #'f))
基本的には上のようになります。
知っている人であれば、もはやDSLという感じもしないかもしれません。
ただ、Clojureではこの程度のコードでもDSLのような処理ができてしまうという利点があります。
まず
(def ^{:private true} f nil) ;DSL(もどき)が書かれたファイルを読み込む (defn read-file ([file] (try (binding [*ns* (the-ns 'sample.core)] (load-file file) f) (catch java.io.FileNotFoundException _))))
ですが、これはそれほど問題ではないと思います。
あるとすれば、このコードではbindingで処理を今の名前空間で行うということです。
これによって他のコードから呼び出されても、常に処理はbindingで固定された名前空間となります。
なぜ f を返すのかはつぎのDSLの処理を行うマクロがカギになります。
つぎに行うことはDSLの処理をするためのマクロです。
;DSLの処理を行う (defmacro- defdsl [& args] `(let [m# (hash-map ~@args)] (alter-var-root #'f (fn [_#] ;引数の処理 #'f))
DSLの中核になる部分です。とくに重要なのは
`(let [m# (hash-map ~@args)] ...)
です。
要は、引数で受け取った引数を展開して、ハッシュマップとしています。最初に書いたファイルをこのマクロで展開した場合、このletで束縛するハッシュマップは
{ :foo "foo" :bar "bar" :num 3 :data ["a" "b" "c"]}
と展開されることになります。
このあとread-fileのbindingの中で、ファイルがロードされたときに f に束縛してしまうわけです。
すると、この値はread-fileのbindingの中では、fに束縛されたままになっているので上手にread-fileは返してくれることになります。