Lispの方言のひとつであるClojureは『実践プログラミングDSL』でいうところの「コンパイル時メタプログラミング」に優れている、ということでLispマクロを活用したごく簡単な DSL(domain-specific language) を実装してみた。
基礎となるデータ構造を定義
ここでは、キャラクター(しかもschool idol限定)のプロフィールデータを管理したいという想定で Profile
レコードを定義してみる。
;; データ構造(レコード)定義
(defrecord Profile [name
birthday
bloodtype
height
favorites
unfavorites
cv
school
grade
group
unit])
ちなみに、Profileレコードのフィールド cv
は我々の業界ではもちろん conversion
(コンバージョン ※インターネット広告/マーケティング業界用語)、ではなく character voice
(※和製英語)のこと(CVと言ったらキャラボイスのことだよね、常識的に考えて)。
操作用の関数を定義
Profileレコードも抽象的にはAssociativeな構造とSequentialな構造で構成されているので、特定のレコード型に依存しない一般的な関数として、レコードデータをJSON変換する ->json
、XML変換する ->xml
という関数を定義してみる。
せっかくなので、前回記事(継承によらないポリモーフィズム実現手法)でも紹介したClojureの マルチメソッド(multimethod) を利用して簡単に(雑に)実装してみた。
もっと仕様に対して正確で賢い実装があるはず……。
;; JSONへのフォーマット変換(略式実装)
(defmulti ->json class)
(defmethod ->json clojure.lang.MapEntry [[k v]]
(str (->json (name k)) ": " (->json v)))
(defmethod ->json clojure.lang.Associative [a]
(str \{ (clojure.string/join ", " (map ->json a)) \}))
(defmethod ->json clojure.lang.Sequential [s]
(str \[ (clojure.string/join ", " (map ->json s)) \]))
(defmethod ->json String [s]
(str \" s \"))
(defmethod ->json Object [x]
x)
(prefer-method ->json clojure.lang.Sequential clojure.lang.Associative)
;; XMLへのフォーマット変換(略式実装)
(defmulti ->xml class)
(defmethod ->xml clojure.lang.MapEntry [[k v]]
(str \< (name k) \> (->xml v) "</" (name k) \>))
(defmethod ->xml clojure.lang.Associative [a]
(clojure.string/join (map ->xml a)))
(defmethod ->xml clojure.lang.Sequential [s]
(clojure.string/join (map (comp #(str "<elem>" % "</elem>") ->xml) s))) ; リスト要素はここでは決め打ちで <elem>...</elem>
(defmethod ->xml Object [x]
x)
(prefer-method ->xml clojure.lang.Sequential clojure.lang.Associative)
この種のデータフォーマット変換自体は本来、特別な理由がなければ独自実装せずdata.jsonなどのライブラリを利用したほうが良いと思う。
DSLユーザー向けにシンタックスシュガーを提供
ここからがいよいよ本題。
DSLのユーザーに直感的で負担の少ないシンプルなシンタックスを提供できないか、考えてみる。
第1段階
まず、上記で用意した Profile
レコードのデータをトップレベルに定義するには、例えば以下のように書く必要がある。
(def umi
(map->Profile
{:name "園田 海未"
:birthday "March-15"
:bloodtype "A"
:height 159
:favorites ["穂乃果の家のまんじゅう"]
:unfavorites ["炭酸飲料"]
:cv "三森 すずこ"
:school "国立音ノ木坂学院"
:grade 2
:group "μ's"
:unit "lily white"}))
;; cf. http://dic.pixiv.net/a/園田海未
位置引数を取るファクトリ関数 ->Profile
よりもMapデータを引数とする map->Profile
のほうがフィールドデータの順序や有無に依存せず柔軟性が高いものの、シンタックス面で若干のノイズがある。
そこで、DSLユーザーのためにトップレベル定義に特化したよりシンプルなシンタックスを提供するために、以下のようなマクロを定義してみる。
;; シンタックスシュガー(第1段階)
(defmacro defprofile [name & body]
`(def ~name
(map->Profile (hash-map ~@body))))
すると、レコードデータの定義は以下のように書けるようになる。
(defprofile umi
:name "園田 海未"
:birthday "March-15"
:bloodtype "A"
:height 159
:favorites ["穂乃果の家のまんじゅう"]
:unfavorites ["炭酸飲料"]
:cv "三森 すずこ"
:school "国立音ノ木坂学院"
:grade 2
:group "μ's"
:unit "lily white")
第2段階
さらにもう一歩進んで、レコードフィールドのデータのうち文字列を指定する際に利用している "
(double quote) も構文的に曖昧にならない範囲で必要最小限にできないか考えてみる(ちょっとやり過ぎな気もするが)。
つまり、次のように適宜 "
を省略して書けたらDSLユーザーはうれしいかもしれない。
(defprofile umi
:name "園田 海未"
:birthday March-15
:bloodtype A
:height 159
:favorites [穂乃果の家のまんじゅう]
:unfavorites [炭酸飲料]
:cv "三森 すずこ"
:school 国立音ノ木坂学院
:grade 2
:group μ's
:unit "lily white")
ということで、再びLispマクロの力を借りて以下のように実装してみた。
;; シンタックスシュガー(第2段階)
(defn symbol->str [v]
(cond
(symbol? v) (name v)
(coll? v) (map symbol->str v)
:else v))
(defmacro profile-body [& kvs]
`(reduce (fn [m# [k# v#]] (assoc m# k# (symbol->str v#)))
{}
(partition 2 '~kvs)))
(defmacro defprofile [name & body]
`(def ~name
(map->Profile (profile-body ~@body))))
DSLを使ってみる
データ構造(レコード)、操作用関数、シンタックスシュガーとしてのマクロで構成されたシンプルなDSLを実際に利用してみる。
レコードデータの定義
(defprofile umi
:name "園田 海未"
:birthday March-15
:bloodtype A
:height 159
:favorites [穂乃果の家のまんじゅう]
:unfavorites [炭酸飲料]
:cv "三森 すずこ"
:school 国立音ノ木坂学院
:grade 2
:group μ's
:unit "lily white")
;; cf. http://dic.pixiv.net/a/園田海未
(defprofile you
:name "渡辺 曜"
:birthday April-17
:bloodtype AB
:height 157
:favorites [ハンバーグ みかん]
:unfavorites [刺身 パサパサした食べ物]
:cv "斉藤 朱夏"
:school 私立浦の星女学院
:grade 2
:group Aqours
:unit CYaRon!)
;; cf. http://dic.pixiv.net/a/渡辺曜
レコードデータのJSON変換
> (->json [umi you])
[{"name": "園田 海未", "birthday": "March-15", "bloodtype": "A", "height": 159, "favorites": ["穂乃果の家のまんじゅう"], "unfavorites": ["炭酸飲料"], "cv": "三森 すずこ", "school": "国立音ノ木坂学院", "grade": 2, "group": "μ's", "unit": "lily white"}, {"name": "渡辺 曜", "birthday": "April-17", "bloodtype": "AB", "height": 157, "favorites": ["ハンバーグ", "みかん"], "unfavorites": ["刺身", "パサパサした食べ物"], "cv": "斉藤 朱夏", "school": "私立浦の星女学院", "grade": 2, "group": "Aqours", "unit": "CYaRon!"}]
綺麗にフォーマットすると、
[
{
"name": "園田 海未",
"birthday": "March-15",
"bloodtype": "A",
"height": 159,
"favorites": [
"穂乃果の家のまんじゅう"
],
"unfavorites": [
"炭酸飲料"
],
"cv": "三森 すずこ",
"school": "国立音ノ木坂学院",
"grade": 2,
"group": "μ's",
"unit": "lily white"
},
{
"name": "渡辺 曜",
"birthday": "April-17",
"bloodtype": "AB",
"height": 157,
"favorites": [
"ハンバーグ",
"みかん"
],
"unfavorites": [
"刺身",
"パサパサした食べ物"
],
"cv": "斉藤 朱夏",
"school": "私立浦の星女学院",
"grade": 2,
"group": "Aqours",
"unit": "CYaRon!"
}
]
レコードデータのXML変換
> (->xml [umi you])
<elem><name>園田 海未</name><birthday>March-15</birthday><bloodtype>A</bloodtype><height>159</height><favorites><elem>穂乃果の家のまんじゅう</elem></favorites><unfavorites><elem>炭酸飲料</elem></unfavorites><cv>三森 すずこ</cv><school>国立音ノ木坂学院</school><grade>2</grade><group>μ's</group><unit>lily white</unit></elem><elem><name>渡辺 曜</name><birthday>April-17</birthday><bloodtype>AB</bloodtype><height>157</height><favorites><elem>ハンバーグ</elem><elem>みかん</elem></favorites><unfavorites><elem>刺身</elem><elem>パサパサした食べ物</elem></unfavorites><cv>斉藤 朱夏</cv><school>私立浦の星女学院</school><grade>2</grade><group>Aqours</group><unit>CYaRon!</unit></elem>
綺麗にフォーマットすると、
<elem>
<name>
園田 海未
</name>
<birthday>
March-15
</birthday>
<bloodtype>
A
</bloodtype>
<height>
159
</height>
<favorites>
<elem>
穂乃果の家のまんじゅう
</elem>
</favorites>
<unfavorites>
<elem>
炭酸飲料
</elem>
</unfavorites>
<cv>
三森 すずこ
</cv>
<school>
国立音ノ木坂学院
</school>
<grade>
2
</grade>
<group>
μ's
</group>
<unit>
lily white
</unit>
</elem>
<elem>
<name>
渡辺 曜
</name>
<birthday>
April-17
</birthday>
<bloodtype>
AB
</bloodtype>
<height>
157
</height>
<favorites>
<elem>
ハンバーグ
</elem>
<elem>
みかん
</elem>
</favorites>
<unfavorites>
<elem>
刺身
</elem>
<elem>
パサパサした食べ物
</elem>
</unfavorites>
<cv>
斉藤 朱夏
</cv>
<school>
私立浦の星女学院
</school>
<grade>
2
</grade>
<group>
Aqours
</group>
<unit>
CYaRon!
</unit>
</elem>
まとめ
- Lispマクロを活用すればシンタックスシュガーを簡単に提供できる
- メタプログラミングを上手く利用できればコードのreadabilityやsimplicityも向上する(はず)
Further Reading
-
『実践プログラミングDSL』
- 4.5 生成型DSL:マクロによるコンパイル時のコード生成
- 5.4 Clojureを使って考え方を変える