Hash/Set-related behavior mismatch between Ruby MRI 3.0.0 and 3.0.1

Consider the following code:

raise unless RUBY_VERSION == "3.0.0" || RUBY_VERSION == "3.0.1"

require "set"

class Locale
  attr_reader :code

  def initialize(code:)
    @code = code
  end

  def eql?(other)
    other.respond_to?(:to_sym) && to_sym == other.to_sym
  end
  alias == eql?

  def to_sym
    code.to_sym
  end

  def hash
    code.hash
  end
end

p Set.new(((1..1000).to_a + [:ru, :en])).include?(Locale.new(code: :en))

Running it on Ruby MRI 3.0.0 will print true or false inconsistently. Running on Ruby MRI 3.0.1 (and 2.7.4) will always print true.

I wonder what exactly changed in MRI internal implementation? Looking at diff between v3.0.0 and v3.0.1 at ruby’s github repo does not seem to give any results

Hi Yaroslav,

The behavior you’re seeing is likely related to changes in the hash collision handling between the two versions. In Ruby 3.0.0, the hash function can lead to collisions between a symbol and a user-defined class using the hash instance method. Starting from Ruby 3.0.1, this type of hash collision is handled differently, hence you see consistent result.

However, it’s still advisable to use the original object instead of converting it to a symbol when comparing within the eql? method to avoid such situations.

Example:

def eql?(other)
  other.instance_of?(self.class) && @code == other.code
end

I hope this helps! If you have more questions, feel free to ask.

-Bobby

Hi Robert! Would you be so kind to point to me the exact commit that implemented these changes? I’m curious because I’ve looked every of them between 3.0.0 and 3.0.1 and found nothing.

nevermind, just figured out that’s a bot