Rubyにおけるパターンマッチング入門

22542 ワード

Rubyでのパターンマッチングに関する簡単な議論から始めましょう.
あなたが数年前私のような何かであるならば、あなたはRegexでパターンマッチングでそれを混同するかもしれません.他のどの文脈も「パターンマッチング」の速いGoogle検索でさえ、あなたにその定義にかなり近い内容をもたらします.
正式に、パターンマッチングは他のデータに対してどんなデータ(文字列、一連のトークン、タプル、または他の何か)であるかをチェックするプロセスです.
プログラミングに関しては、言語の能力に応じて、以下のいずれかを意味します.
  • 予想されるデータ型に対するマッチング
  • 予想されるハッシュ構造に対するマッチング
  • 予想される配列長に対するマッチング
  • いくつかの変数にマッチ(またはその一部)を割り当てる
  • パターンマッチングへの私の最初の進出は、エリクシールを通してでした.エリクシールfirst class support パターンマッチングのために= 演算子は、実際にはmatch 演算子.単純な代入ではなく.
    つまり、エリクシールでは以下のコードが有効です.
    iex> x = 1
    iex> 1 = x
    
    これを念頭に置いて、Ruby 2.7 +の新しいパターンマッチングサポートを見てみましょう.また、今日からスタートして、コードをより読みやすくするために、どのように使うことができますか.

    Rubyパターンマッチングcase/inRuby特別なパターンマッチングをサポートcase/in エクスプレッション.構文は以下の通りです:
    case <expression>
    in <pattern1>
      # ...
    in <pattern2>
      # ...
    else
      # ...
    end
    
    これは、case/when エクスプレッション.when and in 枝は1つで混合できないcase .
    あなたが提供しないならばelse 式が失敗した場合、NoMatchingPatternError .

    ルビーにおけるパターンマッチングアレイ
    パターンマッチングを使用して、データ型、長さまたは値に対して事前に必要な構造に配列を一致させることができます.
    たとえば、次のすべてが一致するin を評価する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
    
    この型のパターンマッチング句は、メソッドコールから複数のシグナルを生成したい場合に非常に便利です.
    エリクサーの世界では、: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のオブジェクトにマッチして特定の構造を強制することもできます.
    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
    
    これは素晴らしい任意のparamsに対するマッチングのための強力なルールを課すときに動作します.
    たとえば、空想的なグリッターを書いているならば、以下の(強く議論された)構造を持つことができます.
    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
    

    ルビーにおける変数結合とピン止め
    上記の例のいくつかに見られるように、パターンマッチングは、パターンの一部を任意の変数に割り当てる際に非常に有用です.
    これは変数結合と呼ばれ、変数にバインドできるいくつかの方法があります.
  • 強力な型マッチで、例えば.in [Integer => a] or in {a: Integer => a}
  • 型指定なしで、例えば.in [a, 1, 2] or in {a: a} .
  • 変数名がなければ、キー名を使用します.in {a:} 変数名a で値をキーa .
  • バインド残り.in [Integer, *rest] or in {a: Integer, **rest} .
  • どのようにして、既存の変数をサブパターンとして使用したいときに、我々はマッチすることができますか?これは、^ ( pin )演算子:
    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つの重要なquirkは、パターンが完全にマッチしないとしても、変数がまだ縛られたということです.
    これは時々有用です.
    しかし、ほとんどの場合、これは微妙なバグの原因である可能性がありますので、マッチの中で使用されている影付き変数値に依存しないことを確認してください.
    たとえば、次のように、あなたは都市が「アムステルダム」であると思っています、しかし、それは代わりに「ベルリン」であるでしょう
    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 and 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
    
    The keys 引数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 (or unless ) 文にマッチするガードを提供する
    case [1, 2]
    in [a, b] if b == a * 2
      "matches"
    else
      "no match"
    end
    

    パターンマッチング=>/in なしでcaseルビー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
    

    ルビーでのパターンマッチング
    まず,パターンマッチングは,把握するのに少し奇妙な感じがする.
    いくつかの場合、それはglorifiedされたオブジェクト/配列deconstructionのように感じるかもしれません.
    しかし、エリクサーの人気が任意の指示である場合は、パターンマッチングはあなたの武器で持っている素晴らしいツールです.
    Elixirでそれを使用して最初の手の経験を持って、私はそれが慣れて一度はそれが生活をするのは難しいことを確認することができます.
    Ruby 2.7では、case/in ) はまだ実験的です.Ruby 3ではcase/in 新しく導入された単一ラインパターンマッチング式が実験的である間、安定して移動しました.
    警告は無効にすることができますWarning[:experimental] = false コードまたは-W:no-experimental コマンドラインキー.
    Rubyでのパターンマッチングはまだ初期段階にありますが、私はあなたがこの導入に有用であることを発見し、私は将来の開発については興奮していることを願って!
    P . S .あなたが彼らがプレスから降りるとすぐに、ルビー魔法のポストを読みたいならば.subscribe to our Ruby Magic newsletter and never miss a single post !
    私たちのゲストの著者Pulkitシニアシニアスタックエンジニアとコンサルタントです.彼の自由な時間に、彼は彼の経験について書きますhis blog .