Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyでのパターンマッチングの概要

Rubyでのパターンマッチング、その機能、およびコードの可読性の向上にどのように役立つかについての簡単な説明から始めましょう。

数年前の私のような人なら、正規表現のパターンマッチングと混同するかもしれません。他のコンテキストがない「パターンマッチング」をGoogleですばやく検索しても、その定義にかなり近いコンテンツが表示されます。

正式には、パターンマッチングは、データ(文字のシーケンス、一連のトークン、タプルなど)を他のデータと照合するプロセスです。

プログラミングに関しては、言語の機能に応じて、これは次のいずれかを意味する可能性があります。

  1. 予想されるデータ型との照合
  2. 予想されるハッシュ構造(特定のキーの存在など)との照合
  3. 予想される配列の長さとの一致
  4. 一致(またはそれらの一部)をいくつかの変数に割り当てる

パターンマッチングへの私の最初の進出は、Elixirを介したものでした。 Elixirはパターンマッチングをファーストクラスでサポートしているため、 = 実際、演算子は match 単純な割り当てではなく、演算子。

これは、Elixirでは、以下が実際に有効なコードであることを意味します。

iex> x = 1
iex> 1 = x

そのことを念頭に置いて、Ruby 2.7以降の新しいパターンマッチングのサポートと、それを使用してコードを読みやすくする方法を、今日から見ていきましょう。

caseとのルビーパターンマッチング / in

Rubyは、特別な caseを使用したパターンマッチングをサポートしています。 / in 表現。構文は次のとおりです。

case <expression>
in <pattern1>
  # ...
in <pattern2>
  # ...
else
  # ...
end

これをcaseと混同しないでください / when 表現。 when およびin ブランチを単一のcaseに混在させることはできません 。

elseを指定しない場合 式では、一致しない場合は NoMatchingPatternErrorが発生します 。

Rubyのパターンマッチング配列

パターンマッチングを使用して、データ型、長さ、または値に対して、配列を事前に必要な構造に一致させることができます。

たとえば、次のすべてが一致します(の最初ののみに注意してください) caseとして評価されます 最初の試合の世話をやめます):

case [1, 2, "Three"]
in [Integer, Integer, String]
  "matches"
in [1, 2, "Three"]
  "matches"
in [Integer, *]
  "matches" # because * is a spread operator that matches anything
in [a, *]
  "matches" # and the value of the variable a is now 1
end

このタイプのパターンマッチング句は、メソッド呼び出しから複数のシグナルを生成する場合に非常に役立ちます。

Elixirの世界では、これは:okの両方を持つ可能性のある操作を実行するときに頻繁に使用されます 結果と:error たとえば、データベースに挿入された結果。

読みやすくするために使用する方法は次のとおりです。

def create
  case save(model_params)
  in [:ok, model]
    render :json => model
  in [:error, errors]
    render :json => errors
  end
end
 
# Somewhere in your code, e.g. inside a global helper or your model base class (with a different name).
def save(attrs)
  model = Model.new(attrs)
  model.save ? [:ok, model] : [:error, model.errors]
end

Rubyのパターンマッチングオブジェクト

Rubyでオブジェクトを照合して、特定の構造を適用することもできます。

case {a: 1, b: 2}
in {a: Integer}
  "matches" # By default, all object matches are partial
in {a: Integer, **}
  "matches" # and is same as {a: Integer}
in {a: a}
  "matches" # and the value of variable a is now 1
in {a: Integer => a}
  "matches" # and the value of variable a is now 1
in {a: 1, b: b}
  "matches" # and the value of variable b is now 2
in {a: Integer, **nil}
  "does not match" # This will match only if the object has a and no other keys
end

これは、パラメータとの照合に強力なルールを課す場合に最適です。

たとえば、派手なグリーターを書いている場合、次の(強く意見のある)構造になる可能性があります。

def greet(hash = {})
  case hash
  in {greeting: greeting, first_name: first_name, last_name: last_name}
    greet(greeting: greeting, name: "#{first_name} #{last_name}")
  in {greeting: greeting, name: name}
    puts "#{greeting}, #{name}"
  in {name: name}
    greet(greeting: "Hello", name: name)
  in {greeting: greeting}
    greet(greeting: greeting, name: "Anonymous")
  else
    greet(greeting: "Hello", name: "Anonymous")
  end
end
 
greet # Hello, Anonymous
greet(name: "John") # Hello, John
greet(first_name: "John", last_name: "Doe") # Hello, John Doe
greet(greeting: "Bonjour", first_name: "John", last_name: "Doe") # Bonjour, John Doe
greet(greeting: "Bonjour") # Bonjour, Anonymous

Rubyでの変数のバインドと固定

上記の例のいくつかで見たように、パターンマッチングは、パターンの一部を任意の変数に割り当てるのに非常に役立ちます。これは変数バインディングと呼ばれ、変数にバインドする方法はいくつかあります。

  1. タイプが強く一致する場合(例: in [Integer => a] またはin{a:Integer => a}
  2. タイプ指定なし、例: in [a、1、2] または{a:a}
  3. 変数名なし。デフォルトではキー名を使用します。例: in {a:} aという名前の変数を定義します キーaに値があります 。
  4. 残りをバインドします。例: in [Integer、* rest] またはin{a:Integer、** rest}

では、既存の変数をサブパターンとして使用する場合、どのように一致させることができますか?これは、変数 pinningを使用できる場合です。 ^で (ピン)演算子:

a = 1
case {a: 1, b: 2}
in {a: ^a}
  "matches"
end

変数がパターン自体で定義されている場合でもこれを使用できるため、次のような強力なパターンを記述できます。

case order
in {billing_address: {city:}, shipping_address: {city: ^city}}
  puts "both billing and shipping are to the same city"
else
  raise "both billing and shipping must be to the same city"
end

変数のバインドで言及する重要な癖の1つは、パターンが完全に一致していなくても、変数がバインドされたままになることです。これは便利な場合があります。

ただし、ほとんどの場合、これは微妙なバグの原因にもなる可能性があります。したがって、試合内で使用されたシャドウ変数の値に依存しないようにしてください。たとえば、次のように、都市は「アムステルダム」になりますが、代わりに「ベルリン」になります:

city = "Amsterdam"
order = {billing_address: {city: "Berlin"}, shipping_address: {city: "Zurich"}}
case order
in {billing_address: {city:}, shipping_address: {city: ^city}}
  puts "both billing and shipping are to the same city"
else
  puts "both billing and shipping must be to the same city"
end
puts city # Berlin instead of Amsterdam

Rubyのカスタムクラスのマッチング

いくつかの特別なメソッドを実装して、Rubyでカスタムクラスのパターンマッチングを認識させることができます。

たとえば、ユーザーを first_nameとパターンマッチングするには およびlast_name deconstruct_keysを定義できます クラスで:

class User
  def deconstruct_keys(keys)
    {first_name: first_name, last_name: last_name}
  end
end
 
case user
in {first_name: "John"}
  puts "Hey, John"
end

キー deconstruct_keysへの引数 パターンで要求されたキーが含まれています。これは、すべてのキーの計算にコストがかかる場合に、受信者が必要なキーのみを提供する方法です。

deconstruct_keysと同じように 、 deconstructの実装を提供できます オブジェクトを配列としてパターンマッチングできるようにします。たとえば、 Locationがあるとします。 緯度と経度を持つクラス。 deconstruct_keysの使用に加えて 緯度と経度のキーを提供するために、 [latitude、longitude]の形式で配列を公開できます。 同様に:

class Location
  def deconstruct
    [latitude, longitude]
  end
end
 
case location
in [Float => latitude, Float => longitude]
  puts "#{latitude}, #{longitude}"
end

複雑なパターンにガードを使用する

通常のパターン一致演算子では表現できない複雑なパターンがある場合は、 ifを使用することもできます。 (またはただし )試合のガードを提供するステートメント:

case [1, 2]
in [a, b] if b == a * 2
  "matches"
else
  "no match"
end

=>とのパターンマッチング / in caseなし

Ruby 3以降を使用している場合は、さらに多くのパターンマッチングの魔法にアクセスできます。 Ruby 3以降、パターンマッチングはcaseステートメントなしで1行で実行できます:

[1, 2, "Three"] => [Integer => one, two, String => three]
puts one # 1
puts two # 2
puts three # Three
 
# Same as above
[1, 2, "Three"] in [Integer => one, two, String => three]

上記の構文にelseがない場合 句、データ構造が事前にわかっている場合に最も役立ちます。

例として、このパターンは、管理者ユーザーのみを許可するベースコントローラー内にうまく適合する可能性があります。

class AdminController < AuthenticatedController
  before_action :verify_admin
 
  private
 
  def verify_admin
    Current.user => {role: :admin}
  rescue NoMatchingPatternError
    raise NotAllowedError
  end
end

Rubyでのパターンマッチング:このスペースを見る

最初は、パターンマッチングを把握するのが少し奇妙に感じることがあります。一部の人にとっては、栄光のオブジェクト/配列の脱構築のように感じるかもしれません。

しかし、Elixirの人気が何らかの兆候であるとすれば、パターンマッチングはあなたの武器庫にある素晴らしいツールです。Elixirでそれを直接使用した経験があるので、一度慣れないと生きていくのは難しいことを確認できます。

Ruby 2.7を使用している場合は、パターンマッチング( case を使用) / in )はまだ実験的です。 Ruby 3では、 case / in 新しく導入された単一行のパターンマッチング式は実験的なものですが、は安定版に移行しました。警告は Warning [:experimental] =falseでオフにできます。 コードまたは-W:no-experimental コマンドラインキー。

Rubyでのパターンマッチングはまだ初期段階ですが、この紹介がお役に立てば幸いです。また、今後の開発について私と同じように興奮していることを願っています。

P.S。 Ruby Magicの投稿が報道されたらすぐに読みたい場合は、Ruby Magicニュースレターを購読して、投稿を1つも見逃さないでください。


  1. Ruby2.6の9つの新機能

    Rubyの新しいバージョンには、新しい機能とパフォーマンスの改善が含まれています。 変更についていきますか? 見てみましょう! 無限の範囲 Ruby 2.5以前のバージョンは、すでに1つの形式の無限範囲をサポートしています( Float ::INFINITY を使用) )、しかしRuby2.6はこれを次のレベルに引き上げます。 新しい無限の範囲 次のようになります: (1..) これは、(1..10)のような終了値がないため、通常の範囲とは異なります。 。 使用例 : [a, b, c].zip(1..) # [[a, 1], [b, 2], [c, 3]] [1,2,3,

  2. Rubyのデコレータデザインパターン

    デコレータのデザインパターンは何ですか? そして、Rubyプロジェクトでこのパターンをどのように使用できますか? デコレータデザインパターンは、新機能を追加することでオブジェクトを強化するのに役立ちます クラスを変更せずにそれに。 例を見てみましょう! ロギングとパフォーマンス この例では、rest-clientのようなgemを使用してHTTPリクエストを作成しています。 次のようになります: require restclient data = RestClient.get(www.rubyguides.com) 今 : 一部のリクエストにログを追加したいが、RestCli