パターンマッチングは、Ruby2.7に登場する大きな新機能です。トランクにコミットされているので、興味のある人は誰でもインストールできます Ruby 2.7.0-dev そしてそれをチェックしてください。これらはいずれも確定しておらず、開発チームはフィードバックを求めているため、フィードバックがある場合は、機能が実際にリリースされる前にコミッターに知らせることができます。
この記事を読んだ後、パターンマッチングとは何か、Rubyでの使用方法を理解していただければ幸いです。
パターンマッチングは、関数型プログラミング言語で一般的に見られる機能です。による ドキュメントの規模 、パターンマッチングは 「パターンに対して値をチェックするためのメカニズム。一致が成功すると、値を構成要素に分解することもできます。」
これを正規表現、文字列照合、またはパターン認識と混同しないでください。パターンマッチングは文字列とは関係ありませんが、データ構造とは関係ありません。パターンマッチングに初めて遭遇したのは、2年ほど前に試したときでした。 エリクサー 。私は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
変数のタイトルに。
この概念は、データ構造が複雑な場合に非常にうまく機能します。変数を割り当てて、値またはタイプをすべて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
引数のハッシュのキーに応じて、さまざまなメソッドが実行されます。
うまくいけば、それはパターンマッチングがいかに強力であるかを示しています。次のようなgemを使用してパターンマッチングをRubyに組み込む試みは数多くあります。 ノアイデ 、 何 、および egison-ruby 。
Ruby 2.7にも、これらのgemとそれほど変わらない独自の実装があり、これが現在行われている方法です。
Rubyでのパターンマッチングは、case
を介して行われます。ステートメント。ただし、通常のwhen
を使用する代わりに、キーワードin
代わりに使用されます。 if
の使用もサポートしますまたはunless
ステートメント:
case [variable or expression] in [pattern] ... in [pattern] if [expression] ... else ... end
Caseステートメントは変数または式を受け入れることができ、これはで提供されるパターンと照合されます。 に 句。 場合 または そうでなければ パターンの後にステートメントを指定することもできます。ここでの等価性チェックでも===
を使用します通常の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
の値を割り当てます。各インデックスのパターン内の変数への変数。
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
内のキーと一致するかどうかをチェックします。変数。また、各キーのすべての値が一致することも確認します。次に、ハッシュ内の変数に値を割り当てます。
パターンマッチングで使用される品質チェックは、===
のロジックに従います。
|
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ステートメントでは、=>
演算子は、無視された値もキャプチャします。
次のJSONデータがあるとします。
{ nickName: 'Tae' realName: {firstName: 'Noppakun', lastName: 'Wongsrinoppakun'} username: 'tae8838' }
Rubyプロジェクトで、このデータを解析し、次の条件で名前を表示する必要があります。
これが私が今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]`.
Ruby2.7で導入された2つの新しい特別なメソッドがあります:deconstruct
およびdeconstruct_keys
。クラスのインスタンスが配列またはハッシュと照合されている場合、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] p a #=> 32 p b #=> 50 end
ここでは、Coordinate
のインスタンスが定義され、配列に対してパターンが照合されています。ここで何が起こるかというとCoordinate#deconstruct
が呼び出され、その結果を使用して配列と照合します[a,b]
パターンで定義されています。
case c in {x:, y:} p x #=> 32 p y #=> 50 end
この例では、Coordinate
の同じインスタンスハッシュに対してパターンマッチングされています。この場合、Coordinate#deconstruct_keys
結果はハッシュとの照合に使用されます{x: x, y: y}
パターンで定義されています。
Elixirで最初にパターンマッチングを経験したとき、この機能にはメソッドのオーバーロードが含まれ、1行しか必要としない構文で実装される可能性があると思いました。ただし、Rubyはパターンマッチングを念頭に置いて構築された言語ではないため、これは理解できます。
caseステートメントを使用することは、おそらくこれを実装するための非常に無駄のない方法であり、既存のコードにも影響しません(deconstruct
およびdeconstruct_keys
メソッドを除く)。 caseステートメントの使用法は、実際にはScalaのパターンマッチングの実装と似ています。
個人的には、パターンマッチングはエキサイティングな新機能だと思います Ruby開発者 。コードをよりクリーンにし、Rubyをもう少しモダンでエキサイティングなものにする可能性があります。人々がこれをどうやって作っているのか、そしてこの機能が将来どのように進化するのかを見てみたいです。
Scalaのドキュメントによると、パターンマッチングは「パターンに対して値をチェックするためのメカニズムです。一致が成功すると、値を構成要素に分解することもできます。」
コードをよりクリーンにし、Rubyをもう少しモダンでエキサイティングなものにする可能性があります。
Ruby2.7のswitchcaseステートメントを使用して、通常の「when」の代わりに「in」というキーワードを使用します。
Scalaのドキュメントによると、パターンマッチングは「パターンに対して値をチェックするためのメカニズムです。一致が成功すると、値を構成要素に分解することもできます。」