1

I was trying a few lines of code on IRB, and then I found that I could not modify a new array returned as a default value from a Hash. The following is an IRB session that shows in more details my situation:

a = Hash.new { Array.new }
#=> {}
a[2]
#=> []
a[2].push '2'
#=> ["2"]
a[2]
#=> []
a[2] = []
#=> []
a[2].push '2'
#=> ["2"]
a
#=> {2=>["2"]}
a[2].push '2'
#=> ["2", "2"]
a
#=> {2=>["2", "2"]}

Why I cannot modify the default value of a nonexistent key?

Yu Hao
  • 119,891
  • 44
  • 235
  • 294
yeyo
  • 2,954
  • 2
  • 29
  • 40
  • You need `a[2] = a[2].push 2`. Then `a => {2=>[2]}`. `a[2]` just returns the default value, so `a[2].push 2` adds `2` to the empty array, but that does not change the hash `a`. – Cary Swoveland Nov 11 '14 at 04:26
  • Look at the `object_id` of `a[2]`. It's not constant, meaning that the result of the block is not inserted with the key `2`. – August Nov 11 '14 at 04:31

3 Answers3

2

The block form of Hash#new is used like this:

b = Hash.new {|hash, key| hash[key] = Array.new}
#=> {}
b[2]
#=> []
b[2].push('2')
#=> ["2"]
b[2]
#=> ["2"]
b[3]
#=> []
b[3].push('3')
#=> ["3"]
b[3]
#=> ["3"]
Yu Hao
  • 119,891
  • 44
  • 235
  • 294
  • I might be wrong but, Hash.new(Array.new) is different than Hash.new {Array.new}. Try it yourself with `#object_id` – yeyo Nov 11 '14 at 04:30
  • @Kira From what I understand, what you expected is `Hash.new(Array.new)`, not the block form. – Yu Hao Nov 11 '14 at 04:33
  • @August No, because I will have the same object for every nonexistent key. – yeyo Nov 11 '14 at 04:33
  • try this: `a = Hash.new Array.new` and then `a[2] = a[2].push 1` then, print the value for ... lets say the key 33 `a[33]`. Using `Hash.new(Array.new)`, the result will be `=> [1]` – yeyo Nov 11 '14 at 04:38
  • @Kira See the edit, that's what you want now, right? – Yu Hao Nov 11 '14 at 04:39
  • 1
    That is one way to to do it, but readers should not infer that there is anything wrong with the default value given by the OP (`{ [] }`). That works fine as long as `a[2]` appears as an lvalue,. (See my answer.). One potential "gotcha" of the form you are using is that if `a = {}`, and I want to print 'hi' if `a` has a key `2`, I need to write `puts 'hi' if a.key?(2)`. If I write `puts 'hi' if a[2]`, I've got two problems: `if a[2]` returns `[]`, a truthy value, and now `a => { 2=>[] }`. – Cary Swoveland Nov 11 '14 at 05:52
  • @CarySwoveland good observation. Thank you. – yeyo Nov 11 '14 at 11:55
2

See Hash.new {|hash, key| block } → new_hash. It says:

If a block is specified, it will be called with the hash object and the key, and should return the default value. It is the block’s responsibility to store the value in the hash if required.

That means you have to create a block that stores key's value you pass into hash when you're creating a new or updating an existing value. Since, your hash definition doesn't store it:

a = Hash.new { Array.new }

You'll lose the value, no matter how many times you try to do this:

a[2].push '2' #=> ["2"]
p a #=> {}

what you're doing is that you're then defining a key with a default value:

a[2] = []

and then you push values:

a[2].push #=> ["2"]
p a #=> {2 => ["2"]}

To this situation, there's another way in which you can define your block to be something like this:

a = Hash.new { |h, k| h[k] = Array.new }

or:

a = Hash.new { |h, k| h[k] = [] }

If you see the above block, it says: default value of the key will be an empty array and if any object is pushed into it:

a[2].push '2' #=> ["2"]

It will store that object into that array:

p a #=> {2=>["2"]}
Surya
  • 15,703
  • 3
  • 51
  • 74
  • The first sentence is incorrect. It is true, of course that it will work with the alternative form of the default block you give, but that is not essential; it works just fine with the block used by the OP, provide `a[2]` appears as an lvalue, as I explained in my answer. – Cary Swoveland Nov 11 '14 at 05:42
  • Well, either an incorrect block is being passed, or the block `{ [] }` block is not being used properly. We can't say one way is correct and the other is not. Both ways are fine as long as they are used properly. I think you should have said that if `{ [] }` is to be used, then you must... (i.e., lvalue), but there is another way that it could be done... – Cary Swoveland Nov 11 '14 at 06:01
  • The first sentence is still not right, as it conflicts with the doc you reproduced directly below it. The block is to return the default value. `{ [] }` (same as `{ |k| [] }`) does that. The block may also create a key-value pair (and/or do many other things), but that's optional. The form `{ [] }` is suitable if it is used with `a[2]` on both sides of `=` or if you simply want to know what the default value is in your code. – Cary Swoveland Nov 11 '14 at 06:16
  • 1
    I will say one more thing, then shut up. :-) (And I'll be deleting this comment.) There's one small thing I suggest you do. Where you say "To make it work. You'll have to define a key by default: `a[2] = []`. If you do that, there is no point having a default empty array. What you want is `a[2] = a[2]`. Now you have a value equal to the default, which could be an empty array or something else. Once that's done, you can modify `a[2]`, e.g., `a[2] << 3`. The more usual way, however, is to do it in one step: `a[2] = a[2] << 3`. Good night! – Cary Swoveland Nov 11 '14 at 06:32
  • Right now I think my earlier comments are instructive. I'll check in the morning to see if they are still needed. – Cary Swoveland Nov 11 '14 at 06:33
  • @CarySwoveland : I was constructing what OP was trying to do with the explanation what's happening when he does: `a[2] = []`, but then yes, that won't make sense. – Surya Nov 11 '14 at 06:38
  • btw, about a week ago I enjoyed reading, "How I started my career after graduation", and referred a friend in B'Lore to it. – Cary Swoveland Nov 11 '14 at 06:42
  • @CarySwoveland : Hey, thank you. Any suggestions you think where I have any area to improve? like writing, technical, etc? Please feel free to tell. Always excited to learn. :-) – Surya Nov 11 '14 at 06:52
  • I have a question for the person who +1'd my comment above that begins, "I will say...". What did you like about the comment, my pithy, insightful observation or just the first sentence? – Cary Swoveland Nov 11 '14 at 16:09
2

There is no problem using:

a = Hash.new { Array.new }

If I execute a[2] it will return [], because that's the default value. It's just telling me what it is, nothing more.

Because the default is given as a block, the default value is a new empty array each time a[x] is invoked. The important thing to understand is that it is merely an empty array, not tied to the hash a in any way. That is, a[2] only answers the question, "what is the default value?"; it doesn't do anything with it. (As shown in other answers, if the block were written differently, all sorts of wondrous things could be done before returning the default value.)

Note that Hash.new { Array.new }, Hash.new { [] } and Hash.new { |h,k| Array.new } are all equivalent.

In order to add the key 2 to a, a[2] must be an lvalue. You have that later in your example:

a[2] = []

but if you do that there's no point to having a default. What you want to do is set a[2] equal to the default value:

a[2] = a[2]

(Note that if, as in this case, the return value of the block does not depend on the key, the line above is equivalent to:

a[2] = a[123456]

provided there is no key 123456.)

That adds the key-value pair 2=>[] to the hash, so now

a[2].push 2
a[2].push 3
a #=> {2=>[2, 3]}

It make more sense, however, to do this in one step:

a[2] = a[2].push 2

If a does not have a key 2, a[2] on the right of the equality will equal the default value of an empty array, 2 will be pushed onto it and the value for key 2 will be set equal to [2].

If a already has a key 2, say a[2] = [3,4,5], a[2] on the right will be [3,4,5], 2 will be pushed onto that and a (if it has no keys other than 2) will be:

a #=> {2=>[3,4,5,2]]}
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100