2011-06-25

パターンマッチングによる処理の分岐(1):Alexandria

OCamlHaskellなどのコードを読むと、パターンマッチングによる処理の分岐が良く出て来る。この機能は実に便利で、リストの要素の内容に合わせて処理を分岐したり、関数の引数の数で処理を分岐したり、といった処理を、非常に簡潔に、そして読みやすく書ける。自分はこれを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 件のコメント: