Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby Set with custom class to equal basic strings

Tags:

ruby

I want to be able to find a custom class in my set given just a string. Like so:

require 'set'

Rank = Struct.new(:name, keyword_init: true) {
  def hash
    name.hash
  end
  def eql?(other)
    hash == other.hash
  end
  def ==(other)
    hash == other.hash
  end
}
one = Rank.new(name: "one")
two = Rank.new(name: "two")
set = Set[one, two]

but while one == "one" and one.eql?("one") are both true, set.include?("one") is still false. what am i missing?

thanks!

like image 370
Justin Bishop Avatar asked May 20 '20 05:05

Justin Bishop


1 Answers

Set is built upon Hash, and Hash considers two objects the same if:

[...] their hash value is identical and the two objects are eql? to each other.

What you are missing is that eql? isn't necessarily commutative. Making Rank#eql? recognize strings doesn't change the way String#eql? works:

one.eql?('one') #=> true
'one'.eql?(one) #=> false

Therefore it depends on which object is the hash key and which is the argument to include?:

Set['one'].include?(one) #=> true
Set[one].include?('one') #=> false

In order to make two objects a and b interchangeable hash keys, 3 conditions have to be met:

  1. a.hash == b.hash
  2. a.eql?(b) == true
  3. b.eql?(a) == true

But don't try to modify String#eql? – fiddling with Ruby's core classes isn't recommended and monkey-patching probably won't work anyway because Ruby usually calls the C methods directly for performance reasons.

In fact, making both hash and eql? mimic name doesn't seem like a good idea in the first place. It makes the object's identity ambiguous which can lead to very strange behavior and hard to find bugs:

h = { one => 1, 'one' => 1 }
#=> {#<struct Rank name="one">=>1, "one"=>1}

# vs

h = { 'one' => 1, one => 1 }
#=> {"one"=>1}
like image 165
Stefan Avatar answered Jan 03 '23 13:01

Stefan