パターンマッチングによる処理の分岐(1):Alexandria
OCamlやHaskellなどのコードを読むと、パターンマッチングによる処理の分岐が良く出て来る。この機能は実に便利で、リストの要素の内容に合わせて処理を分岐したり、関数の引数の数で処理を分岐したり、といった処理を、非常に簡潔に、そして読みやすく書ける。自分はこれをAndrew Wrightが書いたSchemeのマクロで知ったんだけど(Schemeの世界では有名なマクロ)、今では手放せなくなってしまった。今回は、パターンマッチングによる処理の分岐をCommon Lispで使うとき、どうすれば良いかの案内。パターンマッチングによる処理の分岐が、具体的にどのように便利かは、OCamlやHaskellなどの関数型言語の文書で多く紹介されているので、そちらを読んで欲しい。On Lispの「構造化代入」の章を読むのも良い。
パターンマッチング自体は特に新しい技術ではないので、Lispの世界でも馴染みが深い。On Lispにも登場するし、読んでないけどPAIPでも触れられているそうだ。Makoto Hiroiさんのxyzzy Lisp Programmingでも、「記号のパターンマッチング(1)」で取り上げられている。こういったコードを持ってきても良いんだけど(ライセンスに注意。幸いどれも自由に使えるけれど、使ったコードを第三者に公開するなら、出典は明記しなければいけないだろう)、使う側からすると、最初からASDFに対応していて欲しいし、Quicklispで楽に読み込めるともっと嬉しい。
そして、その嬉しいことに、パターンマッチングによる処理の分岐に使えるライブラリが、2011年6月25日の時点でもQuicklispに登録されている。自分が知っている範囲では、
- Alexandria
- Fare-matcher
- arnesi
- cl-match
- cl-pattern
がそうだ。一連の記事で順番に紹介していく。
Alexandriaは、Common Lispを今でも使っている人ならお馴染みのユーティリティライブラリだ。with-gensymsやonce-only、flattenなどの、有名なユーティリティマクロや関数が収録されている。その中のdestructuring-caseで、非常に限定された形だけれど、パターンマッチングによる処理の分岐ができる。
(alexandria:destructuring-case '(3 0 1 2)
((1 x) x)
((2 x y) (+ x y))
((3 x y z) (+ x y z)))
;=> 3
(alexandria:destructuring-case '(:a :b :c)
((x y z) :in-destructuring-case)
((:a x y) :car-cannot-take-binding))
;=> :CAR-CANNOT-TAKE-BINDING
(alexandria:destructuring-case '(1 #\x "string")
((1 #\x "string") :literal-causes-error))
;>> Error: Invalid lambda list: (#\x "string")
第一引数がパターンマッチングの対象になるリストで、以降がパターンと対応する処理の組み合わせだ。このマクロは名前の通り、destructuring-bindとcaseを組み合わせたもので、それぞれの性質を引き継ぐ。展開したあとの式も見てみよう。
(LET ((#:KEY0 '(3 0 1 2)))
(IF (TYPEP #:KEY0 'CONS)
(CASE (CAR #:KEY0)
(1 (DESTRUCTURING-BIND (X) (CDR #:KEY0) X))
(2 (DESTRUCTURING-BIND (X Y) (CDR #:KEY0) (+ X Y)))
(3 (DESTRUCTURING-BIND (X Y Z) (CDR #:KEY0) (+ X Y Z))))
(ERROR "Invalid key to DESTRUCTURING-~S: ~S" 'CASE #:KEY0)))
とても単純な仕組みになっている。パターンのcar部はcaseで処理できなければならない。パターンのcdr部はdestructuring-bindに渡されるため、リテラルを含んではいけない。
一見、かなり厳しい制限に思えるけど、リストの最初の要素が処理の成否を表し、それ以降の要素が処理のデータを表す、といった、ありがちなパターンには十分対応できる。あるいは、最初の要素はアクションを表し、以降の要素はアクションのパラメータを表す、なんていう場合でも問題ない。リストの最初の要素にはアクセスしやすいので(carするだけだ)、リストの最初に条件分岐のためのキーを置くことは多い。多値にもmultiple-value-listで対応でき、意外に適用範囲は広い。
(alexandria:destructuring-case '(nil (10 . 5) "syntax error")
(((t) syntactic-tree) (process syntactic-tree))
(((nil) (line . char) msg) (error "line ~a: char ~a: ~a" line char msg)))
(alexandria:destructuring-case `(:write ,stream #\a)
((:write s x) (princ x s))
((:read s) (aif (read-char s nil) (push it buf))))
特徴をまとめると、alexandria:destructuring-caseは、対応するデータ型はリストのみで、表現力も乏しいけれど、限られた用途には十分で、仕組みが単純なため、destructuring-bindが最適化されている処理系では速い。
0 件のコメント:
コメントを投稿