1

I was experimenting with Ruby's default Hash values in v2.3.7. I was surprised by some output I got in a simple test case, and I was wondering what was going on behind the scenes that would explain it.

foo = Hash.new({x: 0, y: 0}) # provide a default value 
foo['bar'][:x] += 1          # expect to add to the default value
foo                          # outputs `{}` ?! expected {'bar'=>{:x=>1,:y=>0}}
foo['bar']                   # outputs `{:x=>1, :y=>0}` as expected

Why is it that foo appears to be empty on line 3? I expected output like {'bar'=>{:x=>1,:y=>0}}. Am I missing something super basic about why this is happening? foo.empty? returns true, but foo['bar'] produces output.

Is this a bug?

Md. Farhan Memon
  • 6,055
  • 2
  • 11
  • 36
aardvarkk
  • 14,955
  • 7
  • 67
  • 96
  • I think this may be related to the idea that providing a default value that is itself an object means that every entry in `foo` actually points to *the same object*. Changing one of them changes all entries, since the default object isn't copied. – aardvarkk Nov 02 '18 at 20:52

3 Answers3

3

When you use the default value, no key/value is set. The default is simply returned instead of nil.

I think you're imagining it works like this where the default is set on the key being accessed like ||=.

default = {x: 0, y: 0}
foo = Hash.new
foo['bar'] ||= default 
foo['bar'][:x] += 1

Instead, it works like this where the default is returned when there is no key.

default = {x: 0, y: 0}
foo = Hash.new
val = foo['bar'] || default
val[:x] += 1

Put another way, you're expecting this.

def [](key)
  @data[key] ||= default
end

But it works like this.

def [](key)
  @data[key] || default
end

But this behaviour appears to change if I provide, say, an integer instead of a Hash as the default value. For instance, if I do foo = Hash.new(1), then foo['bar'] += 1 the behaviour is what I would expect. foo is not empty, and the default value has not changed. – aardvarkk 4 mins ago

foo['bar'] += 1 is really shorthand for

default = foo['bar']        # fetch the default
foo['bar'] = default + 1    # sets 'bar' on foo

Note that it calls []= on foo.

foo['bar'][:x] += 1 is shorthand for...

default = foo['bar']   # fetch the default value
val = default[:x]      # fetch :x from the default
default[:x] = val + 1  # set :x on the default value

Note that it calls []= on the default value, not foo.

Schwern
  • 153,029
  • 25
  • 195
  • 336
  • But this behaviour appears to change if I provide, say, an integer instead of a Hash as the default value. For instance, if I do `foo = Hash.new(1)`, then `foo['bar'] += 1` the behaviour is what I would expect. `foo` is not empty, and the default value has not changed. – aardvarkk Nov 02 '18 at 21:10
  • 1
    That's what I explained, in case where default is an integer, and your recieving key is `bar`, `bar` is created and added to hash `foo`, in case of your question, when default is hash, your recieving key is `x` and not `bar` so `x` is changed as expected and no change is done to `bar` / `foo`.@aardvarkk – Md. Farhan Memon Nov 02 '18 at 21:14
  • @aardvarkk I've added a further explanation. – Schwern Nov 02 '18 at 21:19
2

Basically you are changing the default value and not assigning new key to the hash. Can be understood by calling any key to the hash e.g.

foo['bar']
=> {:x=>1, :y=>0}
foo['foobar']
=> {:x=>1, :y=>0}

Another way of looking at it,

foo['bar'][:x] += 1
=> 2

foo['bar']
=> {:x=>2, :y=>0}

foo['bar'][:x] += 1
=> 3

foo['bar']
=> {:x=>3, :y=>0}

foo['bar'][:x] + 1
=> 4

foo['bar']
=> {:x=>3, :y=>0} # here the value is not assigned so not changed - as expected
Md. Farhan Memon
  • 6,055
  • 2
  • 11
  • 36
  • Is there a way to do this that behaves the way I was expecting? I would assume when accessing a key value that didn't exist, it would be created. Isn't that the way hashes normally work in Ruby? – aardvarkk Nov 02 '18 at 20:56
  • No, because, you assigned / manipulated the key `x` which is changed as expected but since it was part of default value, and u didn't assign it to any other key it won't create any key for you. – Md. Farhan Memon Nov 02 '18 at 20:58
  • @aardvarkk I updated my answer for more understanding – Md. Farhan Memon Nov 02 '18 at 21:04
0

As Md. Farhan Memon already mentioned:

Basically you are changing the default value and not assigning new key to the hash.

But this is returning what you want:

foo = Hash.new({x: 0, y: 0}) # provide a default value
foo[:whatever][:x] += 1
foo['bar'] = foo['bar']
foo #=> {"bar"=>{:x=>1, :y=>0}}

First you changed the default value, then assigned to a new key ['bar'] the default value. Works also with foo['bar'] = foo[:whatever_else].

foo['bar'] = foo[:whatever_else]
foo['baz'] = foo[:whatever_else]
foo #=> {"bar"=>{:x=>1, :y=>0}, "baz"=>{:x=>1, :y=>0}}
iGian
  • 11,023
  • 3
  • 21
  • 36