Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Method named `hash` in main module overrides some object's `hash` method

Tags:

ruby

Given this script

def hash
  puts "why?"
end

x = {}
x[[1,2]] = 42

It outputs the following

why?
/tmp/a.rb:6:in `hash': no implicit conversion of nil into Integer (TypeError)
    from /tmp/a.rb:6:in `<main>'

It seems that the hash function defned in the script is overriding Array#hash in that case. Since the return value of my hash method is nil and not an Integer, it throws an exception. The following script seems to confirm this

puts [1,2,3].hash

def hash
  puts "why?"
end

puts [1,2,3].hash

The output is

-4165381473644269435
why?
/tmp/b.rb:6:in `hash': no implicit conversion of nil into Integer (TypeError)
    from /tmp/b.rb:6:in `<main>'

I tried looking into the Ruby source code but could not figure out why this happens. Is this behavior documented?

like image 621
Becojo Avatar asked Dec 14 '17 18:12

Becojo


2 Answers

You're not overriding Array#hash, you're shadowing Kernel#hash by creating Object#hash:

puts method(:hash)
def hash   
  puts "why?"
end
puts method(:hash)

That prints:

#<Method: Object(Kernel)#hash>
#<Method: Object#hash>

Fix it so we can see more:

def hash
  puts "why?"
  super
end

x = {}
x[[1,2]] = 42

Now the output is:

why?
why?

And no error. Try it with x[[1,2,3,4,5,6,7]] = 42 and you'll instead see why? printed seven times. Once for each array element, since the array's hash method uses the hashes of its elements. And Integer#hash doesn't exist, it inherits its hash method from Object/Kernel, so yours gets used.

like image 114
Stefan Pochmann Avatar answered Oct 03 '22 00:10

Stefan Pochmann


This is due to a kind of hack in Ruby top level. Have you ever wondered how this works?

def foo
end
p self
foo

class Bar
  def test
    p self
    foo
  end
end

Bar.new.test # no error

How are two totally different objects (main and a Bar) able to call foo like it's a private method call? The reason is because... it is a private method call.

When you define a method at the top level of your Ruby script, it gets included (via Object) in every object. That's why you can call top-level methods like they are global functions.

But why does this break only hash and not other common methods? def to_s;end won't break to_s, for example. The reason is because hash is recursive: most* class implementations ultimately call down to Object#hash for their implementations. By redefining that base case, you break it globally. For other methods like to_s you won't see a global change because it's way up the inheritance chain and doesn't get invoked.

* the only objects this doesn't break are a few literals that probably have hard-coded hash values e.g. [] {} "" true etc.

like image 21
Max Avatar answered Oct 03 '22 00:10

Max