Pattern Matching no Ruby 3

Inline|Block Expressions

O Pattern matching(ou correspondência de padrões), foi introduzida, de forma experimental, a partir do Ruby 2.7.

Como de costume, no natal do ano de 2021, o time do Ruby o papai Noel nos deu uma nova versão do Ruby 3 que trouxe o pattern matching com algumas melhorias e NÃO mais de forma experimental. 🎉

Pattern Matching no Ruby 3.1

Agora que o pattern matching foi adicionado à linguagem podemos usar sem medo.

Existem basicamente duas formas de usar o pattern matching no Ruby. Expressões inline e em bloco.

Expressões em uma linha(inline expressions)

Para expressões inline, você pode usar o operador => ou in.

Usando em arrays.

Neste exemplo, os valores, "Rex", "Lola", "Sardinha", que estão do lado esquerdo do operador => estão sendo atribuídos às variáveis(first, second e last) do direito do operador.

# Dog list
["Rex", "Lola", "Sardinha"] => [first, second, last]
# => nil

p first
# => "Rex"

p last
# => "Sardinha"

O operador in funciona de maneira similar, com a diferença que retorna true ou false.

[1, 2, 3] in [first, second, last]
# => true

p second
# => 2

p first
# => 1

Além de "atribuições" também é possível fazer comparações. O operador in é ideal pra isso.

[1, "2", 3] in [1, "2", 3]
# => true

["Rex", "Lola", "Sardinha"] in [first, second, last]
# => nil

["Rex", second, 1] in [first, "Lola", 1]
# => true

["Rex", second, 1] in [1, "Lola", first]
# => false

Lembrando que a comparação é feita usando o operador ===, então, embora esteja usando Array, o "Value Pattern" também funcionaria normalmente. Então fazer coisas como: "Rex" in first, naturalmente, também funcionaria.

Também é possível fazer combinação com classes auxiliares.

["Rex", "Lola"] => [String => first, String => last]
# => nil

p first
# => "Rex"

p last
# => "Lola"

Caso não haja correspondência de padrões, uma exceção NoMatchingPatternError será lançada.

["Rex", "Lola"] => [Integer => first, String => last]

# => ["Rex", "Lola"]: Integer === "Rex" does not return true (NoMatchingPatternError)

Também é possível usar o pattern matching com Hashs.

{ name: "Aristóteles" } => { name: }
p name
# => "Aristóteles"

Outro presente do Ruby 3, é que podemos omitir o valor de uma hash, caso esse valor, tenha o mesmo nome da key mas também é possível renomear caso queira.

{ name: "Aristóteles" } => { name: philosopher }
p philosopher
# => "Aristóteles"

Mais açúcar sintático(syntax sugar) é que podemos omitir as chaves {}:

{ name: "Aristóteles" } => name:
p name
# => "Aristóteles"

Trabalhar com Valores aninhados também é possível.

{
  programmer: {
    name: "Aristóteles",
    favorite_languages: ["Ruby", "Elixir", "Clojure", "Rust"]
  }
} in { programmer: { favorite_languages: Array => langs }}

p langs
# => ["Ruby", "Elixir", "Clojure", "Rust"]

Arrays e Hashs também suportam o operador rest *:

["Ruby", "Elixir", "Clojure", "Rust"] => [head, *tail]
p head
# => "Ruby"

p tail
# => ["Elixir", "Clojure", "Rust"]

{ name: "Naruto", age: 17 } in { name: String => name, ** }
# => true

p name
# => "Naruto"

O operador | (Alternative Pattern) que serve como uma espécie de "ou".

{ invoice_number: 1001 } in { invoice_number: Integer | String }
# => true

{ code: ["123"] } in { code: String | Array }
# => true

Dada uma coleção de dados, o Finder Pattern irá procurar pela correspondencia de padrão de forma NÃO posicional dentro do Array.

[
  {
    value: 181.99,
    product: "Polished Ruby Programming",
    date: "2021-12-30T00:22:00.000-00:00"
  },
  {
    value: 19.9,
    product: "Practical Object-Oriented Design",
    date: "2021-12-30T00:22:00.000-00:00"
  },
  {
    value: 19.9,
    product: "Practical Object-Oriented Design",
    date: "2021-12-30T00:22:00.000-00:00"
  }
] in [*, { value: Float, product: "Polished Ruby Programming", date: String } => book, *]
# warning: Find pattern is experimental, and the behavior may change in future versions of Ruby!
# => true

p book
# => {:value=>181.99, :product=>"Polished Ruby Programming", :date=>"2021-12-30T00:22:00.000-00:00"}

Como disse anteriormente, simples, porém poderoso. No entanto, não recomendo usar o Find Pattern nesse momento por se tratar de algo que ainda está em fase experimental. 😉

Expressões em bloco(block expressions)

Agora que aprendemos a sintaxe para expressões inline, vamos estender o conhecimento à instrução case.

case [1, 2]
in [Integer, Integer]
  "Match!"
else
  "Not match :("
end
# => "Match!"

Basicamente, é possível fazer tudo que fizemos como nas expressões inline com a possibilidade de ter mais combinações para casamento de padrão.

case [1, 2, 3, 4, 5]
in Integer => head, *tail
  "Head element #{head} and tail -> #{tail}"
end
# => "Head element 1 and tail -> [2, 3, 4, 5]"

O dry-monds pode tornar o uso do pattern matching muito interessante em controllers, POROs e etc. Por exemplo:

class Auth
  include Dry::Monads[:result]

  def call(params)
    case login(params)
    in Success[:ok, { name: String, token: String => token } => result]
      puts "only token: #{token}"
      result
    in Failure[:user_not_found, String => msg]
      { error: msg }
    in Failure[:password_does_not_match, String => msg]
      { error: msg }
    end
  end

  private

  # ...
  # ...
  # def login
  # login_code_stuff
  # end
end

No próximo post irei falar mais sobre os métodos deconstruct e deconstruct_keys; como estender o pattern matching em objetos não primitivos e um pouco sobre o pin operator ^.

Até a próxima.

Referências

Comentários