What is it, naokirin?

Clojureで簡易DSLを書いてみよう

今回は本当に簡単な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は返してくれることになります。