Syntax highlighter

2016-01-26

syntax-case ポコ・ア・ポコ

syntax-caseを解説した日本語の文章というのは極端に少ないらしい。実際Googleで「syntax-case は」(日本語のみを検索する方法を知らないw)と検索しても、(オランダからだからかもしれないが)日本語の解説ページは自分が書いたものが一番上にヒットする。ここは一つ知ったかぶりをしてもう一つ検索結果を汚してもいいだろうと思ったので、syntax-caseの使い方を解説することにした(ここまで前置き)。想定読者はマクロはしってるけどSchemeのマクロはよく分からないという人としている。つまりsyntax-rulesを知らなくてもよい。ただ、マクロ自体は解説しないので、マクロとはなんぞやという疑問はこの記事を読んでも解決しないのであしからず。

初めの一歩

Schemeの最新規格はR7RSだが、一つ前の規格R6RSで標準化されたsyntax-caseの使い方を解説する。まずは簡単な例を見てみよう。ここではwhenを定義することにする。
#!r6rs
(import (except (rnrs) when))

(define-syntax when
  (lambda (x)
    (syntax-case x ()
      ((_ test body1 body* ...)
       #'(if test
             (begin body1 body* ...))))))

(when 'a 'b) ;; -> b
(when #f #f) ;; -> unspecified
(when #f)    ;; -> &syntax
最初のimportは知らなければおまじないと思ってくれればいい。次のwhenでマクロを定義している。

syntax-caseは第一引数に構文オブジェクト、第二引数にリテラルリスト、それ以降にパターンと出力式のリストもしくは、パターン、フェンダー及び出力式のリストを受け取る。ここでは言葉を覚える必要はなく、そういうものだと思ってもらえればいい。
(define-syntax when
  (lambda (x) ;; <- define-syntaxが受け取る手続きが受け取る引数が構文オブジェクト
    (syntax-case x #| <- 第一引数:構文オブジェクト |# () #| >- 第二引数:リテラルリスト |#
      ;; パターンと出力式のリスト
      ;; (パターン 出力式)
      ;; もしくは
      ;; (パターン フェンダー 出力式) [フェンダーについては後述参照]
      ((_ test body1 body* ...)
       #'(if test
             (begin body1 body* ...))))))
出力式は基本構文オブジェクトを返す必要がある。#'を式につけると構文オブジェクトを返すようになる。#'syntaxの省略なので、上記のテンプレート部分は以下のようにも書ける:
(syntax (if test (begin body1 body* ...)))
どちらを使うかは好みだが、筆者は#'を使う方が見た目にも構文オブジェクトを返すことが分かりやすいのでこちらを使う。

設問:
上記のwhenを参考にしてunlessを書いてみよ。
ヒント: unlessの展開形は(if (not test) (begin body1 body* ...)) のようになるはずである。

パターンマッチ

パターンマッチとはなんぞやという人はあまりいないだろう。パターンを書くということは、入力式がそのパターンにマッチする必要がある。syntax-caseではリストもしくはベクタを入力式として分解することができる。基本的には識別子(*1)一つが要素一つに、可変長の入力を扱いたい場合は...(ellipsisと呼ばれる)を使う。例えば一つ以上の要素を持つリストにマッチさせるには以下のように書く:
*1: コード上に現れるシンボルのこと。ここではクオートされたシンボルと区別するためにこう呼ぶ
(e e* ...)
#|
(1 2 3) ;; OK (これ以上でももちろんよい)
(1)     ;; OK
()      ;; NG
|#
一つのパターンに出てくる識別子は重複してはならないので、それぞれに別名をつける必要がある。筆者はよく可変長の入力にマッチするパターンの末尾に*をつける。また、作法としてパターン識別子の先頭に?をつけてパターン識別子であることを分かりやすくするものもある。気をつけたいのは、...(ellipsis)は0個以上の入力にマッチするという点である。なので、以下のように書くと空リストにもマッチする:
(e* ...)
ネストしたパターンを書くこともできる。例えば、要素の先頭がリスト(空リスト含む)であるリストにマッチするパターンは以下のように書ける:
((e1* ...) e2* ...)
#|
((1 2 3) 4 5 6) ;; OK
(() 4 5 6)      ;; OK
(())            ;; OK
()              ;; NG
|#
上記のパターンはリスト、ベクタ両方(ベクタの場合は#をつけてベクタにする必要がある)に使える。

ドット対のcdr部分にマッチさせることもできる。そのためには以下のように書く:
(a . d)
#|
(1 . 2) ;; OK
(1 2 3) ;; OK (1 2 3) = (1 . (2 . (3 . ())))
(1)     ;; OK
()      ;; NG
|#
ドット対のマッチと...(ellipsis)を使うと、連想リストのキーと値にマッチすることも可能である。以下のように書く:
((a . d) ...)
#|
((1 . 2))         ;; OK
(1 2 3)           ;; NG
((1 . 2) (3 . 4)) ;; OK
()                ;; OK
|#
組み合わせ次第で複雑な入力式にマッチさせることができる。

厳密な定義としてのパターンは以下のようになる:
  • 識別子
  • 定数 (文字、文字列、数値及びバイトベクタ)
  • (<パターン> ...)
  • (<パターン> <パターン> ... . <パターン>)
  • (<パターン> ... <パターン> <ellipsis> <パターン> ...)
  • (<パターン> ... <パターン> <ellipsis> <パターン> ... . <パターン>)
  • #(<パターン> ...)
  • #(<パターン> ... <パターン> <ellipsis> <パターン> ...)
<パターン>は再帰的に定義されるので、リストパターンの中にベクタがあっても問題ない。また、定義で使われている...はパターンではなくパターンが複数あるという意味である。パターンの...<ellipsis>となっているので注意されたい。

ちなみに、パターンの定義はsyntax-rulesとほぼ同じなので、パターンマッチに関してはsyntax-caseを覚えればsyntax-rulesのものも使えるようになる(はずである)。

出力式

出力式は基本的に構文オブジェクトを返す必要がある。大事なことなので二度目である。構文オブジェクトの生成にはsyntax (#')構文とquasisyntax (#`)構文の2種類ある。quasisyntaxquasiquotesyntax版だと思えばよい。使い方は(あれば)次回やることにする(もしくはこちらを参照:Yet Another Syntax-case Explanation )。

syntax構文が受け取る引数はテンプレートと呼ばる。テンプレートに指定できるのは、quoteと同じものが指定できる。quoteと違う点としてテンプレート内に現れてパターン変数(パターン内に現れた識別子のこと)がマッチした式に展開されるという点である。最初のwhenの例を見てみよう。whenのパターンは以下:
(_ test body1 body* ...)
そして出力式は以下:
#'(if test (begin body1 body* ...)
ここで、入力式として以下を受け取ったとしよう:
(when (zero? a) (display a) (newline) (do-with-a a))
この入力式とパターンを対応づけると以下のようになる:
_     = when
test  = (zero? a)
body1 = (display a)
body* = ((newline) (do-with-a a))
body*...(ellipsis)が付いているので可変長の入力を受け付けるが、ここでは便宜上複数要素を持つ一つのリストとしておく。パターンマッチでは言及していないが、_はプレースホルダーになるので、なんにでもマッチしかつ出力式では使用できないことに留意したい(*2)
*2: 要らない入力に名前を付けたくない場合に重宝する

ここまでくれば後は出力式に当てはめていくだけである。...(ellipsis)を持つパターン変数はマッチした要素が一つずつ置換される、一つのリストではくなる、ので展開結果は以下のようになる。
(if (zero? a) (begin (zero? a) (display a) (newline) (do-with-a a)))
とても簡単である。気をつけたい点としては...(ellipsis)が付いているパターン変数はこれをつけないとマクロ展開器がエラーを投げることだろうか。マッチした入力式を展開する際は全ての入力式が消費される必要がある。もちろん、出力式に現れなかったパターン変数についてはその限りではない。

フェンダー

用語として出してしまったので解説をしておく。フェンダーはパターンと出力式の間に入れることができるチェック用の式である。これが入っている場合はこの式が真の値を返した場合のみパターンにマッチしたと判定される。例えば以下のように使う:
(define-syntax when
  (lambda (x)
    (syntax-case x ()
      ((_ test body1 body* ...)
       (and (boolean? #'test) #'test) ;; testが#tであれば、if式は要らない
       #'(begin body1 body* ...))
      ((_ test body1 body* ...)
       #'(if test
             (begin body1 body* ...))))))
フェンダー内で入力式を参照するにはsyntax構文を使ってパターン変数を展開してやる必要があることに注意したい。これ以外にも入力が識別子かどうか等のチェックなど用途はさまざまであるが、基本的にはパターンマッチ以外にチェックが必要な際に使う。ちなみに、パターンマッチは上から順に行われるため、上記のwhenの定義を逆にすると、フェンダーは評価されない。

設問:
フェンダーを用いてunlessを書いてみよ。

長くなったのと「ポコ・ア・ポコ」なので今回はこれくらいにしておく。(要望があれば)次回はsyntax-caseが低レベル健全マクロと呼ばれる理由について書くことにする。

No comments:

Post a Comment