Feedforce Developer Blog

フィードフォース開発者ブログ

Ruby 2.7 で導入予定のパターンマッチングを試したら無限大の可能性を感じた話

こんにちは! id:chionyan です。
4/18〜4/20に福岡で開催された RubyKaigi 2019 に参加してきました!
ちょうど1年くらい前に福岡からフィードフォースに来たので、とても感慨深い気持ちでした😋

chionyan.hatenablog.com

社内で感想を話したところ、Ruby 2.7.0 で導入予定の Pattern matching について興味津々な方が多かったので、試してみてわかったことをまとめてみます!

Ruby 2.7.0-dev のインストール

rbenv でインストールできる 2.7.0-dev では既にパターンマッチングが試せるようになっています。

$ rbenv install 2.7.0-dev

ちなみにパターンマッチングを実行すると、

warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!

パターンマッチングは実験的なもので、将来のバージョンのRubyでは動作が変わる可能性があります。」というメッセージが表示されるので、12月リリース予定の 2.7.0 ではここで書いた内容から変更されているかもしれません。

パターンマッチング簡単ご紹介

パターンマッチングは case/in で表現されます。
従来の条件分岐として使われている case/when に加えて、多重代入できるようになっています。

case [0, [1, 2, 3]]
in [a, [b, *c]]
  p a #=> 0
  p b #=> 1
  p c #=> [2, 3]
end

また、in の後にマッチさせたいパターンを書くことで、配列やハッシュのオブジェクト構造のチェックも可能です!

case [0, [1, 2, 3]]
in [a]
  :unreachable
in [0, [a, 2, b]]
  p a #=> 1
  p b #=> 3
end
case  {a: 0, b: 1}
in {a: 0, b: var}
  p var #=> 1
end

JSON データを扱う時にパターンマッチングの良さがとても見えました!
例えば name: Alicechildren (一人息子) である name: Bobage を取得する場合、

{
  "name": "Alice",
  "age": 30,
  "children": [
    {
      "name": "Bob",
      "age": 2
    }
  ]
}

パターンマッチングを使わない条件分岐だと、

person = JSON.parse(json, symbolize_names: true)
if person[:name] == "Alice"
  children = person[:children]
  if children.length == 1 && children[0][:name] == "Bob"
    p children[0][:age] #=> 2
  end
end

対して、パターンマッチングを使った条件分岐だと、

case JSON.parse(json, symbolize_names: true)
in {name: "Alice", children:[{name: "Bob", age: age}]}
  p age #=> 2
end

と書けるようになります!とても読みやすいですね✨
条件分岐って複雑になってくるとネストが深くなってきますが、パターンマッチングを使うととてもシンプルに書けるようになります👏

ちなみに in を採用した理由は、「ご存知ない方がおおいかもしれないですが、Rubyにはforという文法があります…」という一文に隠されている、という噂ですね…👀

パターンマッチングの処理の流れ

ここからはパターンマッチングの具体的な内容に入っていきます!
基本的な文法は以下の通りです。

case expr
in pattern1 [if/unless condition]
  # 処理1
in pattern2 [if/unless condition]
  # 処理2
else
  # 処理3
end

上から順にパターンがマッチするまでパターンマッチングが行われます。
全てのパターンにマッチしない場合は、 else 節の処理が実行されます。
else 節もない場合は、NoMatchingPatternError 例外が発生します。

  • 例: in [a] にはマッチしないのでスキップ、 in [0, [a, 2, b]] の処理が実行される
case [0, [1, 2, 3]]
in [a]
  :unreachable
in [0, [a, 2, b]]
  p a
end
#=> 1
  • 例: in [a] にはマッチしないので、 else 節の処理が実行される
case [0, [1, 2, 3]]
in [a]
  :unreachable
else
  p 'No Matching'
end
#=> "No Matching"
  • 例: in [a] にはマッチせず、 else 節もないので、 NoMatchingPatternError 例外が発生する
case [0, [1, 2, 3]]
in [a]
  :unreachable
end
#=> NoMatchingPatternError

だから条件分岐は網羅的に書こうねってお話しもありました!

また、ガード式に対してガード式を使うことができます!
パターンがマッチした場合にのみ、ガード式が評価されて、その結果によって処理が実行されるかが決まります。 else 節もない場合は、NoMatchingPatternError 例外が発生します。

  • 例: パターンがマッチして、ガード式が真の場合、処理が実行される
case [0, 1]
in [a, b] if a != b
  p a
end
#=> 0
  • 例: パターンがマッチして、ガード式が偽の場合、 NoMatchingPatternError 例外が発生する
case [0, 1]
in [a, b] if a == b
  p a
end
#=> NoMatchingPatternError
  • 例:パターンがマッチしない場合は、ガード式も評価されず、次のパターンに進む
case [0, 1]
in [a, b, c] if a != b
  p a
in [a, b]
  p b
end
#=> 1

基本的な構文は case/when と似ていますが、ガード式などはパターンマッチングで初めて加えられたため、使い方の幅が広がりそうですね😊

パターンのバリエーション

in の後に書けるパターンは6種類の紹介がありました!
また、パターン記法における登場人物は、

  • literal: リテラル
  • Constant: 定数
  • var: 変数
  • pat: パターン

が登場します!
パターンの中にパターンが登場することもあるので、すごい使い方もできそう…!?

Value パターン

パターン記法: literal or Constant

pattern === object のオブジェクトとマッチします。

[0, 1, 2].each do |var|
  case var
  in 0 # 値はもちろん
    p '0だよ'
  in -1..1 # Range もオッケー
    p '-1から1の間だよ'
  in Integer # 定数もオッケー
    p '数値だよ'
  end
end
#=> "0だよ"
#=> "-1から1の間だよ"
#=> "数値だよ"

Variable パターン

パターン記法: var

任意の値とマッチして、変数にその値を代入します。

case 0
in a
  p a
end
#=> 0

変数に代入する必要がない場合、アンダースコアで省略可能。

case [0, 1]
in [_, _]
  :reachable
end

同じ名前の外部変数がある場合、変数の値は上書きされます。

a = 0
case 1
in a
  p a #=> 1
end

p a #=> 1

既存の変数の値に対してパターンマッチングを行う場合は、^ を使用することができます。

a = 0
[0, 1].each do |num|
  case num
  in ^a # `case の外側での a の値は 0 なので、`in 0` と同様
    p "#{a}だよ"
  else
    p "#{a}じゃないよ"
  end
end
#=> "0だよ"
#=> "0じゃないよ"

Alternative パターン

パターン記法: pat | pat ...

複数のパターンを OR 条件でつなぐことができます。

['abc', nil, 123].each do |item|
  case item
  in nil | String
    p 'nil か String だよ'
  else
    p 'nil でも String でもないよ'
  end
end
#=> "nil か String だよ"
#=> "nil か String だよ"
#=> "nil でも String でもないよ"

As パターン

パターン記法: pat => var

値がパターンにマッチした時、値を変数に代入します。

case 0
in Integer => a
  p a #=> 0
end

複雑な構造のオブジェクトをパターンマッチして、値を取り出す時に便利!

case [0, [1, 2]]
in [0, [1, _] => a]
  p a #=> [1,2]
end
case [0, [1, 2]]
in [0, [1, Integer => a]]
  p a #=> 2
end
case [0, [1, 2]]
in [0, a] # a はオブジェクトならマッチングする
  p a #=> [1,2]
end

Value パターンと似てるけど、オブジェクトの構造が複雑であるほど真価を発揮しそうですね👀

Array パターン

名前に反して、 Array パターンは配列オブジェクト専用ではありません笑

パターン記法
Constant(pat, ..., *var, pat, ...)
Constant[pat, ..., *var, pat, ...]
[pat, ..., *var, pat, ...] (BasicObject[ ... ] と同義)

Array パターンは以下の場合にパターンマッチします。

  1. Constant === objecttrue
  2. オブジェクトに Array を返す #deconstruct が定義されている
  3. 入れ子になったパターンに対して object.deconstructtrue
  • Array クラスを使った Array パターン
class Array
  def deconstruct
    self
  end
end

case [0, 1, 2]
in Array(0, *a, 2)
in Object[0, *a, 2]
in [0, *a, 2]
in 0, *a, 2 # `[]` の省略可能
end

p a #=> [1]
  • Struct クラスを使った Array パターン
class Struct
  alias deconstruct to_a
end

Color = Struct.new(:r, :g, :b)
color = Color[0, 10, 20]
p color.deconstruct #=> [0, 10, 20]

case color
in Color[0, 0, 0]
  puts "Black"
in Color(255, 0, 0)
  puts "Red"
in [0, 255, 0]
  puts "Green"
in r, g, b
  puts "#{r}, #{g}, #{b}"
end
#=> 0, 10, 20

Hash パターン

こちらも名前に反して、 Hash パターンはハッシュオブジェクト専用ではありません笑

パターン記法
Constant(id: pat, id:, ..., var)
Constant[id: pat, id:, ...,
var]
{id:, id: pat, ..., **var} (BasicObject{ ... } と同義)

Hash パターンは以下の場合にパターンマッチします。

  1. Constant === objecttrue
  2. オブジェクトに Hash を返す #deconstruct_keys メソッドが定義されている
  3. 入れ子になったパターンに対して object.deconstruct_keys(keys)true
  • Hash クラスを使った Hash パターン
class Hash
  def deconstruct_keys(keys)
    self
  end
end

case {a: 0, b: 1}
in Hash(a: a, b: 1)
in Object[a: a, b: 1]
in {a: a, b: 1}
in a: a, b: 1 # `{}` の省略可能
in a:, b: # 値の省略可能
  p a #=> 0
  p b #=> 1
in {a: a, **rest}
  p rest #=> {:b=>1}
end

p a #=> 0

#deconstruct_keys を誤って実装すると非効率的な結果となることがあるので、たくさんのキーを取りたい場合は愚直に書いた方が早いし見やすくなるとので注意が必要とのことでした😮

使ってみた感想

  • Alternative パターンで、in [a, 2, b] | Integer のような書き方はできなかった気がする💦
    • パターンマッチングはまだ実験的なもの👀
  • 様々なパターンが用意されているため、条件分岐をスッキリ書くことができそうなので期待✨
    • でもパターン、色々組み合わせるってなったら難しそう😂

まとめ

「パターンマッチング使って遊んでみた!」みたいな記事をみなさん書かれていて、みんなパターンマッチングを活用していきたい感を多大に感じ取れます🤗

社内でも気になる!って話をたくさん聞いたのは、パターンマッチングに対する期待・可能性を感じている方が多いのだと思います✨ 12月での 2.7.0 のリリースが楽しみですね🎁🎄