メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

Ruby2.7の(実験的)新機能「パターンマッチ」で遊ぶ

はじめまして、メドピアのサーバサイドエンジニアの草分です。

RubyKaigi2019 1日目のセッションにてRubyのexperimental(実験的)な新機能「パターンマッチ」(Pattern Matching)が発表されましたね。

speakerdeck.com

この記事では発表で紹介されたパターンマッチの各種機能を確認し、遊んでみます。

概要

Rubyのパターンマッチとは?

Rubyist向けの説明として以下のような説明がなされていました。

  • データ構造の評価によるcase/when条件分岐の提供
  • 変数への多重代入

実際に例文を見てみましょう。

例文

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

case/whenではなくcase/inが追加され、caseで指定したオブジェクトとinに記載したデータ構造(パターン)を比較し、マッチした場合処理が行われます。

また、マッチした場合はパターンに記載した各変数に値がバインドされ、内部の処理で利用することが可能となります。


通常の多重代入とは異なり、オブジェクトのデータ構造とパターンが一致しない場合は処理されません。

case [0, [1, 2, 3]]
in [a]
  :unreachable # 配列構造が異なるため処理されない
in [0, [a, 2, b]]
  p a #=> 1
  p b #=> 3
end

その他、Hashもサポートされています。

case {a: 0, b: 1}
in {a: 0, x: 1}
  :unreachable # Hashの構造が異なるため処理されない
in {a: 0, b: var}
  p var #=> 1
end

それではこの機能が実装されているRubyを導入し、実際に試してみましょう。

準備

最新のRubyを導入する

  • まずは最新のRubyのソースコードをcloneし、trunkをチェックアウトします。
$ git clone https://github.com/ruby/ruby.git
$ cd ruby
$ git checkout origin/trunk
$ autoconf
$ ./configure
$ make
$ make install
  • しばらく待てば最新Rubyの完成です。

  • rbenvをお使いの場合、2019年5月時点では2.7.0-devバージョンを用いるとパターンマッチが利用可能なrubyをインストールすることができます。
$ rbenv install 2.7.0-dev

解説

基本文法/仕様

試す前に、パターンマッチの基本文法を確認してみましょう。

case expr
in pattern [if|unless condition]
 ...
in pattern [if|unless condition]
 ...
else
 ...
end

case 句に記載したものを検査対象として、以下のように動作します。

  • 記載したパターンを上から順番に評価し、最初にマッチしたものが処理される。
  • 1つもマッチしない場合はelse句の内容が処理される。
  • 1つもマッチせず、else句も存在しなかった場合は NoMatchingPatternError例外が発生する。

また、if/unlessによるguardも可能となっています。

case [1, 1]
in [a, b] unless a == b
  :unreachable # ここは処理されない
end

パターンマッチの基本文法は以上ですが、実行するとwarning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!という警告が表示されます。
警告の通り、パターンマッチは実験的な機能であり将来振る舞いが変更される可能性があります。ご注意ください。

次は、現時点で利用可能なパターンの記法を1つずつ確認してみましょう。

Value pattern

case 0
in 0
in -1..1
in Integer
end
  • ===で比較して一致した対象にマッチします。
  • そのため、上記3パターンはいずれもマッチさせることができます。
    • ※処理が実行されるのは最初にマッチしたパターンのみです。

Variable pattern

case 0
in a
  puts a #=> 0
end
  • 任意の値にマッチし、マッチした値はローカル変数としてバインドされます。
case [0, 1]
in [_, _]
  :reachable
end
  • また、変数として利用しない場合は_を使うことができます。

Alternative pattern

case 0
in 0 | 1 | 2
  :reachable
end
  • | を用いることで複数のパターンにマッチさせることができます。

As pattern

case 0
in Integer => a
  a #=> 0
end
  • =>を用いることでマッチした値を任意の変数にバインドすることができます。
  • rescueの使い方と似た記法ですね。
    • rescue StandardError => e

Array pattern

ここから少し複雑になってきます。

Array patternでは以下のいずれかの記法がパターンとして利用できます。

pat: Constant(pat, ..., *var, pat, ...)
   | Constant[pat, ..., *var, pat, ...]
   | [pat, ..., *var, pat, ...] # BasicObject(...)のシンタックスシュガー

そして以下の条件を満たすとマッチします。

  • Constant(何らかのclassを指定) === 検査対象objectがtrueを返す
  • 検査対象objectの#deconstruct メソッドが配列を返す
  • そこで返した配列と指定したパターンがマッチする

この#deconstructメソッドの使い方が鍵になります。
まずはArrayを対象としたArray patternのパターンマッチから見ていきましょう。

class Array
  def deconstruct
    self
  end
end

case [0, 1, 2] # ↓4種のいずれの記法でもマッチ可能
in Array(0, *a, 2)
in Object[0, *a, 2]
in [0, *a, 2] 
in 0, *a, 2   # []は省略可能
end

次はStructを対象としたArray patternのパターンマッチを見てみましょう。

class Struct
  alias deconstruct to_a
end

Color = Struct.new(:r, :g, :b)
color = Color[255, 0, 0]

case color
in [0, 0, 0]
  puts "Black"
in [255, 0, 0]
  puts "Red"
in [r, g ,b]
  puts "#{r}, #{g}, #{b}"
end

このように、独自に#deconstructを定義することで任意のobjectに対してArray patternによるパターンマッチを行うことができるようになります。
これがRubyのパターンマッチの大きな特徴の1つとなっています。

Hash pattern

Hash patternでは以下のいずれかの記法がパターンとして利用できます。

pat: Constant(id: pat, id:, ..., **var)
   | Constant[id: pat, id:, ..., **var]
   | {id:, id: pat, **var} # BasicObject(...)のシンタックスシュガー

そして以下の条件を満たすとマッチします。

  • Constant(何らかのclassを指定) === 検査対象objectがtrueを返す
  • 検査対象objectの#deconstruct_keys メソッドがHashを返す
  • そこで返したHashと指定したパターンがマッチする

前述のArray patternと似た形式で、Hashのようにkey名を指定してパターンマッチを行うことができます。

class Hash
  def deconstruct_keys(keys)
    self
  end
end

case {a: 0, b: 1} # ↓4種のいずれの記法でもマッチ可能
in Hash(a: a, b: 1)
in Object[a: a]
in {a: a}
in {a: a, **rest}
  p rest #=> {b: 1}
end

こちらも独自に#deconstruct_keysを定義することで任意のobjectに対してパターンマッチを行うことができるようになるため、Rubyのパターンマッチの大きな特徴の1つとなっています。

また、#deconstruct_keysの引数keysには処理の効率化のためのヒントとして、パターンの処理に必要なkey名の配列が渡されます。 その配列に含まれないkeyについては返却せず無視してよく、処理コストの削減を行うことができます。

遊ぶ

クイックソートしてみる

「クイックソートはパターンマッチを用いると書きやすい」という情報を社内で耳にしたため試してみます。

処理対象は数値の配列であることを前提として、ソート処理を書いてみました。

def qsort(ary) 
  case ary
  in [piv, *xs] # 要素が2個以上
    lt, rt = xs.partition{|x| x < piv}
    qsort(lt) + [piv] + qsort(rt)
  else # 要素が0個または1個
    ary
  end
end

ピボットの位置は先頭で決め打ちですが、かなりシンプルに書けますね!

逆にパターンマッチを使用せずに書くとすればこんな感じでしょうか。

def qsort2(ary)
  return ary if ary.length <= 1
  piv = ary[0]

  lt, rt = ary[1..].partition{|x| x < piv}
  qsort2(lt) + [piv] + qsort2(rt)
end

比べてみると、配列長の確認や配列の分割が冗長に見えてきますね。 オブジェクト構造による条件分岐と変数へのバインドが同時に行えるパターンマッチは、かなり強力な文法なのではないでしょうか。

独自拡張クラスを作ってみる

Hash patternをHash以外で使ってみたかったので、Timeクラスを独自拡張し、令和表記で年号を返すメソッドを作ってみました。
As patternと組み合わせて利用しています。

class Time
  def deconstruct_keys(keys)
    { year: year, month: month, day: day }
  end

  def to_reiwa
    case self
    in year: 2019, month: (5..)
      "令和元年"
    in year: (2020..) => year
      "令和#{year - 2018}"
    else
      "not令和"
    end
  end
end

実行してみます。ちゃんと動きましたね。

p Time.local(2019, 5, 1).to_reiwa  # => 令和元年
p Time.local(2019, 8, 1).to_reiwa  # => 令和元年
p Time.local(2020, 1, 1).to_reiwa  # => 令和2年
p Time.local(2019, 4, 30).to_reiwa # => not令和

引数の型指定をしてみる

当日の発表では「引数に対してパターンマッチを実行可能とするのは技術的には可能だが、型の指定に使われることが想定されるので実装しなかった。」という内容の発言がありました。

ということで言いつけを破ってTryしてみましょう。

def arg(num, str)
  case [num, str]
  in [Integer, String]
    :ok
  end
end

このメソッドを呼び出してみると…

p arg(3, "3")     # => :ok
p arg("2", 2)     # => NoMatchingPatternError
p arg(:a, "a")    # => NoMatchingPatternError
p arg(1, :a.to_s) # => :ok

引数の型を間違えると例外をraiseするメソッドになりました! 全くRubyらしくありませんね。

まとめ

RubyKaigi2019で紹介されたパターンマッチの機能を一通り触ってみました。まだ実験的な機能であるとのことで仕様変更の可能性はありますが、正式リリースが楽しみな機能です。

この他にもRuby3で実装が予定されている静的解析や、逆に搭載が見送られた仕様など、将来のRubyに関する具体的な情報が次々発表されたRubyKaigi2019でした。今後のRubyの動向から目が離せませんね!


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html