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.
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.
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.
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.
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.
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.
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.
This forum is not affiliated to the Ruby language, Ruby on Rails framework, nor any Ruby applications discussed here.