2010-05-22
■[Ruby] Ruby でパターンマッチ
ref: 未来の国のアリス - d.y.d.
で紹介されている implicit future が Ruby に欲しい!
# promise を作る x = Promise.new a = [1, x, 2, x, 3, x] # 今はまだ値になっていない p a #=> [1, _promise_, 2, _promise_, 3, _promise_] # この promise は 42 に決めた! (代入ではないよ) x === 42 # x の箇所は勝手に 42 になっている p a #=> [1, 42, 2, 42, 3, 42]
というのも、これがあれば Ruby でパターンマッチができる気がするんですよね。こんな感じに。
# 何にでもマッチする箇所には _ と書く (実体は Promise.new) def _ Promise.new end # + と定数だけからなる抽象構文木 (二分木) Sum = Struct[:left, :right] Const = Struct[:value] # ... を評価する関数 (ここが重要!) def eval(n) case n when Sum[l = _, r = _] then eval(l) + eval(r) when Const[v = _] then v end end # ... で (1+2)+(3+4) を評価する p eval(Sum[Sum[Const[1], Const[2]], Sum[Const[3], Const[4]]]) #=> 10
case 文が内部的に Sum[l = _, r = _] === n を実行してくれるのがミソ。例えば n が Sum[1, 2] とかだったら、l === 1 と r === 2 が実行されるので *1 、l と r がそれぞれ 1 と 2 になる。
素敵ですよねー。
def eval(n) case n when Sum then eval(n.left) + eval(n.right) when Const then n.value end end
の方が分かりやすいって? この程度の例ではそうかもしれないんだけれど、よくパターンマッチの強力さを示す例としてあげられる赤黒木の balance を見ると、
balance :: RB a -> a -> RB a -> RB a balance (T R a x b) y (T R c z d) = T R (T B a x b) y (T B c z d) balance (T R (T R a x b) y c) z d = T R (T B a x b) y (T B c z d) balance (T R a x (T R b y c)) z d = T R (T B a x b) y (T B c z d) balance a x (T R b y (T R c z d)) = T R (T B a x b) y (T B c z d) balance a x (T R (T R b y c) z d) = T R (T B a x b) y (T B c z d) balance a x b = T B a x bRed Black Trees
def balance(l, v, r) case [l, v, r] when [T[:R,T[:R,a=_,x=_,b=_],y=_,c=_],z=_,d=_]; T[:R,T[:B,a,x,b],y,T[:B,c,z,d]] when [T[:R,a=_,x=_,T[:R,b=_,y=_,c=_]],z=_,d=_]; T[:R,T[:B,a,x,b],y,T[:B,c,z,d]] when [T[:R,a=_,x=_,b=_],y=_,T[:R,c=_,z=_,d=_]]; T[:R,T[:B,a,x,b],y,T[:B,c,z,d]] when [a=_,x=_,T[:R,T[:R,b=_,y=_,c=_],z=_,d=_]]; T[:R,T[:B,a,x,b],y,T[:B,c,z,d]] when [a=_,x=_,T[:R,b=_,y=_,T[:R,c=_,z=_,d=_]]]; T[:R,T[:B,a,x,b],y,T[:B,c,z,d]] when [a=_,x=_,b=_]; T[:B,a,x,b] end end
Haskell のシンプルさには敵わないけれど、だいぶ健闘していますよね。
x=_ が見にくいので、x=_ を <x> などと書けるような syntactic sugar を導入すればだいぶ見違えるんじゃないかと思う。
今の Ruby だけで書くと、
def balance(l, v, r) case when T===l && l.c===:R && T===l.l && l.l.c===:R; T[:R,T[:B,l.l.l,l.l.v,l.l.r],l.v,T[:B,l.r,v,r]] when T===l && l.c===:R && T===l.r && l.r.c===:R; T[:R,T[:B,l.l,l.v,l.r.l],l.r.v,T[:B,l.r.r,v,r]] when T===l && l.c===:R && T===r && r .c===:R; T[:R,T[:B,l.l,l.v,l.r],v,T[:B,r.l,r.v,r.r]] when T===r && r.c===:R && T===r.l && r.l.c===:R; T[:R,T[:B,l,v,r.l.l],r.l.v,T[:B,r.l.r,r.v,r.r]] when T===r && r.c===:R && T===r.r && r.r.c===:R; T[:R,T[:B,l,v,r.l],r.v,T[:B,r.r.l,r.r.v,r.r.r]] else T[:B,l,v,r] end end
という感じ。長さはともかく、自分で書いてみると分かりますが、書くのがつらいです。パターンマッチなら一気に部分木を変数代入できるのに比べて、いちいちルートから l.r.l.r... と辿っていかないといけないのが。
implicit future はできないものの、Delegator を使えば explicit な future はできるので、gem を作ってみました。
$ gem install case_class
ref: http://github.com/mame/case_class
実用する人はいないと思いますが、使い方は examples の計算器や赤黒木を見て感じてください。
implicit future が Ruby 2.0 に組み込みで入るといいなあ。難しそうだけど。
known bugs というか、はまりポイントは以下の 2 点。
*1:正確には、Struct#=== は内部的に == を使ってしまうので、l == 1 と r == 2 が実行されるような気もする。
- 200 http://b.hatena.ne.jp/hotentry/it
- 171 http://twitter.com/
- 139 http://reader.livedoor.com/reader/
- 122 http://b.hatena.ne.jp/
- 101 http://www.google.co.jp/url?sa=t&rct=j&q=ruby パターンマッチ&source=web&cd=4&ved=0CEMQFjAD&url=http://d.hatena.ne.jp/ku-ma-me/20100522/p1&ei=GCCFToF55JOZBYywue8P&usg=AFQjCNGthGpjrweXm0kEgkx4M1
- 80 http://pipes.yahoo.com/pipes/pipe.info?_id=faa858a20082ef6d25ad27557e37e011
- 68 https://www.google.co.jp/
- 64 http://b.hatena.ne.jp/hotentry
- 41 http://www.google.com/reader/view/
- 40 http://www.google.co.jp/url?sa=t&rct=j&q=ruby+パターンマッチ&source=web&cd=5&ved=0CEgQFjAE&url=http://d.hatena.ne.jp/ku-ma-me/20100522/p1&ei=ofLJTojmB4-imQWv08Ql&usg=AFQjCNGthGpjrweXm0kEgkx4M1FN