O Pattern Matching chegou no Ruby 2.7 para lidar com estruturas de dados de forma mais expressiva. Eu passei a usá-lo também para outra coisa: checar tipos em tempo de execução.
Comentei sobre isso no Mastodon, a galera interagiu bem, e resolvi juntar aqui o que tenho usado.
Patterning Matching is beautiful!
class Person def initialize(name:, age:) name => ::String age => ::Numeric @name = name @age = age end end
Um aquecimento
Antes dos tipos, um exemplo que eu gosto de mostrar. O case/in compara um valor com padrões, e o | aceita mais de um padrão no mesmo ramo:
DAYS_OF_WEEK = %w[
monday tuesday wednesday thursday friday saturday sunday
].freeze
FridayImInLove = ->(day) do
case day
in "monday" then puts "I don't care if Monday's blue"
in "tuesday" | "wednesday" then puts "Tuesday's grey and Wednesday too"
in "thursday" then puts "Thursday, I don't care about you"
in "friday" then puts "It's Friday, I'm in love"
in "saturday" then puts "Saturday, wait"
in "sunday" then puts "And Sunday always comes too late"
end
end
DAYS_OF_WEEK.each(&FridayImInLove)
Guarde esse |. Ele vai voltar.
O padrão também pode ser uma classe. Uma classe responde ao === dizendo se o valor é uma instância dela, e é isso que o Pattern Matching usa por baixo. Daí sai a checagem de tipos.
Duas formas: => e in
Com =>, o valor precisa casar com o padrão. Se não casar, o Ruby levanta NoMatchingPatternError:
def sum(a, b)
a => Integer
b => Integer
a + b
end
sum(1, 2)
# => 3
sum("1", 2)
# => "1": Integer === "1" does not return true (NoMatchingPatternError)
Com in, a pergunta vira um booleano, sem exceção:
"texto" in String
# => true
42 in String
# => false
Use o => quando um tipo errado for um bug que deve parar o programa, e o in quando você quer decidir o que fazer:
def check_string(value)
if value in String
"É uma string válida!"
else
"Tipo inválido, esperava uma string."
end
end
Uma pegadinha: ok = value in String guarda value em ok, não true, porque a atribuição acontece antes do in. Use parênteses: ok = (value in String).
Mais de um tipo
Lembra do |? Com ele o padrão aceita qualquer um dos tipos:
def sum(a, b)
a => Integer | Float
b => Integer | Float
a + b
end
sum(2, 2.5)
# => 4.5
Tipos seus
Qualquer objeto que responda ao === serve de padrão, e uma lambda responde. Então dá para criar as suas próprias regras:
PositiveNumber = ->(n) { n.is_a?(Numeric) && n.positive? }
def sum(a, b)
a => PositiveNumber
b => PositiveNumber
a + b
end
sum(2, 3)
# => 5
sum(-2, 3)
# => -2: #<Proc:0x... (lambda)> === -2 does not return true
# (NoMatchingPatternError)
Isso vale para o retorno também. Às vezes é ali que mora a garantia que importa:
def sum(a, b)
a => Integer | Float
b => Integer | Float
result = a + b
result => PositiveNumber
result
end
sum(-1, 3)
# => 2
sum(-3, 2)
# => -1: #<Proc:0x... (lambda)> === -1 does not return true
# (NoMatchingPatternError)
Quando não usar
Não é para isso que o Pattern Matching foi criado, e não é de graça. Para uma checagem simples, value.is_a?(String) é mais rápido e todo mundo entende. Para validar dados em aplicações críticas, existem gems feitas para isso.
Onde eu acho que vale: na fronteira do código, onde um tipo errado causaria um erro confuso mais adiante. Uma linha como a => Integer | Float documenta e garante ao mesmo tempo.