Operador de Igualdade de Caso no Ruby

Neil Carvalho
Traduzido por Neil Carvalho

Dê uma olhada em uma expressão case em Ruby:

case x
when 1, 2
  "É 1 ou 2"
when 3..10
  "É um número de 3 a 10"
when BigDecimal
  "É uma instância de BigDecimal"
when /bot/
  "É uma string que corresponde à expressão regular /bot/"
end

Comparadas a muitas linguagens que possuem switch statements, as expressões case do Ruby são muito mais flexíveis, correspondendo não apenas em casos de estrita igualdade, mas também em uma espécie de filiação ou correspondência: 2 é um membro do intervalo 1..3; 5 é um membro da classe Integer; "thoughtbot" corresponde à expressão regular /bot/.

Este poder vem do operador === (ou operador de igualdade de caso, três iguais, igual triplo, como preferir). Cada argumento passado para when é comparado com x usando esse operador. O Ruby interpretará a expressão case acima como se fosse:

if 1 === x || 2 === x
  "É 1 ou 2"
elsif 3..10 === x
  "É um número de 3 a 10"
elsif BigDecimal === x
  "É uma instância de BigDecimal"
elsif /bot/ === x
  "É uma string que corresponde à expressão regular /bot/"
end

A classe Object, herdada pela maioria das classes Ruby, implementa o método === como um alias para ==. A documentação para Object descreve-o como:

Igualdade de caso – Para a classe Object, efetivamente o mesmo que chamar #==, mas normalmente sobrescrito por descendentes para fornecer semântica significativa em expressões case.

Como esses descendentes sobrescrevem o método ===?`

Module

A classe Module, herdada pela própria classe Class, implementa === como uma verificação is_a? no objeto fornecido. Ele retornará true se a classe ou módulo estiver na árvore ancestral do objeto fornecido.

Enumerable === [1, 2, 3]   # => true
Array === "A string"       # => false
String === "A string"      # => true

Range

Intervalos implementam === com o mesmo comportamento que cover?.

(1..10) === 3  # => true
(5..20) === 2  # => false
(5..20) === 15 # => true
(1..10) === 3.5 # => true
(5..20) === 2   # => false

Set

Um conjunto implementa === verificando se o objeto fornecido é um membro desse conjunto. Efetivamente, o mesmo que include?.

Set[1, 2, 3] === 2    # => true
Set[1, 2, 3] === "😀" # => false

Regexp

Objetos Regexp retornarão true em === quando, dada uma string, a expressão regular corresponder à string. É próximo à implementação de match?.

/bot/ === "thoughtbot"                                 # => true
URI::MailTo::EMAIL_REGEXP === "https://thoughtbot.com" # => false

IPAddr

Objetos IPAddr respondem a === como um equivalente a include?. Eles retornarão true se o IP fornecido for igual ou estiver dentro do intervalo.

subnet = IPAddr.new("192.168.2.0/24")
ip = IPAddr.new("192.168.2.100")

subnet.include?(ip) # => true
subnet === ip       # => true

Proc

Objetos Proc, incluindo lambdas, têm maneiras demais de serem chamados, e === é mais uma delas.

Prime = ->(n) { n >= 2 && (2..Math.sqrt(n)).none? { |i| n % i == 0 } }

Prime === 3 # => true
Prime === 4 # => false # 4 não é um número primo

Qualquer classe que você escrever

Agora que você sabe como as expressões case funcionam nos bastidores, você pode implementar o operador de igualdade de caso em suas próprias classes, fazendo correspondências de maneiras significativas, como por exemplo se um ponto GPS está dentro de uma área:

class Map::Area
  # ...

  def ===(other)
    if other.respond_to?(:latitude) && other.respond_to?(:longitude)
      # A coordenada fornecida está dentro desta área?
    else
      other == self
    end
  end
end

estado = Map::Area.new(lista_de_coordenadas_gps_para_desenhar_a_area)
cidade = Map::Point.new(-9.648139, -35.717239)

estado === cidade # => true

Além das expressões case

Você pode estar se perguntando que, como as expressões case são um code smell que seu código tem conhecimento demais, o método === não é tão útil. Mas há usos do operador de igualdade de caso além de seu propósito inicial.

O módulo Enumerable implementa muitos métodos que o usam. Um exemplo interessante é #grep. Ele retornará os elementos enumeráveis que correspondem ao padrão fornecido usando o método ===, então:

[1, "a", 3].grep(Integer)       # => [1, 3]
["foo", "bar", "baz"].grep(/a/) # => ["bar", "baz"]
(1..10).grep(3..8)              # => [3, 4, 5, 6, 7, 8]
(1..15).grep(Prime)             # => [2, 3, 5, 7, 11, 13]
lugares.grep(area)              # => [local_1, local_3, ...]

Outros métodos de Enumerable que usam o operador de igualdade de caso são: #all?, #none?, #one?, #any?, #grep_v, #slice_after e #slice_before.