DSL / Macro


DSL

マクロ

一言要約

使うな 言語処理を行う前に入力テキストを別のテキストに変換する

要約

仕組み

マクロはプログラミング言語において抽象を構築するための最も古いテクニックの一つで、人気が無く今どきの言語には搭載されていない。

大きくテキストマクロ構文マクロの2つに分けられる。違いはホスト言語の構文構造を把握しているかどうか。最初に学ぶのであればテキストマクロが簡単。

テキストマクロ

ほとんどの言語はマクロをサポートしていないが、m4やVelocityなどのマクロプロセッサを使うことでマクロを利用可能。CSSでの適用例。

div.leftbox { border-bottom-color: #FFB595}
p.head { bgcolor: #FFB595 }

#FFB595のような値を別シンボルとして定義。

div.leftbox { border-bottom-color: MEDIUM_SHADE}
p.head { bgcolor: MEDIUM_SHADE }

マクロをパラメータ化することも可能。C言語での例。

#define max(x,y) x > y ? x : y
  int a = 5, b = 7, c = 0;
  c = max(a,b);

マクロと関数呼び出しの違いは、マクロはコンパイル時に評価される。よってコンパイラはmaxを知ることがない。関数の呼び出しコストを減らすメリットもある。

マクロの悩みは、微妙な問題を多く抱えてしまいがち。二乗マクロの例。

#define sqr(x) x * x
 int a = 5, b = 1, c = 0;
 c = sqr(a + b);

答えは36ではなく11になる。a + (b * a) + bに展開されてしまうため。期待と違う展開をしてしまうことをミス展開(mistaken expansion)と呼ぶ。往々にして発見しづらい。回避方法は括弧をつける。

#define betterSqr(x) ((x) * (x))

別の問題である複数評価(multiple evaluation)を引き起こす例。

#define max(x,y) x > y ? x : y
 int a = 5, b = 1, c = 0;
 c = max(++a, ++b);
 printf("%d",c); // => 7

マクロがネストしてたりするとさらに見つけづらくなり死ねる。

別の問題となる例。

#define cappedTotal(input, cap, result) \
{int i, total = 0; \
for(i=0; i < 5; i ++)  \
  total = total + input[i];\
result = (total > cap) ? cap : total;}

これは上手く動く。

 int arr1[5] = {1,2,3,4,5};
 int amount = 0;
 cappedTotal(arr1, 10, amount);

しかし、これは上手く動かない。

 int total = 0;
 cappedTotal(arr1, 10, total);

変数名totalがカブってるため。このエラーを変数キャプチャ(variable capture)と言う。

構文マクロ

全然人気がなくて、主要な言語ではC++かLispのみ。C++はテンプレートとしてサポートしているけれども、内部DSLとしてC++テンプレートなんて複雑なもん使うな。

Lispはマクロを非常に積極的に利用している言語。Lispプログラマは他言語プログラマに対して上から目線でモノを言うので感じ悪い。

Lispでのマクロ利用のほとんどはクロージャを扱う構文をうまく解決すること。Rubyでのクロージャ例。

aSafe = Safe.new "secret plans"
aSafe.open do
  puts aSafe.contents
end

openは以下のような実装。yieldがポイント。(遅延評価)

 def open
   self.unlock
   yield
   self.lock
 end

これではダメ。

 puts aSafe.openp(aSafe.contents)

Lispでも同様に

(openf-safe aSafe (read-contents aSafe))

ではなく、遅延評価させるために以下のように書く必要がある。

(openf-safe aSafe (lambda() (read-contents aSafe)))

少し冗長なので、マクロですっきりさせる。

(defmacro openm-safe (safe func)
  `(let (result)
     (unlock-safe ,safe)
     (princ (list result ,safe))
     (setq result ,func)
     (lock-safe ,safe)
     result))

(openm-safe aSafe (read-contents aSafe))

Lispでも変数キャプチャの問題は起きる。以下問題例。

 (let (result)
   (setq result (make-safe "secret"))
   (openm-safe result (read-contents result)))

Schemeは健全(hygenic)マクロが搭載されており、Common Lispではgensymで衝突しない変数名を生成する。以下gensymを利用して問題を解決した修正コード。

(defmacro openm-safe2 (safe func)
  (let ((s (gensym))
        (result (gensym)))
     `(let ((,s ,safe))
        (unlock-safe ,s)
        (setq ,result ,func)
        (lock-safe ,s)
        ,result)))

こんなのパッと書けない。

Lispマクロはクロージャ以外での有用な利用方法としてParse Tree Manipulationがある。Common Lispのsetfマクロが良い例。

 (setq aList '(1 2 3 4 5 6))
 (car aList) ; => 1
 (car (cdr aList)) ; => 2
 (rplaca aList 7)
 aList ; => (7 2 3 4 5 6)
 (setf (car (cdr aList)) 8) 
 aList ;  => (7 8 3 4 5 6)

C#にもParse Tree Manipulationをサポートする機能がある。

いつ使うべきか

テキストマクロは一見便利に思えるが、発見しづらい問題を多く抱えてしまう。デバッガなどが高機能になると解決するとかではない根本的な問題があるように思う。

非常にシンプルなケース以外でテキストマクロを使うことはおすすめしない。Templated Generationでは上手くフィットすると思う。

構文マクロにも同じことが言える。そもそもLisp、C++以外は言語自体がサポートしてないから悩む必要なし。他に代替案があるなら、構文マクロはなるべく避けた方がいい。自分があまりC++/Lispに精通してないからかもしれないけれど。

ファウラーへのフィードバック

担当者のつぶやき

  • マクロ人気なさすぎワロタ
  • 現実的に考えると、(現時点では)妥当な考えであると思う
  • あまり得意分野でないせいか、文章の歯切れが悪い。

みんなの突っ込み