Forum: Ruby Weird hash behavior

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
42bc752eed920d91957cf31728c1a615?d=identicon&s=25 Sum Proxy (sumproxy)
on 2016-02-15 12:18
class MyHashWrapper
  def initialize
    @hash = Hash.new([])
  end

  def add(val, key)
    @hash[key] << val
  end

  def to_h
    @hash
  end
end

h = MyHashWrapper.new
h.add("String", 2)
p h.to_h

The code above to my surprise prints {}, although I expected it to print
{2=>["String"]}. What am I missing?
0fa73332c8e4a3b06ea439fd3f034322?d=identicon&s=25 Ronald Fischer (rovf)
on 2016-02-15 12:51
You think that the way you create your hash, ensures, that each (yet
undefined) hash element springs into existence as an empty array.
However, this is not the case. Try out the following:

    hash = Hash.new([])
    hash[2] << 'x'
    puts hash[2]

So far, nothing unusual. Now do a

    puts hash[3]

You will see, that this ALSO outputs an x. Next, do a

    hash[4] << 'y'
    puts hash[2]

You will see that this prints x and y. What happens is, that *all* hash
elements share the same array. This is because when Ruby copies the
element to create a new hash key, there is no "deep copy" (i.e. cloning)
involved. Only the array reference is copied.

Now to your question:

You might get a hint of what is going on, is if you try

    puts hash.keys

This will print an empty list. No keys defined. Why? You had defined a
certain array to replace the value nil to denote "no value defined".
Since there is no key which is different from this value, it means there
is no key defined at all. Hence you get an empty list when you print out
your hash.
42bc752eed920d91957cf31728c1a615?d=identicon&s=25 Sum Proxy (sumproxy)
on 2016-02-15 13:03
Well, I believe this behavior is cryptic and doesn't follow the
principle of least surprise. Thanks for clarification though.
05d9948ffac2a58b6747a8028c1061d6?d=identicon&s=25 Khirod Patra (khirodjruby)
on 2016-02-15 13:08
Try this:

class MyHashWrapper
  def initialize
    @hash = Hash.new
  end

  def add(val, key)
    @hash[key] = val
  end

  def to_h
    @hash
  end
end

h = MyHashWrapper.new
h.add("String", 1)
h.add("String", 2)
p h.to_h
42bc752eed920d91957cf31728c1a615?d=identicon&s=25 Sum Proxy (sumproxy)
on 2016-02-15 13:13
Khirod Patra wrote in post #1181395:
> Try this:
>
> class MyHashWrapper
>   def initialize
>     @hash = Hash.new
>   end
>
>   def add(val, key)
>     @hash[key] = val
>   end
>
>   def to_h
>     @hash
>   end
> end
>
> h = MyHashWrapper.new
> h.add("String", 1)
> h.add("String", 2)
> p h.to_h

I tried it and it works, but that's not what I need. I need @hash[key]
to be an array accumulating multiple values. This is why I used [] as
the default value for Hash.new.
0fa73332c8e4a3b06ea439fd3f034322?d=identicon&s=25 Ronald Fischer (rovf)
on 2016-02-15 16:49
Sum Proxy wrote in post #1181394:
> Well, I believe this behavior is cryptic and doesn't follow the
> principle of least surprise. Thanks for clarification though.

Hmmmm.... What would be the alternative? Imagine that you are using an
instance of a class of your own design as a replacement for nil. Would
you expect to get a "copy" of your object? What if you don't provide a
'clone' method?

The main argument against requiring a Hash to copy your array (and, if
it does, should it do a shallow copy or a deep copy?) would be that this
would break a fundamental property of the Hash class: By design, the nil
object, is the ONLY object of NilClass, and so every "nil" element must
be the identical element. Hence, if you *replace* this nil object by
your own implementation of "nil" - in your case, an array -, it must
also be ensured, that all "nil" elements share the identical copy of
this element.

I wonder, what is your application, that you want to replace the
"nil"-object of your hash by something different? Maybe you are just
using the wrong tool for what you want to achieve.
0fa73332c8e4a3b06ea439fd3f034322?d=identicon&s=25 Ronald Fischer (rovf)
on 2016-02-15 16:55
Sum Proxy wrote in post #1181396:
> I need @hash[key]
> to be an array accumulating multiple values. This is why I used [] as
> the default value for Hash.new.

In this case, I think the usage of a default value is the wrong tool.

You could accumulate the values like this:

     (hash[key]||=[]) << value

If you have many places in your program, where you are doing this, and
find this style cumbersome to write all the time, you could
alternatively use the block argument of Hash.new to provide the code to
create an empty array, if needed.
42bc752eed920d91957cf31728c1a615?d=identicon&s=25 Sum Proxy (sumproxy)
on 2016-02-15 17:29
Ronald Fischer wrote in post #1181402:
> Sum Proxy wrote in post #1181394:
>> Well, I believe this behavior is cryptic and doesn't follow the
>> principle of least surprise. Thanks for clarification though.
>
> Hmmmm.... What would be the alternative? Imagine that you are using an
> instance of a class of your own design as a replacement for nil. Would
> you expect to get a "copy" of your object? What if you don't provide a
> 'clone' method?

Yes, I would expect a copy. Without 'clone' an exception could be
raised, but we are not talking of customary cases here, we're talking
about a plain Array.

> The main argument against requiring a Hash to copy your array (and, if
> it does, should it do a shallow copy or a deep copy?) would be that this
> would break a fundamental property of the Hash class: By design, the nil
> object, is the ONLY object of NilClass, and so every "nil" element must
> be the identical element. Hence, if you *replace* this nil object by
> your own implementation of "nil" - in your case, an array -, it must
> also be ensured, that all "nil" elements share the identical copy of
> this element.

I think these explanations 'why' this is so and not the less surprising
way don't mean much in a language whose purpose was to enforce the
principle of least surprise in the first place.

> You could accumulate the values like this:

I figured out how to find a way around this.
7cee9dd780723c5fa0f06a242338e57c?d=identicon&s=25 Douglas S. (douglas_s)
on 2016-02-19 18:21
Sum Proxy wrote in post #1181391:
> class MyHashWrapper
>   def initialize
>     @hash = Hash.new([])
>   end

This will create a Hash whose default value is a single array, the one
instantiated in the Hash.new statement. To create a Hash whose default
value is an array dynamically created when needed (which is your intent
I think), you need to use the block form of the Hash constructor.

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

Here is the relevant documentation (which isn't linkable unfortunately)
from http://docs.ruby-lang.org/en/2.0.0/Hash.html

new → new_hash
new(obj) → new_hash
new {|hash, key| block } → new_hash
Returns a new, empty hash. If this hash is subsequently accessed by a
key that doesn't correspond to a hash entry, the value returned depends
on the style of new used to create the hash. In the first form, the
access returns nil. If obj is specified, this single object will be used
for all default values. 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.
02c8490928c5546aaef1ec6f5616f50c?d=identicon&s=25 Kidz Kul (ronn91)
on 2017-04-04 05:04
Only wanna tell that this is very useful.Thanks for taking your time to
write this. You can also visit my website.....

http://wordcookiesgame.com/
http://hillclimbracing-2.com/
http://fivenightsatfreddys-4.com/
http://hotmailcomsignin.email/
This topic is locked and can not be replied to.