Récemment j'ai eu besoin d'utiliser uniq sur une collection d'objet ruby. En l'occurrence il s'agissait de modèles ActiveRecord.

L'idée était donc de filtrer ces objets en se basant sur l'un des attributs pour éviter les doublons.
En premier lieu je pensais que uniq pourrait prendre un bloc en paramètre indiquant comment comparer les éléments deux à deux, du genre : books.uniq { |b1, b2| b1.name == b2.name }

Pour le moment uniq ne sait pas faire ça (ou j'ai raté quelquechose).

Egalité et identité des objets ruby

uniq utilise eql? pour comparer les éléments deux à deux.
Pour qu'uniq fonctionne sur une collection d'objets, il nous faut donc surcharger eql? et hash qui vont de pair.

Pourquoi pas == ?
Contrairement à ==, .eql? compare aussi les types. Par exemple 42.0 == 42 mais ! 42.eql?(42.0)

A quoi sert hash ?

hash permet de générer un identifiant pour un objet non pas pour rendre l'objet unique mais pour permettre les comparaisons.
eql? et hash sont intimement liés.

Et equal?
equal permet de comparer l'identité des objets. Deux objets peuvent être strictement identiques en terme de valeur sans pour autant être les mêmes.

Ex :

a = "42"
b = "42"

a == b, a.eql?(b), a.hash == b.hash mais ! a.equal?(b) car a.object_id != b.object_id

La pratique : implémenter uniq sur une collection d'objets ruby

#!/usr/bin/env ruby -w
 
class Book
  attr_accessor :name
 
  def initialize(attributes)
    attributes.each do |k, v|
      send("#{k}=", v)
    end
  end
 
  def eql?(o)
    hash.eql?(o.hash)
  end
 
  def hash
    name.downcase.hash
  end  
end
 
books = [
  Book.new({ :name => "Programming Ruby" }),
  Book.new({ :name => "Harry Potter" }),
  Book.new({ :name => "Ruby on rails" }),
  Book.new({ :name => "programming ruby" })
]

Avec ce code :

books.length == 4
books.uniq.length == 3