5

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?

Becojo
  • 551
  • 1
  • 7
  • 18
  • I'm not getting this behaviour. `class Array; def hash; nil; end; end; puts [1,2,3].hash` prints an empty line and returns nil for me. `x[[1,2]] = 1` still raises the TypeError, but `x[nil] = 1` works fine. Wierd behavior indeed .. what ruby version are you on? I'm using 2.3.1 – max pleaner Dec 14 '17 at 18:59

2 Answers2

4

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.

Stefan Pochmann
  • 27,593
  • 8
  • 44
  • 107
  • 1
    On my system the second `method(:hash)` actually crashes the interpreter. Maybe Ruby devs should put a warning when top-level hash is defined... – Max Dec 14 '17 at 19:24
  • Where/how did you try it? In IRB or so, or did you save it in a script file and run that? – Stefan Pochmann Dec 14 '17 at 19:25
  • Only in IRB does it crash. But redefining `Object#hash` still seems like a warning-worthy thing. I can't see that ever being a good idea. – Max Dec 14 '17 at 19:32
  • 1
    Interesting. I did not think a method from the main script could shadow a `Kernel` method like that without explicitely specifying it. – Becojo Dec 14 '17 at 20:41
  • @Becojo Didn't know that before this, either :-). I think the first paragraph of [Chuck's answer about the `main` object](https://stackoverflow.com/a/917842/1672429) explains it well. – Stefan Pochmann Dec 14 '17 at 20:56
3

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.

Max
  • 21,123
  • 5
  • 49
  • 71
  • 1
    Hmm, `x[[[], {}, ""]] = 42` and `x[true] = 42` don't cause an error, but `x[[true]] = 42` does. I can't explain that yet... – Stefan Pochmann Dec 14 '17 at 19:40
  • I might have found the culprit. Starts at [line 666](https://github.com/ruby/ruby/blob/9cbb3bd1f2b05d59b6dd315ebdec295c2ced6c01/hash.c#L666) :-). Looks like `x[true]` uses `RHASH` to get the hash value of `true`. Which is defined [in `internal.h`](https://github.com/ruby/ruby/blob/9cbb3bd1f2b05d59b6dd315ebdec295c2ced6c01/internal.h#L669), so I guess that really uses something low-level internal instead of our `hash` method. But `Array#hash` [uses `rb_hash`](https://github.com/ruby/ruby/blob/trunk/array.c#L3964), which apparently does use our high-level `hash` method. – Stefan Pochmann Dec 14 '17 at 20:39