Scheme:マクロの効用

Scheme:マクロの効用

普通のやつらの上を行けでLispにおけるマクロの効用が述べられているが, 「じゃあ具体的にマクロを使って『こりゃ便利だ』っていう例を見せてよ」 と言われてもなかなかすぐに出て来ない。 そこで、なんかいいマクロの使用例を思い付いたら書いてってみるコーナー。

もくじ

関連:Scheme:マクロの危険


マクロを使う局面

なんとなく、マクロを使いたくなる理由というのが分類できるような 気がしてきたので書いてみる。もちろんそれぞれの理由は排他的でなく、 またオーバーラップするものもあるけれど。

定型パターンの簡略化

繰り返し似たようなコードを書かなければならず、 しかも関数による抽象化がやりにくい場合、あるいは性能の問題から 関数による抽象化をやりたくない場合。

Cの引数付きマクロは多くの場合、このケースに相当するように思う。 但し、Cの場合は関数による抽象化に厳しい制限があるため、 マクロにせざるを得ない場合が多くあるが、 Schemeではlambdaを書くのを厭わなければ高階関数による抽象化だけで済むことも多い。

Gaucheのlib/gauche/listutil.scmの%define-cxrマクロなんかがこの例。 マクロを使わないと、本質的でない要素が繰り返し並んで見づらくなる。

関数呼び出しのインライン展開の代用

関数呼び出しのように見せかけて、実はその場でコードが展開されるので 呼び出しが一回減ってお得、というやつ。

但し、処理系によって最適化の方策は異なるので、なんでもかんでも マクロにすれば速くなるとは限らない。 ナイーブなインタプリタ(ほとんど無いと思うけど)では評価の度にマクロが展開 されるかもしれない。 モジュールを解釈するコンパイラなら、逆にモジュール内ローカルの 関数定義は勝手にインライン展開してくれるかもしれない。

よって、「べき論」で言えばこのパターンは多用すべきではない。 ただし、特定の処理系のコンパイラの癖を熟知した上で、 特定のプログラムにチューニングをかけてゆく時には チューニング手段としてのマクロは大いに活躍する。 「コンパイラが最適化しやすいパターンのコード」と 「人間が読んでわかりやすいコード」が違う場合に、 マクロによって後者を前者に変換してやるのだ。

新しい構文による抽象化

新しい構文の導入は「定型パターンの簡略化」であることも多いけど、 着目したい要素を目立つように配置する、というような目的もある。

たとえば (dolist (elt lis) body ...) は (for-each (lambda (elt) body ...) lis) と等価だけれど、 bodyが長くて、かつリストによる繰り返しがネストするような場合は、 dolistの方が見やすい。

ミニ言語の埋め込み

「新しい構文による抽象化」をさらに進めた形で、 新たなシンタクスとセマンティクスを持つミニ言語をプログラム中に 埋め込むのにもマクロが使える。

具体例にあげる、リストの内包表記を実現するlist-ofなんかは このパターンの控えめな例。 Schemeにprologを埋め込むSchelogや、 処理系BiglooにあるLALR grammerなんかもこれ。

Common Lispのloopマクロは一番極端な例かな。ほとんど独立した埋め込み言語と 化している。

宣言的なコードに見せる

このパターンもよく使うな。 本当はランタイムに手続きが走っていろいろやっているんだけど、 宣言的に書けたほうが整理しやすい場合がある。

Wilikiの macro.scm では、define-writer-macro, define-reader-macro, define-virtual-pageのような 構文で新しいマクロを定義できるようにしている (ソースの後半参照)。 実体は実行時にマクロ手続きを作ってグローバルなリストに登録しているだけなんだが、 見掛け上宣言的なので、マクロを「定義している」って気分になる。

オブジェクトシステムのdefine-classやdefine-methodもそう。 実体はクラスオブジェクトを作って変数にbindしてたり、 ジェネリックファンクションにadd-method!してるんだけど、 見掛けが宣言的だと「オブジェクト指向言語」っぽく見えるし、 実際そういうつもりで使って何ら問題はない。

コンパイル時の処理

これは処理系特有の事情なんだが、 実行時ではなくコンパイル時に何か処理をしたいという場合がある。 マクロは一般にコンパイル時に展開されるので、 マクロの展開コードの中に処理を仕込んでおくという方法がある。 極端な場合、展開されたコードそのものには意味はないので、 展開結果は#fだけ、という場合だってある。

もっとも、これは激しく処理系依存なので、どっちかっていうと その処理系特有の裏事情を表に見せないようにこっそり処理する ような形で使うのが吉かな。Gaucheのadd-load-pathとかrequireとかで使っている。

Common Lispではeval-whenという構文が仕様にあって、 ちゃんとポータブルにそのへんが書けるようになってる。


具体例

クラス定義でのリーダーの生成

高林さんの いやな日記(3/15) に、Rubyでインスタンス変数がたくさんあるときにattr_readerを たくさん書くのが面倒、という話題が出てた。Rubyでは、reflectionを 使って生成時にevalでリーダーの定義をしてゆくという方法が挙げられている。

Lisperだとこういうときはマクロを使う。Gaucheの構文だと

  (define-macro (define-class* name supers slots . options)
    `(define-class ,name ,supers
       ,(map (lambda (slot)
               (let ((reader-name (string->symbol #`"get-,(car slot)")))
                 (list* (car slot) :getter reader-name (cdr slot))))
             slots)
       ,@options))

通常のdefine-classではなくdefine-class* を使ってクラス定義をすると、 各スロットに対して自動的に get-スロット名 という名のリーダーメソッド が定義される。

  (define-class* <foo> ()
    ((foo :init-value 1)
     (bar :init-value 2)
    ))

  (define foo (make <foo>))
  (get-foo foo) ==> 1
  (get-bar foo) ==> 2

さらに、もとの定義で :read-only というオプションがついていたらリーダー メソッドのみ、そうでなければライターメソッドも定義するようにしてみる。

  (define-macro (define-class* name supers slots . options)
    `(define-class ,name ,supers
       ,(map (lambda (slot)
               (let ((reader-name (string->symbol #`"get-,(car slot)"))
                     (writer-name (string->symbol #`"set-,(car slot)!")))
                 (if (get-keyword :read-only (cdr slot) #f)
                     (list* (car slot) :getter reader-name (cdr slot))
                     (list* (car slot) :getter reader-name
                                       :setter writer-name (cdr slot)))))
             slots)
       ,@options))

  (define-class* <foo> ()
    ((foo :init-value 1)
     (bar :init-value 2 :read-only #t)
    ))

  (define foo (make <foo>))
  (get-foo foo) ==> 1
  (get-bar foo) ==> 2
  (set-foo! foo 8) ==> #<undef>
  (get-foo foo) ==> 8
  (set-bar! foo 8) ==> error

なお、この例に関してはMetaobject Protocol(MOP)を使っても可能で、 むしろ拡張性を考えるとMOPの方が好ましい。 Scheme:メタオブジェクトプロトコルで考えてみる。


リストの内包表記 (List comprehension)

Haskellとか、最近ではPythonも採り入れたlist comprehension。 これをSchemeのマクロで実現するコードが最近 comp.lang.schemeに投稿されていました。

コード (これだけ。ちなみにfold-leftはSRFI-1でfoldという名前で定義されています)。

 (define (fold-left f b l) 
   (if (null? l) b (fold-left f (f b (car l)) (cdr l))))
 (define-syntax list-of
   (syntax-rules ()
     ((list-of expr ...)
         (reverse (list-of-tail '() expr ...)))))
 (define-syntax list-of-tail
   (syntax-rules (in)
     ((list-of-tail base expr)
          (cons expr base))
     ((list-of-tail base expr (var in generator) rest ...)
          (let* ((f (lambda (z var) (list-of-tail z expr rest ...))))
             (fold-left f base generator)))
     ((list-of-tail base expr pred? rest ...)
          (if pred? (list-of-tail base expr rest ...) base))))

以下は実行例

 (list-of (* x x) (x in '(1 2 3 4 5))) => (1 4 9 16 25)

 (define (upto a b) (if (< b a) '() (cons a (upto (+ a 1) b))))

 (upto 1 5) => (1 2 3 4 5)

 (define (squares n) (list-of (* x x) (x in (upto 1 n))))

 (squares 5) => (1 4 9 16 25)

 (define (qsort lt? lst)
    (if (null? lst) '()
      (append
        (qsort lt? (list-of x (x in (cdr lst)) (lt? x (car lst))))
        (list (car lst))
        (qsort lt? (list-of x (x in (cdr lst)) (not (lt? x (car lst))))))))

 (qsort < '(3 1 4 1 5 9 2 6 5 4)) => (1 1 2 3 4 4 5 5 6 9)

 (define (pyth n)
    (list-of (list a b c)
      (a in (upto 1 n))
      (b in (upto a n))
      (c in (upto b n))
      (= (+ (* a a) (* b b)) (* c c))))

 (pyth 20) => ((3 4 5) (5 12 13) (6 8 10) (8 15 17) (9 12 15) (12 16 20))

Haskellと違ってeagerな評価になってしまうのが欠点ですが… delayを使ってlazyにすることはできるでしょう。

Scheme:TaxiNumberも参照。

        (define (qsort lt? lst)
          (if (null? lst)
              '()
              (append
                (qsort lt? (let-zf ((x (cdr lst)) (lt? x (car lst))) x)
                (list (car lst))
                (qsort lt? (let-zf ((x (cdr lst)) (not (lt? x (car lst)))) x)))))

リストの構築

わりと良く出会うパターンである。こういうリストを作りたい。

もちろん実際にはこういう条件の数はもっとたくさんあるし、 procNの部分もxNの部分もごちゃっとした式が入ると思いねえ

代表的なのは、条件によってメニュー項目を出したり引っ込めたりとか、 動的なHTML生成で条件によってリンクを表示したりしなかったりとかする場合だ。

手続き的な方法なら素直に書ける。

(let ((r '()))
  (when (proc1) (push! r x1))
  (when (proc2) (push! r x2))
  (when (proc3) (push! r x3))
  (reverse! r))

でも、リストといういわばLispの土俵であるデータ構造で、 副作用を使うのはちょっと悔しい。

関数的に書けなくはないんだが、なんか素直にならないんだこれが。

バッククオート内で ,@ を使うと、空リストは「なかったこと」になるので、 それを利用する方法:

`(,@(if (proc1) (list x1) '())
  ,@(if (proc2) (list x2) '())
  ,@(if (proc3) (list x3) '())
  )

しかしif式の戻り値をいちいちリストにしなきゃならんし、 条件不成立の場合に空リストを返すのもちょっとうざったい。 特に条件や値が長くてif式を複数行に渡って書く場合、elseの部分が '()だけってのは どうもバランスが悪い。

リストを扱うのに定番なのはmap系なんで、無理矢理使ってみる:

(use srfi-1)
(filter-map (lambda (test value) (if test value #f))
            (list (proc1) (proc2) (proc3))
            (list x1 x2 x3))

条件のリストとか戻り値のリストとかが別に用意されている場合はこれでも 良いかもしれないが、そういうのが固定でソースに埋め込みたい時には ちょっと条件と戻り値の関係がわかりづらい。

素直なのは、条件を調べて真の時には値をcons, 偽なら現在の値をそのままにする、という操作なのだが、 それをそのまま書こうとするとconsのネストが深くなって何やら わからなくなる。

そこでマクロの登場だ。

(define-syntax cond-cons
  (syntax-rules ()
    ((_) '())
    ((_ (test . expr) . more)
     (let ((r (cond-cons . more)))
       (if test (cons (begin . expr) r) r)))
   ))

これで、当初の問題はこんなふうに書ける:

(cond-cons
  ((proc1) x1)
  ((proc2) x2)
  ((proc3) x3))

素直だが見づらいコードを、見やすいシンタックスでラップしているわけだ。


他の処理系の移植

(2004/02/06 03:54:24 PST): そうそう、こういうふうな使いかたもよくするんだけど、 あまりに普通に使ってるので気づかなかった。

「ある処理系でサポートされている構文が、自分の処理系にはない。 さてどうする?」

以下は、PLT SchemeのライブラリをGaucheで走らそうかとした時に、 ひっかかったPLT特有の構文をマクロでカバーした例。

こういうことが簡単だから、逆にScheme/Lispは方言の発生にあまり 抵抗が無いってこともあるかなあ。

More ...