Ruby hash defaults: where do nested values go? [duplicate] - ruby

This question already has answers here:
Strange, unexpected behavior (disappearing/changing values) when using Hash default value, e.g. Hash.new([])
(4 answers)
Closed 6 years ago.
I wanted to use Ruby's default hash values to allow me to more easily nest hashes without having to manually initialize them. I thought it'd be nice to be able to dig a level down for each key safely without having pre-set the key as a hash. However, I find that when I do this, the data gets stored somewhere, but is not visible by accessing the top-level hash. Where does it go, and how does this work?
top = Hash.new({}) #=> {}
top[:first][:thing] = "hello" #=> "hello"
top[:second] = {thing: "world"} #=> {:thing => "world"}
top #=> {:second => {:thing => "world"}}
top[:first] #=> {:thing => "hello"}

You want to know where your inserted hash is? Maybe you have heard about Schroedingers cat:
h = Hash.new({})
h[:box][:cat] = "Miau"
=> "Miau"
h
=> {}
The cat seem to be dead....
h[:schroedingers][:cat]
=> "Miau"
The cat seem still to be alive, but in a different reality....
Ok, if nothing helps, "Read The Fine Manual". For Hash.new, we read:
If obj is specified, this single object will be used for all default values.
So when you write h[:box], a object is returned, and this object is another hash, and it happen to empty.
Into this empty hash, you write an key-value.
Now this other hash is no longer empty, it has a key-value pair. And it is returned every time you search for a key is not found in your original hash.

You can access the default value via a variety of #default methods
http://ruby-doc.org/core-2.2.3/Hash.html#method-i-default
top.default
=> {:thing=>"hello"}

You can also tell it how you want it to act, example:
irb(main):058:0> top = Hash.new {|h,k| h[k] = {}; h[k]}
=> {}
irb(main):059:0> top[:first][:thing] = "hello"
=> "hello"
irb(main):060:0> top[:second] = {thing: "world"}
=> {:thing=>"world"}
irb(main):061:0> top
=> {:first=>{:thing=>"hello"}, :second=>{:thing=>"world"}}

Related

Unexpected FrozenError when appending elements via <<

I have a hash containing names and categories:
hash = {
'Dog' => 'Fauna',
'Rose' => 'Flora',
'Cat' => 'Fauna'
}
and I want to reorganize it so that the names are grouped by their corresponding category:
{
'Fauna' => ['Dog', 'Cat'],
'Flora' => ['Rose']
}
I am adding each names via <<:
new_hash = Hash.new
hash.each do |name , category|
if new_hash.key?(category)
new_file[category] << name
else
new_hash[category] = name
end
end
But I am being told that this operation is being performed on a frozen element:
`<<' : Can’t modify frozen string (FrozenError)
I suppose this is because each yields frozen objects. How can I restructure this code so the '.each' doesn't provide frozen variables?
I needed to add the first name to an array and then that array to the hash.
new_hash = Hash.new
hash.each do |name , category|
if new_hash.key?(category)
new_file[category] << name
else
new_hash[category] = [name] # <- must be an array
end
end
How can I restructure this code so the '.each' doesn't provide frozen variables?
Short answer: you can't.
Hash#each doesn't "provide frozen variables".
First off, there is no such thing as a "frozen variable". Variables aren't frozen. Objects are. The distinction between variables and objects is fundamental, not just in Ruby but in any programming language (and in fact pretty much everywhere else, too). If I have a sticker with the name "Seamus" on it, then this sticker is not you. It is simply a label that refers to you.
Secondly, Hash#each doesn't provide "variables". In fact, it doesn't provide anything that is not in the hash already. It simply yields the objects that are already in the hash.
Note that, in order to avoid confusion and bugs, strings are automatically frozen when used as keys. So, you can't modify string keys. You can either make sure they are correct from the beginning, or you can create a new hash with new string keys. (You can also add the new keys to the existing hash and delete the old keys, but that is a lot of complexity for little gain.)

Using nested hash and a hash as default value isn't working as expected [duplicate]

This question already has answers here:
Strange, unexpected behavior (disappearing/changing values) when using Hash default value, e.g. Hash.new([])
(4 answers)
Closed 6 years ago.
I want to understand why a in the following is an empty hash after a[:a] is called, which is not empty:
a = Hash.new({a:1}) # => {}
a[:a][:a] += 1 # => 2
a # => {}
a[:a] # => {:a=>2}
I know that {a: 1} is shared between every inexistent key (a[:something_inexistent]). But why isn't it displayed as a key of a? If possible, using Ruby's internal would be much appreciated.
It is because a[:a] is never assigned. When you do a[:a], it is not returning the stored value, but is returning the default value.
Unlike this, when you instead have:
a = Hash.new{|h, k| h[k] = {a: 1}}
then, whenever a missing key is called, the block is executed, which assigns the value to the key, and a will no longer be an empty hash.

is there a convenience way to build nested hash in ruby [duplicate]

This question already has answers here:
Ruby - Access multidimensional hash and avoid access nil object [duplicate]
(3 answers)
Closed 8 years ago.
I have several vars such as year, month, day, hour, minute. I need to do some operations on a nested hash with these values like hash[2012][12][12][4][3]. When meet a new date, I need to extend the hash.
I can do this by judging each level one by one, and create a hash if not exists. But I'm wondering there is a convenience way to do it.
In perl, I can just call $hash{$v1}{$v2}{$v3}..{$vN} = something because hash not defined can be created by default.
I agree with sawa's comment. You will probably have many other problems with such approach. However, what you want is possible.
Search for "ruby hash default value".
By default, the default value of Hash entry is nil. You can change that, ie:
h = Hash.new(5)
and now the h when asked for non-existing key will return 5, not nil. You can order it to return a new empty array, or new empty hash in a similar way.
But, be careful. It is easy to accidentally SHARE the default instance through all entries.
h = Hash.new([]) // default value = Array object, let's name it X
one = h[:dad] // returns THE SAME object X
two = h[:mom] // returns THE SAME object X
You must be careful to not use the shared-default-instance, and to use operations that will not mutate it. You cannot just
h[:mom] << 'thing'
as the h[:brandnewone] will now return mutated default instance with "thing" inside.
See here for a good explanation and proper usage examples
or, even better: example of autovivifying hash
You could add a helper method, which you might find useful in other contexts:
def mfetch(hash, *keys)
return nil if (keys.empty? || !hash[keys.first])
return hash[keys.first] if keys.size == 1
k = keys.shift
raise ArgumentError, "Too many keys" unless hash[k].is_a? Hash
return mfetch(hash[k], *keys)
end
h = {cat: {dog: {pig: 'oink'}}} # => {:cat=>{:dog=>{:pig=>"oink"}}}
mfetch(h, :cat, :dog, :pig) # => "oink"
mfetch(h, :cat, :dog) # => {:pig=>"oink"}
mfetch(h, :cat) # => {:dog=>{:pig=>"oink"}}
mfetch(h, :cow) # => nil
mfetch(h, :cat, :cow) # => nil
mfetch(h, :cat, :dog, :cow) # => nil
mfetch(h, :cat, :dog, :pig, :cow) # => ArgumentError: Too many keys
If you preferred, you could instead add the method to the Hash class:
class Hash
def mfetch(*keys)
return nil if (keys.empty? || !hash[keys.first])
return self[keys.first] if keys.size == 1
k = keys.shift
raise ArgumentError, "Too many keys" unless self[k].is_a? Hash
return self[k].mfetch(*keys)
end
end
h.mfetch(:cat, :dog, :pig) # => "oink"
or if you are using Ruby 2.0, replace class Hash with refine Hash do to limit the addition to the current class. It might be convenient to put it in a module to be included.

Ruby change value in hash by reference

I'm using eval to work with a hash. This part works:
some_hash = {"a" => {"b" => "c"}}
target_hash = "some_hash"
target_key = "['a']"
my_value = eval(target_hash + target_key)
puts "my_value " + my_value.to_s
and prints:
my_value {"b"=>"c"}
How would I change the value, using eval, so that the hash results in this:
some_hash = {"a" => {"d" => "e"}}
Thanks
Edit:
I don't think I'm explaining correctly. I have to drill down into a hash, but I want the flexibility to do it with a string that is set at run time. That string could be "['key_level_1']['key_level_2']['key_level_3']" which is supposed to refer to some_hash['key_level_1']['key_level_2']['key_level_3'].
And again, i need to set that value to something. Does that make sense?
I would take an array e.g. ['key1', 'key2', 'key3'] (which can be constructed from an appropriately formatted string) and the "root object" and use it to locate a particular "target object" branch (I would recommend recursion). Then manipulate the appropriate object that was located. In this case some_hash should be the "root object" to start the traversal from, and not a string.
Here is a link to a really old answer I wrote (when I still actively used ruby). It doesn't handle the assignment bit, but I believe it shows the start of a valid approach that is eval-free: How do you access nested elements of a hash with a single string key? After the "target object" is located, then it's just a matter of assigning a new value to particular key. (The accepted answer in the linked post is also a gem, if not a little more cryptic and symbol-leaking.)
Happy coding.
You can use Hash#replace
def changeHash(a, b, targetHash, targetKey)
change = ".replace({'#{a}' => '#{b}'})"
my_value = eval(target_hash + target_key + change)
end
some_hash = {"a" => {"b" => "c"}}
target_key = "['a']"
changeHash('d', 'e', 'some_hash', '["a"]')
I suggest setting up a method like this:
def hash_change(target_hash,target_key,*new_value)
if new_value
target_hash["target_key"] = new_value
end
puts "my_value " + target_hash["target_key"]
end
That will give you flexibility to either display the original or a new hash should you pass in a new value.
Edit: sorry forgot the call to the method
hash_change(some_hash,"a",{"d" => "e"})
Thanks all, for the help. I changed it up a bit. Instead of passing in string as array, I just pass in an object like this:
{some_hash => {"a" => {"b" => "nil"}}}
Then I recursively traverse both the reference, and the real hash. When I detect nil in the reference, I know I am there.

Why does Hash.new({}) hide hash members? [duplicate]

This question already has answers here:
Strange, unexpected behavior (disappearing/changing values) when using Hash default value, e.g. Hash.new([])
(4 answers)
Closed 7 years ago.
Ok, so I wanted to create a hash which has an empty hash as the default value. A bit weird, I know, but let's say that I thought it might be useful.
So here's what I did:
>> a = Hash.new({})
=> {}
>> a[:a][:b] = 5
=> 5
>> a
=> {}
>> a[:a]
=> {:b=>5}
>> a.keys
=> []
>> a.size
=> 0
>> a[:a].size
=> 1
In other words, I don't see hash member when I inspect the hash, but I can access it by its key.
Is this expected behavior? What is going on here?
BTW, this is Ruby 1.9.1, haven't tried earlier versions.
Edit: simplified example as it doesn't have to be a hash of hashes of hashes...
It is expected behaviour (across all ruby versions). And if you experiment a bit further, you'll see that you always access the same hash, no matter which key you use:
>> a[:a][:b] = 1
=> 1
>> a[:c][:d] = 2
=> 2
>> a[:d]
=> {:b=>1, :d=>2}
The way Hash.new with a default argument works is: If you do hash[key] it checks whether that key exists in the hash. If it does, it returns the value for that key. If not it returns the default value. It does not add the key to the hash and it will return the same default object (not a copy) every time.
To get what you want, you want to specify a default block instead. That way, the block will be executed every time you access a key that is not in the hash. Inside the block you can create a new Hash and set the key to "point" to that hash. Like so:
Hash.new { |h,k| h[k] = {} }

Resources