Ruby

Reimplementando o operador de igualdade (==) no Ruby

15 Jul 2022

Numa postagem anterior falei sobre como o Ruby implementa o nil de forma correta. Nesta vou falar sobre o operador de igualdade (==) (não sei se essa é a tradução correta, caso tenha outra melhor, me avise) que o Ruby também implementa de forma muito correta. No entanto, nem sempre queremos que ele tenha o comportamento padrão, e aí chega a hora de reimplementar o operador nos nossos próprios termos.

Esse foi um conhecimento que adquiri realizando o teste da Husky, e resolvi compartilhar a solução em português pois me aprofundei bastante no assunto e atualmente a melhor postagem é uma do blog do Shopify, apenas em inglês. Eles lidam com dois casos: um para reimplementar o operador em entidades (models do Ruby on Rails, por exemplo) e outra para reimplementar em objetos de valor (código Ruby puro, por exemplo). Vou abordar nesta postagem apenas os objetos de valor.

O problema

Vou assumir que você já sabe a função básica do ==. Usamos a todo momento quando queremos, por exemplo, saber se dois Livros tem a mesma editora, usando arte_da_guerra.editora == harry_potter.editora.

Mas digamos que você queira comparar dois objetos de uma mesma classe, os dois com os mesmos argumentos. Você pode querer que eles sejam considerados iguais, não é?

carlos = Funcionario.new('Carlos', 'Marketing')
carlos_dois = Funcionario.new('Carlos', 'Marketing')

carlos == carlos # => retorna true
carlos == carlos_dois # => retorna false???

Isso acontece porque o Ruby vai dizer que dois objetos são iguais apenas nos casos em que eles são os mesmos objetos. Mas eu quero que ele considere que o carlos e o carlos_dois são iguais, até para eu poder evitar uma duplicação indesejada no futuro.

O Ruby não nos retorna true para essa comparação pois a implementação padrão vai buscar apenas se os dois objetos tem o mesmo id.

carlos.object_id => retorna 60
carlos_dois.object_id => retorna 80

Como eles são objetos com id's diferentes o Ruby nos diz que eles não são iguais, mesmo que os parâmetros tenham os mesmos valores.

A solução

É aqui que entra o papel de redesenhar o operador de igualdade. O que nós queremos é que os valores sejam comparados, e se os valores forem iguais e da mesma classe, o == vai retornar true.

A diferença das implementações.

Primeiro, vamos recriar o método dentro da nossa classe:

class Funcionario
  attr_reader :nome, :time

  def initialize(nome, time)
    @nome = nome
    @time = time
  end

  def ==(outro)
    end
end

Definimos o método == com um parâmetro chamado outro, que vai ser o objeto de comparação. O primeiro passo agora é conferir se o nosso primeiro objeto é da mesma classe do que o objeto outro de comparação.

def ==(outro)
  self.class == outro.class
end
  
Ao fazer apenas isso, vamos ter os seguintes resultados:

carlos = Funcionario.new('Carlos', 'Marketing')
jose = Empregado.new('José', Marketing')
carlos_dois = Funcionario.new('Carlos', 'Marketing')
marina = Funcionario.new('Marina', 'Vendas')

carlos == jose
# => retorna false, pois são de classes diferentes
carlos == carlos_dois
# => retorna true, mas não pelos motivos corretos
carlos == marina
# => retorna true, pois são da mesma classe,
# mas não queremos pois tem valores diferentes

Então agora está faltando a gente comparar os valores, pois não queremos que carlos e marina sejam iguais, apenas que carlos e carlos_dois sejam iguais. Para isso, vamos adicionar um operador de conjunção:

def ==(outro)
  self.class == outro.class &&
    @nome == outro.nome &&
        @time == outro.time
end

Agora ele está comparando se os parâmetros nome e time também são iguais nos dois objetos, resultando no comportamento desejado:

carlos == jose
# => retorna false, pois são de classes diferentes
carlos == carlos_dois
# => retorna true, pois tem os mesmos valores
carlos == marina
# => retorna false, pois tem valores diferentes

Testando vários casos

Para provar que essa implementação funciona em vários casos, vamos testar algumas coisas:

carlos == Funcionario.new('Carlos', 'Marketing')
# retorna true pois tem os mesmos valores
carlos != carlos_dois
# retorna false, pois tem os mesmos valores

Para mais uma prova, vamos refazer apenas com um valor, considerando que não existe mais o parâmetro time na nossa classe Funcionario.

def ==(outro)
  self.class == outro.class &&
    @nome == outro.nome
end

Agora se fizermos mais um teste:

carlos = Funcionario.new('Carlos')

carlos == 'Carlos'
# retorna false, pois são de classes diferentes,
# o primeiro é da classe Funcionario, o segundo é
# da classe String padrão do Ruby.

Como o ActiveRecord reescreve o ==

Se você quiser ir além e reescrever esse código para entidades (models do Rails, por exemplo), você vai precisar adicionar um super (que irá retornar true se forem os mesmos objetos) e conferir se, de fato, o id não é nil. É assim que o ActiveRecord reescreve o operador de igualdade:

def ==(comparison_object)
   super ||
     comparison_object.instance_of?(self.class) &&
     !id.nil? &&
     comparison_object.id == id
end
alias :eql? :==

O instance_of? faz a mesma coisa que fizemos acima: self.class == comparison_object.class. Para se aprofundar, veja o post do Shopify.

É isto! Gostei de ter feito esse desafio, e espero que essa postagem seja útil para alguém que queira fazer isso no futuro. Caso encontre algum erro, por favor, me avise. Até mais!

Receba atualizações por e-mail

Sempre que tiver novidades por aqui vou enviar aos inscritos.

Entre na conversa

Criei uma postagem lá no LinkedIn para receber comentários:

Comente