可能性の配列:Rubyパターンマッチングのガイド

公開: 2022-03-11

パターンマッチングは、Ruby2.7に登場する大きな新機能です。 トランクにコミットされているので、興味のある人は誰でもRuby2.7.0-devをインストールしてチェックアウトできます。 これらはいずれも確定されておらず、開発チームはフィードバックを求めているため、フィードバックがある場合は、機能が実際にリリースされる前にコミッターに知らせることができます。

この記事を読んだ後、パターンマッチングとは何か、Rubyでの使用方法を理解していただければ幸いです。

パターンマッチングとは何ですか?

パターンマッチングは、関数型プログラミング言語で一般的に見られる機能です。 Scalaのドキュメントによると、パターンマッチングは、「パターンに対して値をチェックするためのメカニズムです。 一致が成功すると、値を構成要素に分解することもできます。」

これは、正規表現、文字列照合、またはパターン認識と混同しないでください。 パターンマッチングは文字列とは関係ありませんが、データ構造とは関係ありません。 パターンマッチングに初めて遭遇したのは、2年ほど前にElixirを試したときでした。 私はElixirを学び、それを使ってアルゴリズムを解こうとしていました。 私は自分のソリューションを他のソリューションと比較し、パターンマッチングを使用していることに気付きました。これにより、コードがはるかに簡潔で読みやすくなりました。

そのため、パターンマッチングは本当に印象的でした。 Elixirのパターンマッチングは次のようになります。

 [a, b, c] = [:hello, "world", 42] a #=> :hello b #=> "world" c #=> 42

上記の例は、Rubyでの複数の割り当てに非常によく似ています。 しかし、それだけではありません。 また、値が一致するかどうかもチェックします。

 [a, b, 42] = [:hello, "world", 42] a #=> :hello b #=> "world"

上記の例では、左側の番号42は割り当てられている変数ではありません。 その特定のインデックスの同じ要素が右側の要素と一致することを確認する値です。

 [a, b, 88] = [:hello, "world", 42] ** (MatchError) no match of right hand side value

この例では、値が割り当てられる代わりに、 MatchErrorが発生します。 これは、88番が42番と一致しないためです。

また、マップ(Rubyのハッシュに似ています)でも機能します。

 %{"name": "Zote", "title": title } = %{"name": "Zote", "title": "The mighty"} title #=> The mighty

上記の例では、キーnameの値がZoteであることを確認し、キーtitleの値を変数titleにバインドします。

この概念は、データ構造が複雑な場合に非常にうまく機能します。 変数を割り当てて、値またはタイプをすべて1行で確認できます。

さらに、Elixirのような動的型付け言語でメソッドのオーバーロードを行うこともできます。

 def process(%{"animal" => animal}) do IO.puts("The animal is: #{animal}") end def process(%{"plant" => plant}) do IO.puts("The plant is: #{plant}") end def process(%{"person" => person}) do IO.puts("The person is: #{person}") end

引数のハッシュのキーに応じて、さまざまなメソッドが実行されます。

うまくいけば、それはパターンマッチングがいかに強力であるかを示しています。 noaidi、qo、egison-rubyなどのgemを使用してパターンマッチングをRubyに組み込む試みは数多くあります。

Ruby 2.7にも、これらのgemとそれほど変わらない独自の実装があり、これが現在行われている方法です。

Rubyパターンマッチング構文

Rubyでのパターンマッチングは、 caseステートメントを介して行われます。 ただし、通常のwhenを使用する代わりに、キーワードinが代わりに使用されます。 また、 ifまたはunlessステートメントの使用もサポートしています。

 case [variable or expression] in [pattern] ... in [pattern] if [expression] ... else ... end

Caseステートメントは変数または式を受け入れることができ、これはin句で提供されるパターンと照合されます。 パターンの後にステートメントを指定することもできます。 ここでの等価性チェックでも、通常のcaseステートメントと同様に===を使用します。 これは、クラスのサブセットとインスタンスを一致させることができることを意味します。 使用方法の例を次に示します。

一致する配列

translation = ['th', 'เต้', 'ja', 'テイ'] case translation in ['th', orig_text, 'en', trans_text] puts "English translation: #{orig_text} => #{trans_text}" in ['th', orig_text, 'ja', trans_text] # this will get executed puts "Japanese translation: #{orig_text} => #{trans_text}" end

上記の例では、変数のtranslationは2つのパターンと照合されます。

['th', orig_text, 'en', trans_text]および['th', orig_text, 'ja', trans_text] 。 パターンの値が各インデックスのtranslation変数の値と一致するかどうかを確認します。 値が一致する場合は、 translation変数の値を各インデックスのパターンの変数に割り当てます。

Rubyパターンマッチングアニメーション:配列のマッチング

マッチングハッシュ

translation = {orig_lang: 'th', trans_lang: 'en', orig_txt: 'เต้', trans_txt: 'tae' } case translation in {orig_lang: 'th', trans_lang: 'en', orig_txt: orig_txt, trans_txt: trans_txt} puts "#{orig_txt} => #{trans_txt}" end

上記の例では、 translation変数はハッシュになっています。 in句の別のハッシュと照合されます。 何が起こるかというと、caseステートメントは、パターン内のすべてのキーがtranslation変数内のキーと一致するかどうかをチェックします。 また、各キーのすべての値が一致することも確認します。 次に、ハッシュ内の変数に値を割り当てます。

Rubyパターンマッチングアニメーション:配列のマッチング

一致するサブセット

パターンマッチングで使用される品質チェックは、 ===のロジックに従います。

複数のパターン

  • | 1つのブロックに複数のパターンを定義するために使用できます。
 translation = ['th', 'เต้', 'ja', 'テイ'] case array in {orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt} | ['th', orig_text, 'ja', trans_text] puts orig_text #=> เต้ puts trans_text #=> テイend

上記の例では、 translation変数は{orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt}ハッシュと['th', orig_text, 'ja', trans_text]配列。

これは、同じものを表すわずかに異なるタイプのデータ構造があり、両方のデータ構造で同じコードブロックを実行する場合に役立ちます。

矢印の割り当て

この場合、 =>を使用して、一致した値を変数に割り当てることができます。

 case ['I am a string', 10] in [Integer, Integer] => a # not reached in [String, Integer] => b puts b #=> ['I am a string', 10] end

これは、データ構造内の値をチェックするだけでなく、これらの値を変数にバインドする場合に役立ちます。

ピン演算子

ここで、ピン演算子は変数が再割り当てされるのを防ぎます。

 case [1,2,2] in [a,a,a] puts a #=> 2 end

上記の例では、パターン内の変数aが1、2、2の順に一致します。1、2、2の順に割り当てられます。これは、すべての配列の値は同じです。

 case [1,2,2] in [a,^a,^a] # not reached in [a,b,^b] puts a #=> 1 puts b #=> 2 end

ピン演算子を使用すると、変数を再割り当てするのではなく、変数を評価します。 上記の例では、最初のインデックスではaが1に割り当てられているため、[1,2,2]は[a、^ a、^ a]と一致しません。2番目と3番目では、aは1と評価されます。しかし、2と一致します。

ただし、[a、b、^ b]は[1,2,2]と一致します。これは、aが最初のインデックスで1に割り当てられ、bが2番目のインデックスで2に割り当てられ、^ b(現在は2)が次のように一致するためです。 3番目のインデックスの2なので、合格します。

 a = 1 case [2,2] in [^a,^a] #=> not reached in [b,^b] puts b #=> 2 end

上記の例に示すように、caseステートメントの外部からの変数も使用できます。

アンダースコア( _ )演算子

アンダースコア( _ )は、値を無視するために使用されます。 いくつかの例でそれを見てみましょう:

 case ['this will be ignored',2] in [_,a] puts a #=> 2 end
 case ['a',2] in [_,a] => b puts a #=> 2 Puts b #=> ['a',2] end

上記の2つの例では、 _と一致するすべての値が渡されます。 2番目のcaseステートメントでは、 =>演算子は無視された値もキャプチャします。

Rubyでのパターンマッチングのユースケース

次のJSONデータがあるとします。

 { nickName: 'Tae' realName: {firstName: 'Noppakun', lastName: 'Wongsrinoppakun'} username: 'tae8838' }

Rubyプロジェクトで、このデータを解析し、次の条件で名前を表示する必要があります。

  1. ユーザー名が存在する場合は、ユーザー名を返します。
  2. ニックネーム、名、および姓が存在する場合は、ニックネーム、名、および姓を返します。
  3. ニックネームが存在しないが、名前と名前が存在する場合は、名前を返し、次に名前を返します。
  4. いずれの条件も当てはまらない場合は、「新規ユーザー」を返します。

これが私が今Rubyでこのプログラムを書く方法です:

 def display_name(name_hash) if name_hash[:username] name_hash[:username] elsif name_hash[:nickname] && name_hash[:realname] && name_hash[:realname][:first] && name_hash[:realname][:last] "#{name_hash[:nickname]} #{name_hash[:realname][:first]} #{name_hash[:realname][:last]}" elsif name_hash[:first] && name_hash[:last] "#{name_hash[:first]} #{name_hash[:last]}" else 'New User' end end

それでは、パターンマッチングでどのように見えるかを見てみましょう。

 def display_name(name_hash) case name_hash in {username: username} username in {nickname: nickname, realname: {first: first, last: last}} "#{nickname} #{first} #{last}" in {first: first, last: last} "#{first} #{last}" else 'New User' end end

構文の好みは少し主観的かもしれませんが、私はパターンマッチングバージョンを好みます。 これは、パターンマッチングにより、ハッシュの値を記述してチェックする代わりに、期待するハッシュを書き出すことができるためです。 これにより、予想されるデータを簡単に視覚化できます。

 `{nickname: nickname, realname: {first: first, last: last}}`

それ以外の:

 `name_hash[:nickname] && name_hash[:realname] && name_hash[:realname][:first] && name_hash[:realname][:last]`.

DeconstructとDeconstruct_keys

Ruby 2.7では、 deconstructdeconstruct_keysという2つの新しい特別なメソッドが導入されています。 クラスのインスタンスが配列またはハッシュと照合されると、それぞれdeconstructまたはdeconstruct_keysが呼び出されます。

これらのメソッドの結果は、パターンとの照合に使用されます。 次に例を示します。

 class Coordinate attr_accessor :x, :y def initialize(x, y) @x = x @y = y end def deconstruct [@x, @y] end def deconstruct_key {x: @x, y: @y} end end

このコードは、 Coordinateというクラスを定義しています。 属性としてxとyがあります。 また、 deconstructメソッドとdeconstruct_keysメソッドが定義されています。

 c = Coordinates.new(32,50) case c in [a,b] pa #=> 32 pb #=> 50 end

ここでは、 Coordinateのインスタンスが定義され、配列に対してパターンが照合されています。 ここで何が起こるかというと、 Coordinate#deconstructが呼び出され、その結果がパターンで定義された配列[a,b]と照合するために使用されます。

 case c in {x:, y:} px #=> 32 py #=> 50 end

この例では、 Coordinateの同じインスタンスがハッシュに対してパターンマッチングされています。 この場合、 Coordinate#deconstruct_keysの結果は、パターンで定義されたハッシュ{x: x, y: y}と照合するために使用されます。

エキサイティングな実験的特徴

Elixirで最初にパターンマッチングを経験したので、この機能にはメソッドのオーバーロードが含まれ、1行だけを必要とする構文で実装される可能性があると思いました。 ただし、Rubyはパターンマッチングを念頭に置いて構築された言語ではないため、これは理解できます。

caseステートメントを使用することは、おそらくこれを実装するための非常に無駄のない方法であり、既存のコードにも影響しません( deconstructおよびdeconstruct_keysメソッドを除く)。 caseステートメントの使用法は、実際にはScalaのパターンマッチングの実装と似ています。

個人的には、パターンマッチングはRuby開発者にとってエキサイティングな新機能だと思います。 コードをよりクリーンにし、Rubyをもう少しモダンでエキサイティングなものにする可能性があります。 私は人々がこれをどのように作っているのか、そしてこの機能が将来どのように進化するのかを見たいと思っています。