LoginSignup
6
4

More than 5 years have passed since last update.

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

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4