Help us understand the problem. What is going on with this article?

サンプルコードでわかる!Ruby 2.7の新機能・パターンマッチ(前編)

はじめに

Rubyは毎年12月25日にアップデートされます。
Ruby 2.7については2019年11月23日にpreview3がリリースされました。

Ruby 2.7.0-preview3 リリース

この記事ではRuby 2.7で導入される変更点や新機能について、サンプルコード付きでできるだけわかりやすく紹介していきます。

ただし、Ruby 2.7は多くの新機能や変更点があり、1つの記事に収まらないのでいくつかの記事に分けて書いていきます。
本記事で紹介するのはパターンマッチ(もしくはパターンマッチング)です。

前編と後編にわかれています

パターンマッチは説明する内容が多いので、次のように前編と後編の2部構成になっています。

  • 前編 = パターンマッチの概要、case文っぽい使い方、配列やハッシュとのマッチ、変数への代入
  • 後編 = 自作クラスをパターンマッチで活用する方法、パターン名の整理

後編をご覧になりたい方はこちらからどうぞ。

サンプルコードでわかる!Ruby 2.7の新機能・パターンマッチ(後編) - Qiita

パターンマッチ以外のRuby 2.7の新機能はこちら

Ruby 2.7ではパターンマッチ以外にもさまざまな新機能や変更点があります。
それらについては以下の記事で紹介しています。こちらもあわせてご覧ください。

本記事の情報源

本記事は以下のような情報源をベースにして、記事を執筆しています。
(下記の情報源はいずれもRubyにおけるパターンマッチの提唱者・開発者である@k_tsjさんによるものです)

動作確認したRubyのバージョン

本記事は以下の環境で実行した結果を記載しています。

$ ruby -v
ruby 2.7.0preview3 (2019-11-23 master b563439274) [x86_64-darwin19]

(2019.12.26追記)正式リリースされたRuby 2.7.0でも動作確認済みです。

$ ruby -v
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin19]

フィードバックお待ちしています

本文の説明内容に間違いや不十分な点があった場合は、コメント欄や編集リクエスト等で指摘 or 修正をお願いします🙏

それでは以下が本編です!

Step 0. パターンマッチの概要

Ruby 2.7では新しくパターンマッチ構文が導入されました。
構文自体はこのあとで詳しく説明するので、まずここではパターンマッチの概要について説明しておきます。

パターンマッチとは

パターンマッチ(もしくはパターンマッチング)は関数型言語でよく使われる条件分岐の一種です。
Wikipediaには「多分岐の一種で、場合分けと同時に構成要素の取り出しのできる言語機能」という説明があります。

また、Rubyにおけるパターンマッチはざっくりいうと、「多重代入ができるcase文」です(参考)。

ただ、この説明だけではほとんどの人はまだピンと来ないと思います。
パターンマッチの構文や機能的な説明については、このあとで詳しく説明していきます。

ざっくりとした構文説明

Rubyのパターンマッチは次のようにcase/inを使います。

case [0, [1, 2]]
in [0, [1, a]]
  # パターンがマッチした場合の処理
else
  # それ以外の処理
end

見た目上はcase文によく似ていますが(そしてcase文とよく似た使い方もできますが)、機能的には別物です。
上のサンプルコードでもこれまでのcase文では説明が付かない点が隠れています。
具体的にパターンマッチでどんなことができるのかは、このあとの記事の中で説明していきます。

後方互換性に十分配慮した設計になっている

Rubyのパターンマッチは今回新しく導入された機能であるため、新しい予約語を追加したりすると、既存のRubyのコードが動かなくなる可能性があります。
そのため、caseinといった既存の予約語を活用するなどして、後方互換性に十分配慮した設計になっています。

また、Rubyが元から有している配列やハッシュのパワフルさを活用したり、静的な型よりもダックタイピングを重視したりするなど、「Rubyらしさを失わないこと」も設計の重要なポイントのひとつになっているそうです。

こうした設計上の詳しい裏話は下記スライドの33枚目〜44枚目を参照してください。

https://speakerdeck.com/k_tsj/rubyconf2019-pattern-matching-new-feature-in-ruby-2-dot-7?slide=33

Ruby 2.7ではまだ試験段階

Ruby 2.7の時点では、パターンマッチはまだ試験的(experimental)な実装となっています。
将来的に仕様が変更される可能性があるため、パターンマッチ構文を使うとその都度警告が発生します。

# パターンマッチ構文を使うとその都度警告が出る
case 0
in 0
  'zero'
end
#=> warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!

また、パフォーマンスも改善の余地が大きいとのことです。(参考
ですので、実務で書くコードにパターンマッチ構文を組み込むのは、Ruby 2.7の時点ではまだ避けた方が良いかもしれません。

パターンマッチの提唱者・開発者の@k_tsjさんいわく「実際に使って気になった点をフィードバックしてほしい」とのことですので(参考)、「こういうユースケースでこんなふうに使いたい」「ここの挙動がちょっと気になる」等の意見があれば、積極的にフィードバックを上げていくのが良いと思います。

警告の表示を抑制したい場合

(2019.12.26追記)パターンマッチを積極的に使ってみたいが警告が多すぎて困る、という場合はコマンドラインオプションやRUBYOPT環境変数を使って警告の出力を抑制することができます。

# コマンドラインオプションで試験的機能の警告を抑制する
ruby -W:no-experimental your_code.rb
# RUBYOPT環境変数で試験的機能の警告を抑制する
RUBYOPT=-W:no-experimental bundle exec rspec

警告の抑制に関する詳しい内容は以下の記事をご覧ください。

パターンマッチの概要については以上です。
それではこれからパターンマッチの使い方を説明していきます。

Step 1. 一種のcase文として使う

パターンマッチはcase文っぽく使うことができます。

# case文
case 1
when 0
  'zero'
when 1
  'one'
end
#=> one

# パターンマッチ
case 1
in 0
  'zero'
in 1
  'one'
end
#=> one

whenがinに変わったところ以外は同じですね。

case文と同様に戻り値を変数に代入することも可能です。

# パターンマッチ全体の戻り値を変数に代入する
ret =
  case 1
  in 0
    'zero'
  in 1
    'one'
  end

ret #=> one

else節で「それ以外の条件」を指定したり、範囲オブジェクトやクラス名(定数)が渡せるところも同じです。

# 範囲オブジェクトで比較する例 ===========
# case文
case 2000
when 1926..1988
  '昭和'
when 1989..2018
  '平成'
else
  '令和'
end
#=> 平成

# パターンマッチ
case 2000
in 1926..1988
  '昭和'
in 1989..2018
  '平成'
else
  '令和'
end
#=> 平成

# クラス名(定数)で比較する例 ===========
# case文
case 'abc'
when Time
  '時間です'
when Integer
  '整数です'
when String
  '文字列です'
end
#=> 文字列です

# パターンマッチ
case 'abc'
in Time
  '時間です'
in Integer
  '整数です'
in String
  '文字列です'
end
#=> 文字列です

少し技術的なことを説明すると、上の例ではcase文もパターンマッチも===で比較して真になれば節が実行されます。

(1989..2018) === 2000
#=> true

String === 'abc'
#=> true

また、case文もパターンマッチも、上から順に評価して最初に一致した節を実行する、という挙動は同じです。

# case文
case 'abc'
when Object
  'オブジェクトです'
when Integer
  '整数です'
when String
  # 文字列ではあるが、when Objectが先に一致する
  '文字列です'
end
#=> オブジェクトです

# パターンマッチ
case 'abc'
in Object
  'オブジェクトです'
in Integer
  '整数です'
in String
  # 文字列ではあるが、in Objectが先に一致する
  '文字列です'
end
#=> オブジェクトです

case文もパターンマッチも、thenを使って次のように書くことができます。

# case文
case 1
when 0 then 'zero'
when 1 then 'one'
end
#=> one

# パターンマッチ
case 1
in 0 then 'zero'
in 1 then 'one'
end
#=> one

複数の条件を並べることもできます。
ただし、パターンマッチでは,ではなく|で条件を並べます。

# case文
case 'melon'
when 'tomato', 'potato', 'carrot'
  '野菜です'
when 'orange', 'melon', 'banana'
  '果物です'
end
#=> 果物です

# パターンマッチ(,ではなく、|で条件を並べる)
case 'melon'
in 'tomato' | 'potato' | 'carrot'
  '野菜です'
in 'orange' | 'melon' | 'banana'
  '果物です'
end
#=> 果物です

パターンマッチではどの条件にも一致しなければ例外が発生する

さて、ここまでは「case文もパターンマッチもほとんど同じです」というような説明をしてきました。
しかし、それではパターンマッチを使う理由がほとんどありません。
上で説明してきたようなコード例であれば、従来通りのcase文を使う方がよいと思います。

大きな違いがあるとすれば、どの条件にも一致しなかったときの振る舞いです。
以下のコード例を見てください。

# case文(どの条件にも一致しなければ何も起きずnilが返る)
case 'chicken'
when 'tomato', 'potato', 'carrot'
  '野菜です'
when 'orange', 'melon', 'banana'
  '果物です'
end
#=> nil

# パターンマッチ(どの条件にも一致しなければ例外が発生する)
case 'chicken'
in 'tomato' | 'potato' | 'carrot'
  '野菜です'
in 'orange' | 'melon' | 'banana'
  '果物です'
end
#=> NoMatchingPatternError (chicken)

上のように、パターンマッチではどの条件にも一致せず、なおかつelse節も用意されていなかった場合は、NoMatchingPatternErrorが発生します。
ですので、「予想外の値が渡ってきたら例外を発生させる」というコードをわざわざ書かずに済みます。

たとえば以下はタスクのステータスに応じて条件分岐させる架空のコード例です。

# case文で書く場合
case task.status
when :todo
  # ...
when :doing
  # ...
when :done
  # ...
else
  # 知らない間に新しいstatusが追加された可能性があるので
  # 例外を発生させる(考慮漏れの防止)
  raise "Unknown status: #{task.status}"
end

# パターンマッチで書く場合
case task.status
in :todo
  # ...
in :doing
  # ...
in :done
  # ...
# else
  # 考慮漏れがあれば例外が発生するのでelseは不要
end

Step 1の説明は以上です。
ここではパターンマッチを一種のcase文として使う方法を説明しました。
しかし、本当にパターンマッチらしい使い方は次のStep 2からになります。

コラム:文法的な解説を少し

ここでは新しい構文を強調する目的で、従来のcase文とパターンマッチを対比する形で書きましたが、厳密に言えばこれはどちらもRubyのcase式(値を返すので文ではなく式)です。
これまではcase/whenの構文しかなかったRubyのcase式に、パターンマッチを実現するためにcase/inの構文が新たに導入された、というのが正しい解釈になります。

ですので、Step 1で解説した内容は「従来のcase/whenを使ったcase式 vs パターンマッチを実現するために導入されたcase/inを使ったcase式(パターンマッチ構文)」と書くのが正確ですが、長くなりすぎる上にそれはそれで読者のみなさんの混乱を招きそうなので、「case文 vs パターンマッチ」と書いています。

また、ここからあとでの説明でも「パターンマッチ構文」の意味で「パターンマッチ」と記述している箇所があります。

Step 2. 配列やハッシュの構造で分岐させる + 変数に代入する

Step 1では、あえてcase文とよく似た使い方を説明しました。
構文もよく似ているので、一見するとcase文とパターンマッチは同じような機能しかないように見えるかもしれません。
ですが、このStep 2ではcase文とは全く異なるパターンマッチの使い方を説明していきます。

まずは配列でパターンマッチを使ってみましょう。

# 配列とパターンマッチを組み合わせる
case [3, 4, 5]
in [0, 1, 2]
  '0-2'
in [3, 4, 5]
  # ここが実行される
  '3-5'
in [6, 7, 8]
  '6-8'
end
#=> 3-5

上の例ではin [3, 4, 5]の節が実行されました。
でも、これだとcase文と構文以外の差異はないですし(inをwhenに変えても同じ結果になります)、特に違和感はありませんね。

それでは次のコードはどうでしょうか?

case [3, 4, 5]
in [0, 1, n]
  '0-2'
in [3, 4, n]
  # ここが実行される(のはなぜ??)
  '3-5'
in [6, 7, n]
  '6-8'
end
#=> 3-5

突然、変数らしきnが登場しました。
ですが、n = 1のようなローカル変数の宣言はどこにもありません。(書き忘れているわけではないですよ!)
そして、どういうわけかin [3, 4, n]の節が実行されています。

つづいて、上のコードを次のように変えてみましょう。

case [3, 4, 5]
in [0, 1, n]
  "0, 1 and #{n}"
in [3, 4, n]
  # nに5が代入される(のはなぜ??)
  "3, 4 and #{n}"
in [6, 7, n]
  "6, 7 and #{n}"
end
#=> 3, 4 and 5

なんと、"3, 4 and #{n}"n5が代入されています。
それなら、case [3, 4, 5]case [3, 4, 100]に変えてみるとどうでしょうか?

case [3, 4, 100]
in [0, 1, n]
  "0, 1 and #{n}"
in [3, 4, n]
  # nに100が代入される(のはなぜ??)
  "3, 4 and #{n}"
in [6, 7, n]
  "6, 7 and #{n}"
end
#=> 3, 4 and 100

今度はn100が代入されました!

では、そろそろちゃんと上のコードの意味を説明しましょう。
上のパターンマッチで使われているin [3, 4, n]の意味は次のように解釈してください。

  • 要素が3つの配列で、最初の2つが34であれば、マッチしたものと見なす
  • 3つ目の要素は任意。5でも100でも10000でもよい
  • さらに、3つ目の要素をローカル変数nに代入する

これは今までのRuby文法にはなかった、まったく新しい仕組みです。
表面的にはcase文っぽく見えるので、つい頭の中で「従来のcase文の考え方」に当てはめたくなるかもしれませんが、いったんその考えは捨ててください。

「要素が3つの配列で、最初の2つが3と4、3つ目は任意」という、「オブジェクト(ここでは配列)の構造」を条件分岐の判定条件に使っている点と、=演算子なしでローカル変数への代入が実現できている点が、パターンマッチのまったく新しい考え方になります。
(ピンと来ない人はピンと来るまで、上のコードと説明を繰り返し読み直しましょう!)

コラム:代入ではなく束縛?

ちなみに、パターンマッチはもともと関数型言語でよく使われている構文です。
関数型言語では「変数への代入」ではなく「変数束縛(variable binding)」という言い方をします。

in [3, 4, n]は、「nに5を代入する」というよりも、「構造的に一致した3つ目の値をつかまえて、変数nに縛りつけた(束縛した)」というふうに解釈した方が、個人的にはしっくりくるような気もします。

ハッシュでも同じように構造で判定して、変数に代入できる

上の例では配列を使いましたが、ハッシュでも同じようなことを実現できます。

case {status: :error, message: 'User not found.'}
in {status: :success, message: message}
  "Success!"
in {status: :error, message: message}
  # ここが実行される
  "Error: #{message}"
end
#=> Error: User not found.

配列の例が理解できていれば、ハッシュの場合も解釈はそこまで難しくないと思います。
上のコードではin {status: :error, message: message}の節が実行されています。
これは次のように読み解いてください。

  • キーが:statusで値が:errorである要素と、キーが:messageで値は任意である要素が含まれるハッシュであれば、マッチしたものと見なす
  • キーが:messageである要素の値は、ローカル変数messageに代入される

ちなみに、変数として参照する必要がない任意の値は、_を使って無視することもできます。

# :statusが:successの場合
case {status: :success, message: ''}
in {status: :success, message: _}
  # :statusが:successの場合は:messageの値は任意かつ、参照しないので _ を使う
  "Success!"
in {status: :error, message: message}
  "Error: #{message}"
end
#=> Success!

さらに言うと、パターンマッチでハッシュを使う場合は「部分一致」でマッチしたかどうかが判定されます。
ですので、次のように書けばさらにシンプルになります。

case {status: :success, message: ''}
in {status: :success}
  # :statusがキーで値が:successの要素が含まれる(それ以外の要素は何でもよい)、
  # という条件に一致すれば、この節が実行される
  "Success!"
in {status: :error, message: message}
  "Error: #{message}"
end
#=> Success!

# ちなみに配列の判定条件は完全一致
case [0, 1, 2]
in [0, n]
  'NO'
in [0, 1, n]
  'YES'
end
#=> YES

ハッシュと配列を組み合わせる

パターンマッチの判定条件には配列とハッシュを組み合わせることもできます。

# 下記ページのサンプルコードを一部改変
# https://speakerdeck.com/k_tsj/rubyconf2019-pattern-matching-new-feature-in-ruby-2-dot-7?slide=10

data = {
  name: 'Alice',
  age: 30,
  zodiac: 'Capricorn',
  children: [
    {
      name: 'Bob',
      age: 10,
      zodiac: 'Aquarius'
    }
  ]
}

case data
in {name: 'Alice', children: [{name: 'Bob', age: age}]}
  "Bob is #{age} years old."
end
#=> Bob is 10 years old.

上のサンプルコードのパターンマッチは、次のように解釈します。

  • キーが:nameで値が'Alice'である要素と、キーが:childrenで値が配列である要素が含まれるハッシュであり、さらにその配列の要素数は1つで、その要素がキーが:nameで値が"Bob"、そしてキーが:ageで値は任意の要素を含むハッシュになっていればマッチする
  • :childrenの配列の中に含まれるハッシュのキー:ageに対応する値は、ローカル変数ageに代入される

文章にすると結構複雑になりますね。
文章で理解しようとするよりも、コードをじっくり読んだ方がこの場合は近道になるかもしれません。

コラム:inのうしろは、実は配列やハッシュそのものではなく「パターン」

実はinのうしろに登場する配列やハッシュは一見「配列やハッシュそのもの(つまり配列オブジェクトやハッシュオブジェクト)」に見えますが、そうではありません。
厳密に言うと、これらは「パターン」です。
そして、たまたま(というか意図的に)パターンを表す構文が、配列リテラルやハッシュリテラルに一致しているだけです。

case [3, 4, 5] # <= これは配列オブジェクト
in [3, 4, 5]   # <= これはパターン(配列オブジェクトではない)
  # ...
end
case {a: 1, b: 2} # <= これはハッシュオブジェクト
in {a: 1, b: 2}   # <= これはパターン(ハッシュオブジェクトではない)
  # ...
end

ですので、このあとの説明でも「一見、配列やハッシュっぽいが、従来の配列リテラルやハッシュリテラルでは説明が付かない構文」がたくさん出てきます。

ちなみに、パターンマッチ構文の設計では一時期、次のような構文も検討されていたことがあるそうです(出典:n月刊ラムダノート Vol.1, No.3(2019))。
これを見ると、配列オブジェクトやハッシュオブジェクトではなくパターンである、ということが見た目にも分かりやすいのではないでしょうか。

# n月刊ラムダノート Vol.1, No.3(2019) p.75より抜粋
case [:ok, 200]
when %p(:ok, x)
  x #=> 200
end

また、後編の「コラム:inの後ろはまったく新しい世界」でも、別の観点から同じ話題を取り上げます。

注意点1:ハッシュで使えるキーはシンボルのみ

さて、ここからはここまで説明したパターンマッチの使い方で、注意すべき点をいくつか説明します。

まず、Ruby 2.7の時点ではハッシュのキーにはシンボルしか使えないという制限があるので注意してください。
(これは後述する変数代入のための=>と、ハッシュリテラルの=>が構文的に衝突してしまうためです)

# シンボル以外のキーで比較しようとすると構文エラーが発生する
case {'status' => :success, 'message' => ''}
in {'status' => :success}
  "Success!"
end
#=> syntax error, unexpected terminator, expecting literal content or tSTRING_DBEG or tSTRING_DVAR or tLABEL_END (SyntaxError)
#       in {'status' => :success}
#                  ^

注意点2:パターンマッチで代入できる変数はローカル変数のみ

パターンマッチで代入できるのはローカル変数のみです。
次のようにインスタンス変数に代入しようとすると構文エラーが発生します。

# インスタンス変数に代入しようとすると構文エラーになる
case 0
in @a
  # ...
end
#=> syntax error, unexpected instance variable
#   in @a

ちなみに「ローカル変数以外の変数にもパターンマッチで代入できた方が便利なのでは?」という提案をしてみましたが、「パターンマッチがさらに複雑になってしまう」という理由でいったん却下されています。(参考

注意点3:マッチに失敗しても変数の代入は完了している

次のコードのようにマッチに成功しなかった分岐があっても、ローカル変数への代入は完了します。

case [0, 1, 2]
# この条件にはマッチしない(が、変数nへの代入は完了している)
in [n, 1, 3]
  'matched in'
else
  'matched else'
end
#=> matched else

# パターンマッチで使ったnに0が代入されている
n #=> 0

ただし、パターンマッチで使われた変数のスコープに関しては将来的に仕様が改善される可能性があります。

参考 https://speakerdeck.com/k_tsj/rubyconf2019-pattern-matching-new-feature-in-ruby-2-dot-7?slide=45

注意点4:in節で同名の変数やキーは使用できない

パターンマッチでは、次のようにin節で同名の変数を使うと構文エラーが発生します。

# in節で同名の変数を使っているので構文エラーになる
case [0, 0]
in [a, a]
  # ...
end
#=> duplicated variable name
#      in [a, a]

ただし、任意の値を表す_は重複してもエラーにはなりません。

# _ は何度でも使える
case [0, 0]
in [_, _]
  # ...
end

ハッシュで同名のキーを指定した場合も構文エラーが発生します。

# in節で同名のキーを使っているので構文エラーになる
case {a: 0}
in {a: 0, a: 1}
  # ...
end
#=> duplicated key name
#      in {a: 0, a: 1}

さて、パターンマッチに配列やハッシュを使ったり、変数に代入したりする場合の基本的な考え方は、だいたい説明しました。
ここからあとではさらに、応用的な使い方や発展的な使い方を説明していきます。

応用1:既出の変数の値そのものを条件に使う ^ (ピン演算子)

先ほど、パターンマッチでは判定のタイミングでローカル変数への代入ができると説明しました。
ですので、みなさんはもう次のコードでやっていることが理解できるはずです。

case 10
in 0
  'zero'
in x
  # 0以外の任意のオブジェクトが変数xに代入される
  "#{x}です"
end
#=> 10です

では上のコードですでに変数xが宣言されているとどうなるでしょうか?
やってみましょう。

# 変数xを宣言しておく
x = 1

case 10
in 0
  'zero'
in x
  # xに新しい値(ここでは10)が代入される
  "#{x}です"
end
#=> 10です

# ローカル変数xの中身も書き換えられている
x
#=> 10

ご覧のとおり、既出の変数xはパターンマッチのタイミングで新しい値が代入されました。

しかし、場合によっては変数xそのものの値(上の例なら1)をパターンマッチの条件に使いたい、という場面もあると思います。
そういう場合は^xのように、変数の手前に^(ピン演算子)を付けます。

x = 1

case 10
in 0
  'zero'
in ^x
  # in 1 と書いたことに等しい(ので、マッチするパターンがなく例外が発生する)
  "#{x}です"
end
#=> NoMatchingPatternError (10)

case 1
in 0
  'zero'
in ^x
  # case 1 なので、今回はマッチする
  "#{x}です"
end
#=> 1です

ご覧のとおり、^を付けることで変数xの値そのものをパターンマッチの条件として使うことができました。

ちなみに、上の例に近いユースケースとして、文字列の式展開であれば、^なしで変数を参照できます。

name = 'Alice'

# 文字列の式展開は ^ なしで変数を参照できる
case "Hi, Alice!"
in "Hi, #{name}!"
  'match!'
end
#=> match!

コラム:複雑な式(メソッド呼び出しなど)は直接in節に指定できない

パターンマッチでは次のようにメソッド呼び出しを含む式をin節に記述することはできません(構文エラーになります)。

case 1
# randメソッドの戻り値を直接in節に指定することはできない(構文エラー)
in rand(1..3)
  # ...
end
#=> syntax error, unexpected '(', expecting `then' or ';' or '\n'
#   in rand(1..3)
#   syntax error, unexpected ')', expecting end-of-input
#   in rand(1..3)

# ちなみにcase文であればwhen節に複雑な式も書ける(構文として有効)
case 1
when rand(1..3)
  # ...
end

ただし、次のようにいったん変数に入れて^演算子を使えば、もともと意図していたコードと同等のコードが書けます。

# randメソッドの戻り値をいったん変数nに入れる
n = rand(1..3)

# 変数nと^演算子を組み合わせれば、結果としてrandメソッドの戻り値で比較したことになる
case 1
in ^n
  # ...
end

この点については、将来的に^(rand(1..3))のような構文をサポートするかもしれないとのことです(参考)。

# 将来的にはこんな構文がサポートされるかも?
case 1
in ^(rand(1..3))
  # ...
end

応用2:マッチした構造の一部(または全体)を変数に代入する =>

パターンマッチでは=> (変数名)という構文を使うことで、マッチしたオブジェクトを変数に代入することができます。
たとえばこんな感じです。

case 123
in String
  # 文字列ではないのでマッチしない
in Integer => i
  # 123は整数(Integerクラスのインスタンス)なのでマッチする
  # さらに、マッチした値が変数iに代入される
  "#{i}です"
end
#=> 123です

配列やハッシュと=>を組み合わせると、マッチした構造の一部をまるっと変数に代入することもできます。
たとえば、先ほど「ハッシュと配列を組み合わせる」の項では次のようなコードを紹介しました。

data = {
  name: 'Alice',
  age: 30,
  zodiac: 'Capricorn',
  children: [
    {
      name: 'Bob',
      age: 10,
      zodiac: 'Aquarius'
    }
  ]
}

case data
in {name: 'Alice', children: [{name: 'Bob', age: age}]}
  "Bob is #{age} years old."
end
#=> Bob is 10 years old.

このとき、:childrenの値である配列部分がほしいと思ったときはどうすればいいでしょうか?
もし、そのままやろうとすると次のようなコードになります。

case data
in {name: 'Alice', children: [{name: 'Bob', age: age}]}
  children = [{name: 'Bob', age: age}]
  # childrenを使った処理が続く...
end

しかし、上のコードはDRYではないのでイマイチですね。
こんなときは=>を使って変数に代入すると便利です。

case data
in {name: 'Alice', children: [{name: 'Bob', age: age}] => children}
  # [{name: 'Bob', age: age}] にあたる部分が変数childrenに代入される
  p children
  #=> [{:name=>"Bob", :age=>10, :zodiac=>"Aquarius"}]
end

ただし、上の実行結果を見てもらえればわかるとおり、変数childrenの中身は[{:name=>"Bob", :age=>10}]だけではなく、inの条件には指定していない:zodiac=>"Aquarius"も含まれている点に注意してください。(つまり、data[:children]で得られる結果と同じになっています)

また、一番外側に=>を書けば、条件にマッチした構造全体を取得することができます。

case data
in {name: 'Alice', children: [{name: 'Bob', age: age}]} => alice
  p alice
  #=> {:name=>"Alice", :age=>30, :zodiac=>"Capricorn", :children=>[{:name=>"Bob", :age=>10, :zodiac=>"Aquarius"}]}
end

2つ以上の=>を同時に使うこともできます。

case data
in {name: 'Alice', children: [{name: 'Bob', age: age}] => children} => alice
  p alice
  #=> {:name=>"Alice", :age=>30, :zodiac=>"Capricorn", :children=>[{:name=>"Bob", :age=>10, :zodiac=>"Aquarius"}]}

  p children
  #=> [{:name=>"Bob", :age=>10, :zodiac=>"Aquarius"}]
end

配列でも同じように=>が使えます。

# 下記ページのサンプルコードを一部改変
# https://speakerdeck.com/k_tsj/rubyconf2019-pattern-matching-new-feature-in-ruby-2-dot-7?slide=23

case [0, [1, 2]]
in [0, [1, _] => arr]
  # マッチした内側の配列を変数arrに代入
  p arr
  #=> [1, 2]
end

case [0, [1, 2]]
in [0, [1, _]] => arr
  # マッチした配列全体を変数arrに代入
  p arr
  #=> [0, [1, 2]]
end

ただし、elseに=>を使うことはできません。(構文エラーになります)

case []
in String
  # ...
in Integer
  # ...
else => obj
  # else => は構文エラー
  "#{obj}です"
end
#=> syntax error, unexpected =>
#   else => obj

ちなみに上の例であれば、in objのように書けば任意の値を変数objに代入できます。

case []
in String
  # ...
in Integer
  # ...
in obj
  # 文字列でも整数でもないオブジェクトが変数objに代入される
  "#{obj}です"
end
#=> "[]です"

応用3:ガード条件を追加する

パターンマッチにはifやunlessを使ったガード条件を追加することができます。

# ageが20より大きいこと、というガード条件を付ける(マッチしない)
case {name: 'Alice', age: 20}
in {name: 'Alice', age: age} if age > 20
  'Hi, Alice!'
end
#=> NoMatchingPatternError ({:name=>"Alice", :age=>20})

# ageが20以下であること、というガード条件を付ける(マッチする)
case {name: 'Alice', age: 20}
in {name: 'Alice', age: age} if age <= 20
  'Hi, Alice!'
end
#=> Hi, Alice!

こちらはifの代わりにunlessを使う例です。

# ageが偶数でないこと、という条件を付ける(マッチしない)
case {name: 'Alice', age: 20}
in {name: 'Alice', age: age} unless age.even?
  'Hi, Alice!'
end
#=> NoMatchingPatternError ({:name=>"Alice", :age=>20})

=> で代入した変数もガード条件に使えます。

case 0
in Integer => n if n.zero?
  'zero'
end
#=> zero

応用4:*で配列のその他の要素を指定する

*を使うと配列のその他の要素を指定することができます。

case [0, 1, 2, 3, 4]
in [0, *rest]
  # 配列の要素が1個以上で最初の要素が0ならマッチする
  # さらに、2番目以降の要素は変数restに代入される
  p rest
end
#=> [1, 2, 3, 4]

case [0]
in [0, *rest]
  # 0以外の要素がないので、この場合はrestには空の配列が代入される
  p rest
end
#=> []

次のような少し凝った使い方も可能です。

case [0, 1, 2, 3, 4]
in [first, second, *rest, last]
  # 1番目、2番目、最後の要素をそれぞれ変数に入れる
  puts "1st: #{first}, 2nd: #{second}, last: #{last}"
  # それ以外の要素は配列として受け取る
  p rest
end
#=> 1st: 0, 2nd: 1, last: 4
#   [2, 3]

変数名なしで*だけで使うこともできます。

case [0, 1, 2, 3, 4]
in [*, 4]
  # 最後が4で終わる配列にマッチ(それより手前の要素は任意)
  'end with 4'
end
#=> end with 4

case [4]
in [*, 4]
  # この場合、配列の要素数は1個でも良い
  'end with 4'
end
#=> end with 4

応用5:**でハッシュの残りの要素を指定する / ハッシュを完全一致させる

配列の*と同じように、**を使うとハッシュの残りの要素を指定することができます。

case {japan: 'yen', us: 'dollar', india: 'rupee'}
in {japan: _, **rest}
  # キーに:japanを含むハッシュ(値は任意)にマッチ
  # さらに、キーが:japan以外の要素は変数restに代入される
  p rest
end
#=> {:us=>"dollar", :india=>"rupee"}

**を使う場合は最後の要素として記述します。
次のように途中で挟みこまれるような形で書くと構文エラーが発生します。

# **は最後に来ないと構文エラーが発生する
case {japan: 'yen', us: 'dollar', india: 'rupee'}
in {japan: _, **rest, us: _}
  p rest
end
#=> syntax error, unexpected ',', expecting '}'
#   in {japan: _, **rest, us: _}

# **が最後に来ているのでこれはOK
case {japan: 'yen', us: 'dollar', india: 'rupee'}
in {japan: _, us: _, **rest}
  p rest
end
#=> {:india=>"rupee"}

**nilと書くと、「それ以外の要素がないこと」を指定したことになります。
これにより、ハッシュのパターンマッチを部分一致ではなく、完全一致で指定することができます。

# これは部分一致
case {status: :success, message: ''}
in {status: :success}
  # 何も指定しない場合はハッシュは部分一致する
  # この場合は:statusがキーで値が:successの要素が含まれれば、それ以外の要素は何でもよい
  "Success!"
end
#=> Success

# これは完全一致
case {status: :success, message: ''}
in {status: :success, **nil}
  # **nilで「それ以外の要素がないこと」を指定する
  # この場合は:statusがキーで値が:successの、要素が1つだけあるハッシュでないとマッチしない
  "Success!"
end
#=> NoMatchingPatternError ({:status=>:success, :message=>""})

# これも完全一致
case {status: :success}
in {status: :success, **nil}
  # :statusがキーで値が:successの、要素が1つだけのハッシュなのでマッチ
  "Success!"
end
#=> Success

なお、in {}と書いた場合は「空のハッシュ」にマッチ(つまり完全一致)します。

case {}
in {}
  'empty'
end
#=> empty

応用6:一番外側の[]{}を省略する

配列やハッシュをinに書く場合は、一番外側の[]{}を省略できます。

case [0, [1, 2]]
# 一番外側の[]を省略しない場合
in [0, [a, b]]
  # ...
end

case [0, [1, 2]]
# 一番外側の[]を省略する場合
in 0, [a, b]
  # ...
end
case {name: 'Alice', age: 20}
# 一番外側の{}を省略しない場合
in {name: 'Alice', age: age}
  # ...
end

case {name: 'Alice', age: 20}
# 一番外側の{}を省略する場合
in name: 'Alice', age: age
  # ...
end

ただし、=>を使ってマッチしたオブジェクト全体を取得する場合は、外側の[]{}を書く必要があります。

case [0, [1, 2]]
in 0, [1, 2] => a
  # 外側の[]を省略したので、内側の[1, 2]がaに代入される
  p a
end
#=> [1, 2]

case [0, [1, 2]]
in [0, [1, 2]] => a
  # 外側の[]を書いたので、配列全体がaに代入される
  p a
end
#=> [0, [1, 2]]

case {a: {b: 2, c: 3}}
in a: {b: 2, c: _} => h
  # 外側の{}を省略したので、内側の{b: 2, c: 3}がhに代入される
  p h
end
#=> {:b=>2, :c=>3}

case {a: {b: 2, c: 3}}
in {a: {b: 2, c: _}} => h
  # 外側の{}を書いたので、ハッシュ全体がhに代入される
  p h
end
#=> {:a=>{:b=>2, :c=>3}}

応用7:変数名を省略し、暗黙的にハッシュのキーと同名の変数に代入する

パターンマッチでは変数名を省略し、暗黙的にハッシュのキーと同名の変数に代入することができます。
次の2つのコードはどちらも同じことをやっていますが、前者は明示的に変数名を指定し、後者は変数名を省略して暗黙的にキーと同名の変数に代入しています。

case {name: 'Alice', age: 20}
# 変数名を明示的に指定する
in name: name, age: age
  "#{name} is #{age} years old"
end
#=> Alice is 20 years old

case {name: 'Alice', age: 20}
# 変数名の指定を省略し、暗黙的にキーと同名の変数に代入することも可能
in name: , age:
  "#{name} is #{age} years old"
end
#=> Alice is 20 years old

コラム:パターンマッチを1行で書く

パターンマッチは次のように1行で書くこともできます。

# 1行で書くときはcaseが不要
[0, 1, 2] in [0, 1, n]

この構文を利用すると、次のようにしてハッシュの値を変数に代入することができます。

data = {name: 'Alice', age: 20, zodiac: 'Capricorn'}

# 1行で書くパターンマッチを利用して、キーに対応する値を暗黙的に変数に代入する
# (ただし、この場合は{}を省略できない)
data in {name:, zodiac:}
"name=#{name}, zodiac=#{zodiac}"
#=> name=Alice, zodiac=Capricorn

# values_atメソッドでも同じことができるが、変数名を明示的に書く必要がある(ので、ちょっとDRYでない)
name, zodiac = data.values_at(:name, :zodiac)
"name=#{name}, zodiac=#{zodiac}"
#=> name=Alice, zodiac=Capricorn

ちなみに、パターンマッチを1行で書いた場合の戻り値はnilになります。

[0, 1, 2] in [0, 1, n]
#=> nil

応用8:in節で配列パターンと範囲オブジェクトやクラス名を組み合わせる

in節では配列パターンと範囲オブジェクトやクラス名を組み合わせることもできます。
言葉では説明しづらいので、次のコードを見てもらった方がわかりやすいかもしれません。

# 要素が2個で、最初の値が-1から1の間に含まれればマッチ
case [0, 1]
in [-1..1, _]
  'match!'
end
#=> match!

# 要素が2個で、最初の値がInteger型であればマッチ
case [0, 1]
in [Integer, _]
  'match!'
end
#=> match!

応用9:in節で配列パターンと|を組み合わせる

少し複雑になりますが、in節で配列パターンと|を組み合わせることもできます。

# 最初の要素が0で、2つめの要素が[1, 2]または['one', 'two']ならマッチ
case [0, [1, 2]]
in [0, [1, 2] | ['one', 'two']]
  'match!'
end
#=> match!

# 同上
case [0, ['one', 'two']]
in [0, [1, 2] | ['one', 'two']]
  'match!'
end
#=> match!

ただし、|の左右に書いたパターンで変数を登場させると構文エラーになります。

# nのような変数を登場させると構文エラー
case [0, [1, 2]]
in [0, [1, n] | ['one', 'two']]
  # ...
end
#=> illegal variable in alternative pattern (n)
#   compile error (SyntaxError)

# |と無関係な場所に書いた変数なら問題ない
case [0, [1, 2]]
in [n, [1, 2] | ['one', 'two']]
  n
end
#=> 0

一方、_であれば、|の左右に登場しても構文エラーにはなりません。

# _ であれば | の左右に含まれていてもエラーにならない
case [0, [1, 2]]
in [0, [1, _] | ['one', 'two']]
  'match!'
end
#=> match!

続きは後編で!

前編は以上です。
今回はパターンマッチの概要、case文っぽい使い方、配列やハッシュとのマッチ、変数への代入といったトピックを説明しました。

パターンマッチの残りのトピック(自作クラスをパターンマッチで活用する方法と、パターン名の整理)は後編の記事で紹介していますので、続けてこちらをどうぞ!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away