1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ruby のパターンマッチ使ってなかったので練習する

Last updated at Posted at 2022-06-22

これは何?

Python に 30年の時を超えて switchcase のようなものが入ったという噂を聞いて。
そういえば ruby のパターンマッチ使ってなかったと思ったので、ちょっと触ってみる。

参考資料

公式の普通のリファレンスマニュアルには書いてないので、わかりにくい。
こんなところに書くより貢献したほうがいいのかなとも思うけど、まずはこちら。

単なる値のパターンマッチ

まずは普通の使い方。

なにが普通かあんまりわかってないんだけど

ruby3.1
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

という感じかな。

配列を受ける

配列を受けるなら

ruby3.1
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]]

こんな感じ。

配列なので順序と位置に依存する。
末尾でも余計な要素があるとマッチしない。

先頭や末尾を読み捨てたい

先頭や末尾にある余計な要素を無視したければ * が使えるが、

ruby3.1
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 警告が出ることがある。なるほど。

メソッドの引数リストに使う

こんなことができるんなら引数リストにも使えるのかなと思ったら

ruby3.1
def foo(String=>a, Numeric=>b)
  # ↑ syntax error, unexpected =>, expecting '='
  p [ a, b ]
end

そんなことはない。こういう(ちょっと型注釈付き Python3 っぽい)ことをしたいと思ったら

ruby3.1
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

型注釈だとおもうと「文字列またはシンボル」とか言いたくなるよね。

ruby3.1
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 は駄目だった。残念。
あと

ruby3.1
s = String|Symbol
#=> undefined method `|' for String:Class (NoMethodError)

となるので、これはパターンマッチ専用の文法らしい。

型以外のものを書く

今まで型かリテラルを書いていたけど、 === を使っているだけと思われるので、正規表現やラムダ書いたり

ruby3.1
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]

と、勝手なことができる。ラムダが書けるが、ラムダを変数に受けると

ruby3.1
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])

と、よくわからないエラーが出る。これを回避するためには以下のように

ruby3.1
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]

ラムダを定数で受ければよい。よく意味のわからない制限だと思う。

このよく意味のわからない制限は、以下のように

ruby3.1
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 ともパターンマッチできる。

ruby3.1
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 キーと型を両方指定したいこともあるよね。と思って適当に書いたら

ruby3.1
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 で配列にしてからパターンマッチかな。

ruby3.1
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 の場合変数名を省略できる。

ruby3.1
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:} っていうのはパターンマッチ専用の構文ではなく、以下のように

ruby3.1
{foo:}
#=> undefined local variable or method `foo' 
bar="BAR"; {bar:}
#=> {:bar=>"BAR"}

{bar:}{bar:bar} の略らしい。
こんな文法あったっけ? と思って古い ruby を触ると

ruby2.7
qux="QUX!";{qux:}
#=> SyntaxError (syntax error, unexpected '}')

やっぱり最近できた機能らしい。ほう。
この機能は キーワード引数でも有効で

ruby3.1
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 でもパターンマッチできる。

ruby3.1
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 との関係

このパターンマッチ。どうも => という右代入と関係が深いらしい。

ruby3.1
[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 は成功失敗を返す。

ruby3.1
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

ユーザー定義クラス

ユーザー定義クラスを => のパターンマッチに食べさせてみると。

ruby3.1
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)

なんか deconstructdeconstruct_keys に応答したらいいことありそうだということがわかる。

まずは deconstruct

ruby3.1
class Foo
  def inspect; "Foo"; end
  def deconstruct
    ["FooValue"]
  end
end

Foo.new => [a]
p a #=> FooValue

なるほど。

続いて deconstruct_keys

ruby3.1
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"

まとめ

上記で大体わかったような気分になった。

「まとめ」という場所を作ったけど、書くことないかな。

今後は casein を使っていこうと思うけれど、多分岐は長いメソッドやトラブルの原因になりやすい制御構造なので注意深く使っていきたいね。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?