これは何?
Python に 30年の時を超えて switch
〜 case
のようなものが入ったという噂を聞いて。
そういえば ruby のパターンマッチ使ってなかったと思ったので、ちょっと触ってみる。
参考資料
公式の普通のリファレンスマニュアルには書いてないので、わかりにくい。
こんなところに書くより貢献したほうがいいのかなとも思うけど、まずはこちら。
単なる値のパターンマッチ
まずは普通の使い方。
なにが普通かあんまりわかってないんだけど
def pat_match_sample(a)
case a
in String=>x
puts "String=>#{x}"
in Numeric=>x
puts "Numeric=>#{x}"
else
puts "else: #{a}"
end
end
pat_match_sample("hoge") #=> String=>hoge
pat_match_sample(1.2r) #=> Numeric=>6/5
pat_match_sample(:fuga) #=> else: fuga
という感じかな。
配列を受ける
配列を受けるなら
def pat_match_sample(a)
case a
in [String=>x, Numeric=>y]
puts "String=>#{x} Numeric=>#{y}"
in [String=>x, Object=>y]
puts "String=>#{x} Object=>#{y}"
else
puts "else: #{a}"
end
end
pat_match_sample(["hoge"]) #=> else: ["hoge"]
pat_match_sample(["fuga", 123]) #=> String=>fuga Numeric=>123
pat_match_sample(["piyo", :foo]) #=> String=>piyo Object=>foo
pat_match_sample(["fuga", 123, [456]]) #=> else: ["fuga", 123, [456]]
こんな感じ。
配列なので順序と位置に依存する。
末尾でも余計な要素があるとマッチしない。
先頭や末尾を読み捨てたい
先頭や末尾にある余計な要素を無視したければ *
が使えるが、
def pat_match_sample(a)
case a
in [*,1,x]
puts "*1x: x=#{x}"
in [1,x,*]
puts "1x*: x=#{x}"
in [*,1,x,*]
# ↑ warning: Find pattern is experimental, and the behavior may change in future versions of Ruby!
puts "1x*: x=#{x}"
else
puts "else: #{a}"
end
end
pat_match_sample([1,1,2,4]) #=> 1x*: x=1
pat_match_sample([1,1,1,4]) #=> *1x: x=4
pat_match_sample([2,1,2,4]) #=> 1x*: x=2
pat_match_sample([2,3,2,4]) #=> else: [2, 3, 2, 4]
上記の通り、二個以上あると、なのかな、experimental 警告が出ることがある。なるほど。
メソッドの引数リストに使う
こんなことができるんなら引数リストにも使えるのかなと思ったら
def foo(String=>a, Numeric=>b)
# ↑ syntax error, unexpected =>, expecting '='
p [ a, b ]
end
そんなことはない。こういう(ちょっと型注釈付き Python3 っぽい)ことをしたいと思ったら
def foo(*x)
x=>[String=>a, Numeric=>b]
p [ a, b ]
end
foo("hoge", 1) #=> ["hoge", 1]
foo("hoge", "fuga") #=> `foo': ["hoge", "fuga"]: Numeric === "fuga" does not return true (NoMatchingPatternError)
とするのがよい。
型の Union
型注釈だとおもうと「文字列またはシンボル」とか言いたくなるよね。
def pat_match_sample(a)
case a
in [String|Symbol=>x, Numeric=>y]
puts "String|Symbol=>#{x} Numeric=>#{y}"
else
puts "else: #{a}"
end
end
pat_match_sample(["hoge", 12])
pat_match_sample([:fuga, 34])
class Piyo;end
pat_match_sample([Piyo, 56])
おお。調べずに適当に書いたら行けた。ruby のこういうところが好き。
ちなみに or
は駄目だった。残念。
あと
s = String|Symbol
#=> undefined method `|' for String:Class (NoMethodError)
となるので、これはパターンマッチ専用の文法らしい。
型以外のものを書く
今まで型かリテラルを書いていたけど、 ===
を使っているだけと思われるので、正規表現やラムダ書いたり
def pat_match_sample(a)
case a
in [/hoge/=>x, (->(x){x.respond_to?(:odd?)})=>y]
puts "/hoge/=>#{x} y=>#{y}"
else
puts "else: #{a}"
end
end
pat_match_sample(["hogefuga", 12]) #=> /hoge/=>hogefuga y=>12
pat_match_sample(["foohoge", 3.4]) #=> else: ["foohoge", 3.4]
pat_match_sample(["fugapiyo", 56]) #=> else: ["fugapiyo", 56]
と、勝手なことができる。ラムダが書けるが、ラムダを変数に受けると
odd = (->(e){e.odd?})
even = (->(e){e.even?})
def pat_match_sample(a)
case a
in [odd=>x, odd=>y]
# ↑ duplicated variable name
puts "oo: #{x}, #{y}"
in [even=>x, even=>y]
# ↑ duplicated variable name
puts "ee: #{x}, #{y}"
else
puts "else: #{a}"
end
end
pat_match_sample([1,3])
と、よくわからないエラーが出る。これを回避するためには以下のように
Odd = (->(e){e.odd?})
Even = (->(e){e.even?})
def pat_match_sample(a)
case a
in [Odd=>x, Odd=>y]
puts "oo: #{x}, #{y}"
in [Even=>x, Even=>y]
puts "ee: #{x}, #{y}"
else
puts "else: #{a}"
end
end
pat_match_sample([1,3]) #=> oo: 1, 3
pat_match_sample([2,4]) #=> ee: 2, 4
pat_match_sample([5,6]) #=> else: [5, 6]
pat_match_sample([8,9]) #=> else: [8, 9]
ラムダを定数で受ければよい。よく意味のわからない制限だと思う。
このよく意味のわからない制限は、以下のように
def pat_match_sample(a)
case a
in [m=>x, n=>y]
puts "#{m}=>#{x}, #{n}=>#{y}"
else
puts "else: #{a}"
end
end
pat_match_sample(["foo", 123]) #=> foo=>foo, 123=>123
=>
の左辺が変数だとそこにもキャプチャするという動作を実現するためなんだと思うんだけど、なんのために =>
の左辺にもキャプチャできるようにしたかったのかはわからない。
Hash とのパターンマッチ
当然 Hash
ともパターンマッチできる。
def pat_match_sample(h)
case h
in { hoge: x, fuga: y}
puts "hoge: #{x}, fuga: #{y}"
else
puts "else: #{h}"
end
end
pat_match_sample({ hoge:1, fuga:[2,3]}) #=> hoge: 1, fuga: [2, 3]
pat_match_sample({ hoge:4}) #=> else: {:hoge=>4}
pat_match_sample({ piyo:5, fuga:"FUGA", hoge:"HOGE"}) #=> hoge: HOGE, fuga: foo
Hash
の場合、配列と違って余計な要素は無視される。もちろん順序も無視。
Hash + 型
Hash
キーと型を両方指定したいこともあるよね。と思って適当に書いたら
def pat_match_sample(h)
case h
in { hoge: String=>x}
puts "hoge(String): #{x}"
in { hoge: Numeric=>x}
puts "hoge(Numeric): #{x}"
in { hoge: x}
puts "hoge(else): #{x}"
else
puts "else: #{h}"
end
end
pat_match_sample({ hoge: 12}) #=> hoge(Numeric): 12
pat_match_sample({ hoge: "foo"}) #=> hoge(String): foo
pat_match_sample({ hoge: [4,5,6]}) #=> hoge(else): [4, 5, 6]
pat_match_sample({ fuga: 12}) #=> else: {:fuga=>12}
行けた。すばらしい。
シンボル以外のキーの Hash
Hash
のキーがシンボル以外だとパターンマッチできない気がする。
in { "hoge"=>x }
や in { 1=>x }
とは書けない。そういうものか。
どうしてもそういうことをしたい場合、to_a
で配列にしてからパターンマッチかな。
def pat_match_sample(h)
case h.to_a
in [ ["foo", x] ]
puts "'foo'=> #{x}"
in [ [1, x] ]
puts "1=> #{x}"
else
puts "else: #{h}"
end
end
pat_match_sample({ "foo"=>"foo_value"}) #=> 'foo'=> foo_value
pat_match_sample({ 1 => "1_value"}) #=> 1=> 1_value
pat_match_sample({ 2r => "2r_value"}) #=> else: {(2/1)=>"2r_value"}
Hash で変数名を省略
ところで。
Hash の場合変数名を省略できる。
def pat_match_sample(h)
case h
in {foo:}
puts "foo:#{foo}"
in {bar:}
puts "bar:#{bar}"
else
puts "else: #{h}"
end
end
pat_match_sample({foo:"FOO"}) #=> foo:FOO
pat_match_sample({bar:"BAR"}) #=> bar:BAR
pat_match_sample({baz:"BAZ"}) #=> else: {:baz=>"BAZ"}
便利なのはわかるけど、わりと気持ち悪い。
この {foo:}
っていうのはパターンマッチ専用の構文ではなく、以下のように
{foo:}
#=> undefined local variable or method `foo'
bar="BAR"; {bar:}
#=> {:bar=>"BAR"}
{bar:}
は {bar:bar}
の略らしい。
こんな文法あったっけ? と思って古い ruby を触ると
qux="QUX!";{qux:}
#=> SyntaxError (syntax error, unexpected '}')
やっぱり最近できた機能らしい。ほう。
この機能は キーワード引数でも有効で
def func(**kw)
p kw
end
foo="FOO!"
func( a:1 ) #=> {:a=>1}
func( foo: ) #=> {:foo=>"FOO!"}
func( bar: ) #=> undefined local variable or method `bar'
となる。わりとびっくりする。
Struct でもパターンマッチ
Struct でもパターンマッチできる。
S0 = Struct.new( :foo, :bar )
S1 = Struct.new( :bar, :foo, :baz )
S2 = Struct.new( :foo )
def pat_match_sample(a)
case a
in { foo:, bar: }
puts "foo: #{foo}, bar:#{bar}"
else
puts "else: #{a}"
end
end
pat_match_sample(S0.new( "FOO_0", "BAR_0")) #=> foo: FOO_0, bar:BAR_0
pat_match_sample(S1.new( "BAR_1", "FOO_1", "BAZ_1")) #=> foo: FOO_1, bar:BAR_1
pat_match_sample(S2.new( "FOO_2")) #=> else: #<struct S2 foo="FOO_2">
右代入との関係 / in
との関係
このパターンマッチ。どうも =>
という右代入と関係が深いらしい。
[1,2,3]=>[Numeric=>a,b,c]
p [ a, b, c ] #=> [1, 2, 3]
{foo:"FOO!", bar:[1,"hoge"]} => {foo:,bar:[Numeric=>x, String=>y]}
p "foo:#{foo} x:#{x} y:#{y}" #=> "foo:FOO! x:1 y:hoge"
["foo",2,3]=>[Numeric=>x,y,z]
#=> ["foo", 2, 3]: Numeric === "foo" does not return true (NoMatchingPatternError)
右代入は失敗すると例外だけど、 in
は成功失敗を返す。
p ([1,2,3] in [Numeric=>a,b,c]) #=> true
p ([1,2] in [Numeric=>a,b,c]) #=> false
p ( {foo:"FOO!", bar:[1,"hoge"]} in {foo:,bar:[Numeric=>x, String=>y]} )
#=> true
p ( ["foo",2,3] in [Numeric=>x,y,z] )
#=> false
ユーザー定義クラス
ユーザー定義クラスを =>
のパターンマッチに食べさせてみると。
class Foo
def inspect; "Foo"; end
end
Foo.new => [a]
#=> Foo: Foo does not respond to #deconstruct (NoMatchingPatternError)
Foo.new => {foo:b}
#=> Foo: Foo does not respond to #deconstruct_keys (NoMatchingPatternError)
なんか deconstruct
と deconstruct_keys
に応答したらいいことありそうだということがわかる。
まずは deconstruct
。
class Foo
def inspect; "Foo"; end
def deconstruct
["FooValue"]
end
end
Foo.new => [a]
p a #=> FooValue
なるほど。
続いて deconstruct_keys
class Foo
def inspect; "Foo"; end
def deconstruct_keys(x)
p x #=> [:hoge, :fuga]
x.each.with_object({}) do |k,o|
o[k] = [k.to_s].map(&:upcase).*(2).join("-")
end
end
end
Foo.new => { hoge:, fuga: }
p hoge #=> "HOGE-HOGE"
p fuga #=> "FUGA-FUGA"
まとめ
上記で大体わかったような気分になった。
「まとめ」という場所を作ったけど、書くことないかな。
今後は case
〜 in
を使っていこうと思うけれど、多分岐は長いメソッドやトラブルの原因になりやすい制御構造なので注意深く使っていきたいね。