Andrew K. Wrightのmatchを読む
いくつかのScheme処理系のパターンマッチライブラリとして採用されているAndrew K. Wrightのmatchライブラリがある。
このmatchライブラリがどのようなものかは、日本語だとGaucheのutil.matchモジュールのドキュメントがわかりやすい。
このmatchライブラリのソースを手に入れて(@SaitoAtsushiさんにいただきました。感謝!)、読んでみようとしているのだけどなかなかに手ごわそうで、途中で心が折れてしまわないように、途中経過の自分用メモ内容をネットにあげながら自分を追い込もうと思う。
基本的には関数単位で、役割や内容を呼んで行く。
手元のソースコード上にもコメントとしてメモを書いているため、全ての説明メモをここに記述することは出来ないけどある程度わかるようには書くように心がける。
先頭に記述されている、matchの動作を制御するための変数と関数群は後回し。
- genmatch 後回し。
- genletrec 後回し。
- gendefine 後回し。
- pattern-var? (λ (x) ...) xがシンボル以外なら#f。
- dot-dot-k? (λ (s) ...) sが...のシンボルなら0。もしくは..数字なら数字の部分を返す。
- error-marker (λ (match-expr) ...) gen...の三つの関数から呼ばれている。
- 'unspecifiedの時は、単にundefinedな値を返す。undefinedな値は(cond (#f #f))で生成している。
- 'errorまたは'failは実行時エラーを発生させる。
- 'matchはマッチ式の追加情報とともに実行時エラーを発生させる。
- unreachable (λ (plist match-expr) ...) gen...の三つの関数から呼ばれている。
- validate-pattern (λ (pattern) ...) gen...の三つの関数から呼ばれている。
(dot-dot-k? x)が#t なら #f。
シンタックスシンボル(quasiquoteやquote、and, orなど他いろいろ)なら#f。
それ以外は#t。
つまり、パターン中にある任意の式にマッチするシンボルなら#tを返す。
ちなみに...の部分は___でも代用可能。
それ以外は#f。
match:error-control変数の値によってエラー時の操作を制御する。
match:error-controlが
plistのそれぞれの要素Xについて、(car (cdddr X))が#fなら警告文字列を出力する。
plistの構造はまだわからないので、詳細は後回し。
引数のpatternにはたとえば以下の様なmatch式の場合、
(match 123 [(? string? x) (list 'string x)] [(? number? x) (list 'number x)])
各節のパターン部である、(? string? x)と(? number? x)がそれぞれ渡されて処理される。
validate-pattern自体は内部で定義されているordinary関数に引数のpatternを渡すことしかしていない。
内部にヘルパー関数が定義されているため、別々に説明する。
- simple? (λ (x) ...) xがstring?、boolean?、char?、number?、null?のいずれかなら#t。それ以外は#f。
- ordinary (λ (p) ...) pがどのような値かによって処理を分岐する巨大な条件分岐を持つ関数。
- quasi (λ (p) ...) ordinary関数の中で(quasiquote X)の形式を見つけた時にXの部分を処理するためのヘルパー関数。
- pが(unquote X)なら(ordinary X)になる。
- pが((unquote-splicing X))なら(ordinary X)になる。
- pが((unquote-splicing X) *)なら(append (ordlist X) (quasi *))になる。(*は(cdr p)を意味する。)
- pが(X ...)なら*1( (quasi (cdr p)))
- ordlist (λ (p) ...) pが'()かpair?でなければシンタックスエラー。
つまり、xがリテラルなら#tを返す。
処理の内容はpの値を正規化しながら、無効な構造ならエラーを発生させる(たぶん)。
pがリストで(car p)の値が定められたシンボル(quasiquoteや?、=、andなど)かどうかで処理を分けている。
【余談だけども】ここの巨大なif式はcondを展開したような形になっている。
はじめてみた時は若干引いてしまったが慣れてしまえば難しいことをしている式ではなかった。
だけどこの巨大なif式はcondの偉大さを教えてくれるいい例な気がする。
どの構造にも当てはまらなければシンタックスエラーを発生させる。
処理の内容はordinary関数と同じようにpを正規化しながら、無効な構造ならエラーを発生させる。
pがリストなら特定の構造を持っているかテストされ構造ごとの処理に分岐する。
【また余談です】ここのunquote-splicingのチェックでは、unquote-splicingが複数引数を持つようになっているとunquote-splicingの処理と認識しないようになっている。
これはr5rs(と、たぶんr7rs)では複数引数を取るunquote-splicingの動作が未定義になっているためだと思う。
処理系によっては独自の解釈で複数引数を受け付けるようになっていた気がするが、その処理系ではここのチェックはどうなってるんだろう?
どの構造にも当てはまらなければシンタックスエラーを発生させる。
それ以外の動作は、(map ordinary p)と同等。
この関数も内部に複数のヘルパー関数を持っているため別々に説明する。
内部関数である、bound、boundv、bound*が継続渡しスタイルで処理されるため難しそうに見えるが、処理のパターンが理解できればそんなに複雑なことはしていない。
- bound (λ (p a k) ...) 外側の関数と同名の関数。これがメインの処理を行っている。
引数pは処理するパターン式。この式を分解しながら再帰的に降下することで解析している。
引数aはパターン式内に出現する束縛シンボルのリスト。aはaccumulatorの頭文字か?
ちなみに束縛シンボルとは、
(match '(foo bar) [('baz x) x] ... パターン1 [(x y) x]) ... パターン2
パターン1ではx、パターン2ではxとyの様なシンボルのこと。
引数kはbound関数終了時に実行する関数。おそらくkは継続を表す。
bound処理本体ではpがどのような値かを判別しながら処理している。
たとえばpが、
- '_だった場合 通常シンボル同様全てのパターンを受け付けるが、構造の束縛は行わないため引数aに束縛リストに追加はしない。
- (or p1 p2 ..)だった場合 各パターン部に出現する束縛シンボルは同じ内容でなければならないためチェックを行っている。(ここで後述するpermutation関数が使われている)
- (not p1 p2 ..)だった場合 各パターンには束縛シンボルが現れてはいけないためチェックを行っている。(ここでもpermutation関数が使われている)
- (x ...)だった場合(ちなみにここの...はdot-dot-k?的なシンボルのこと) なにやら複雑な値を生成しているが先を読むまでわからないため後回し。
- Vectorだった場合 リストに変換し処理をboundv関数に委譲する。
bound関数の引数pがVectorだった時のヘルパー関数。
単純にplistの各要素をboundに渡して結果をconsする。
plistの末尾が(x ...)の様な構造だった時は要素に分解せずplistをそのままboundに渡している。
plistの各要素について内部関数であるboundを適用させる。
boundのk引数には(cdr plist)を自己再帰的に適用させるクロージャを生成して渡している。
つまり、bound関数の処理終了後に実行すべき継続を渡している模様。
リストbの先頭からリストaに含まれない要素だけを取り出す。
例:
(find-prefix '(1 2 3) '(2 3)) => '(1)
リストp1、p2について、順不同で内容が同じかどうかをチェックする。
例:
(permutation '(1 2) '(2 3)) => #f (permutation '(3 2 1) '(2 1 3)) => '(1 3)
異なる要素を持っていれば#f。
同じであればリストが返るのだが、呼出元では全てnot関数を通っているので値は使用されていない。
gendefineとgenmatchから呼ばれている。
引数のlet-expはlet式の構造を持つリスト。letのbodyの部分は単一の式という前提で処理されている。
この関数は引数として渡されたlet式から無駄な式を削除、置き換えをして最適化を図っている
たとえば
(let ([x 1] [y (lambda () 2)] [z 3]) (+ x y)
上記のlet式は次のように変換される。
(+ 1 (lambda () 2))
定数の置き換えと参照されていない変数の削除が行われ、最後にletの束縛部が一つもなければ実行部分だけに変換される。
この関数も内部にヘルパー関数を持っているため、別々に説明する。
- occ (λ (x e) ...) 引数xはlet束縛部の変数名シンボル。
- const? (λ (sexp) ...) 名前のとおり、引数sexpが定数かどうかを判断する。
- isval? (λ (sexp) ...) 引数sexpが即値であれば#tを返す。
- small? (λ (sexp) ...) 引数sexpが単純な式なら#tを返す。
引数eはletの実行部分の式。
occはletの実行部分の式eの中で変数xが何回参照されているかをカウントする。
sexpがペアだった場合、(quote x)でxもシンボルだった時だけconst?は#tを返す。
(const? sexp)が#tであればisval?も#tを返す。
または、sexpがペアで、(car sexp)がlambda、quote、match-lambda、match-lambda*のいずれかであれば#tを返す。
(const? sexp)が#tであればsmall?も#tを返す。
または、sexpの構造が(lambda _ x)で、(const? x)が#tならsmall?は#tを返す。
bound関数が返した結果などを利用して実際にパターンマッチを行うコードを生成する関数。
コード生成には多くの関数がかかわっていて、しかもパターンマッチに成功した時の継続と失敗した時の継続を複数の関数間で引き渡しながら処理を進めている。
読む人にとっては地獄以外の何者でもない。ただ、手法としては参考になった。
引数xには、パターンマッチさせる値そのものが入る。たとえば、
(match '(1 2) [...] [...])
の様なマッチ式の場合、xには'(1 2)が渡される。
引数sfは、現在処理中のパターン以前ですでに実行されている値へのパターンマッチテストのリストが渡される。
実際には、(string? x)や(equal? x pattern)、(not (null? x))などテスト関数のリストになっている。
この引数は、すでにパターンマッチが確認されている値に関しては重複してテストを行わないように最適化するために利用されている。
たとえば以下のマッチ式の場合、
(match '((1 . 2) 3) [((_ . 2) (? string?)) 'string] ;;パターン1 [((_ . 2) (? number?)) 'number]);;パターン2
パターン1とパターン2では、
- 最素に(_ . 2)というパターン
- マッチして欲しい構造は長さ2のリスト
(式の展開にはGaucheを使用して、gensymの部分は適当な名称に変更しました。)
(let ((x '((1 . 2) 3))) (let ((pat2-body (lambda () 'number)) (pat1-body (lambda () 'string))) ;;パターン1とパターン2の共通の構造テストで、((_ . 2) _ ...)という構造になっているとこまでチェックしている。 (if (and (pair? x) (pair? (car x)) (equal? (cdar x) 2) (pair? (cdr x))) (if (string? (cadr x)) (if (null? (cddr x)) (pat1-body) (match:error x)) (if (and (number? (cadr x)) (null? (cddr x))) (pat2-body) (match:error x))) (match:error x))))
コメントに書いているように、最初の長いifで共通部分を一気にテストしている。
しかし共通部分のテストでは長さが2以上のリストであるというところまでしかテストしていないため、パターンのそれぞれで長さが2かどうかのテストを行っている。
引数plistには、bound関数がパターン部分を解析した結果を含んだデータのリストが渡される。
リストの一要素は一つのパターンに対応している。
引数erractは、パターンマッチに失敗した際に実行する式を生成する関数が渡される。
この関数を実行すると、(match:error x)の様な式が生成される。
引数length>=とetaには、(gensym)で生成されたユニークなシンボルが渡される。
以下、まだ未調査。出来次第追記します。
*1:quasi X) ...)になる。