Setting Ruby Hash .Default to a List

Setting ruby hash .default to a list

Hash.default is used to set the default value returned when you query a key that doesn't exist. An entry in the collection is not created for you, just because queried it.

Also, the value you set default to is an instance of an object (an Array in your case), so when this is returned, it can be manipulated.

a = {}
a.default = [] # set default to a new empty Array
a[8] << 9 # a[8] doesn't exist, so the Array instance is returned, and 9 appended to it
a.default # => [9]
a[9] # a[9] doesn't exist, so default is returned

In Ruby, how to set a default value for a nested hash?

Apparently I only had to do:

hash = Hash.new { |h,k| h[k] = Hash.new(0) }

Whoops. I'll try not to be so hasty to ask a question next time.

How to get a default value with hashes in ruby

You just need to give fetch a default it can handle. It doesn't know what to do with january as you haven't declared any variable with that name. If you want to set the default value to the string "january", then you just need to quote it like this:

cohort.fetch(:cohort, "january") 

There are some decent examples in the documentation for fetch.

Also, cohort isn't a Hash, it's a String since gets.chomp returns a String. fetch is for "fetching" values from a Hash. The way you're using it should be throwing an error similar to: undefined method 'fetch' for "whatever text you entered":String.

Finally, since you're using it in a conditional, the result of your call to fetch is being evaluated for its truthiness. If you're setting a default, it will always be evaluated as true.

If you just want to set a default for cohort if it's empty, you can just do something like this:

cohort = gets.chomp
cohort = "january" if cohort.empty?
while !name.empty? && !hobbies.empty? && !country.empty?
students << {
name: name,
hobbies: hobbies,
country: country,
cohort: cohort
}
... # do more stuff

Hope that's helpful.

Declaring a Hash as a Constant with a default value in Ruby in one line

You can use update:

MY_HASH = Hash.new(-1).update(1 => 0, 2 => 42)
MY_HASH[1]
#=> 0
MY_HASH[52]
#=> -1

Or you can use Hash#merge.

Ruby Hash initialization (default value nil)

Try passing a default value to your new hash as such

numbers = Hash.new(0)

How can I initialize an Array inside a Hash in Ruby

@my_hash = Hash.new(Array.new)

This creates exactly one array object, which is returned every time a key is not found. Since you only ever mutate that array and never create a new one, all your keys map to the same array.

What you want to do is:

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

or simply

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

Passing a block to Hash.new differs from simply passing an argument in 2 ways:

  1. The block is executed every time a key is not found. Thus you'll get a new array each time. In the version with an argument, that argument is evaluated once (before new is called) and the result of that is returned every time.

  2. By doing h[k] = you actually insert the key into the hash. If you don't do this just accessing @my_hash[some_key] won't actually cause some_key to be inserted in the hash.

Strange, unexpected behavior (disappearing/changing values) when using Hash default value, e.g. Hash.new([])

First, note that this behavior applies to any default value that is subsequently mutated (e.g. hashes and strings), not just arrays. It also applies similarly to the populated elements in Array.new(3, []).

TL;DR: Use Hash.new { |h, k| h[k] = [] } if you want the most idiomatic solution and don’t care why.



What doesn’t work

Why Hash.new([]) doesn’t work

Let’s look more in-depth at why Hash.new([]) doesn’t work:

h = Hash.new([])
h[0] << 'a' #=> ["a"]
h[1] << 'b' #=> ["a", "b"]
h[1] #=> ["a", "b"]

h[0].object_id == h[1].object_id #=> true
h #=> {}

We can see that our default object is being reused and mutated (this is because it is passed as the one and only default value, the hash has no way of getting a fresh, new default value), but why are there no keys or values in the array, despite h[1] still giving us a value? Here’s a hint:

h[42]  #=> ["a", "b"]

The array returned by each [] call is just the default value, which we’ve been mutating all this time so now contains our new values. Since << doesn’t assign to the hash (there can never be assignment in Ruby without an = present), we’ve never put anything into our actual hash. Instead we have to use <<= (which is to << as += is to +):

h[2] <<= 'c'  #=> ["a", "b", "c"]
h #=> {2=>["a", "b", "c"]}

This is the same as:

h[2] = (h[2] << 'c')

Why Hash.new { [] } doesn’t work

Using Hash.new { [] } solves the problem of reusing and mutating the original default value (as the block given is called each time, returning a new array), but not the assignment problem:

h = Hash.new { [] }
h[0] << 'a' #=> ["a"]
h[1] <<= 'b' #=> ["b"]
h #=> {1=>["b"]}


What does work

The assignment way

If we remember to always use <<=, then Hash.new { [] } is a viable solution, but it’s a bit odd and non-idiomatic (I’ve never seen <<= used in the wild). It’s also prone to subtle bugs if << is inadvertently used.

The mutable way

The documentation for Hash.new states (emphasis my own):

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.

So we must store the default value in the hash from within the block if we wish to use << instead of <<=:

h = Hash.new { |h, k| h[k] = [] }
h[0] << 'a' #=> ["a"]
h[1] << 'b' #=> ["b"]
h #=> {0=>["a"], 1=>["b"]}

This effectively moves the assignment from our individual calls (which would use <<=) to the block passed to Hash.new, removing the burden of unexpected behavior when using <<.

Note that there is one functional difference between this method and the others: this way assigns the default value upon reading (as the assignment always happens inside the block). For example:

h1 = Hash.new { |h, k| h[k] = [] }
h1[:x]
h1 #=> {:x=>[]}

h2 = Hash.new { [] }
h2[:x]
h2 #=> {}

The immutable way

You may be wondering why Hash.new([]) doesn’t work while Hash.new(0) works just fine. The key is that Numerics in Ruby are immutable, so we naturally never end up mutating them in-place. If we treated our default value as immutable, we could use Hash.new([]) just fine too:

h = Hash.new([].freeze)
h[0] += ['a'] #=> ["a"]
h[1] += ['b'] #=> ["b"]
h[2] #=> []
h #=> {0=>["a"], 1=>["b"]}

However, note that ([].freeze + [].freeze).frozen? == false. So, if you want to ensure that the immutability is preserved throughout, then you must take care to re-freeze the new object.



Conclusion

Of all the ways, I personally prefer “the immutable way”—immutability generally makes reasoning about things much simpler. It is, after all, the only method that has no possibility of hidden or subtle unexpected behavior. However, the most common and idiomatic way is “the mutable way”.

As a final aside, this behavior of Hash default values is noted in Ruby Koans.


This isn’t strictly true, methods like instance_variable_set bypass this, but they must exist for metaprogramming since the l-value in = cannot be dynamic.

Ruby hash vivification weirdness

Apologies for answering my own question, but I figured out what is happening.

The answer here is that we are assigning into the default hash being returned by a[:b], NOT a[:b] directly.

As before, we're going to create a hash with a single key of b and a value of Hash.new({})

irb(main):068:0> a = { b: Hash.new({}) }
=> {:b=>{}}

As you might expect, this should make things like a[:b][:unknown_key] return an empty hash {}, like so:

irb(main):070:0> a[:b].default
=> {}
irb(main):071:0> a[:b][:unknown_key]
=> {}
irb(main):072:0> a[:b].object_id
=> 70127981905400
irb(main):073:0> a[:b].default.object_id
=> 70127981905420

Notice that the object_id for a[:b] is ...5400 while the object_id for a[:b].default is ...5420

So what happens when we do the assignment from the original question?

a[:b][:c]["foo"] = "bar"

First, a[:b][:c] is resolved:

irb(main):075:0> a[:b][:c].object_id
=> 70127981905420

That's the same object_id as the .default object, because :c is treated the same as :unknown_key from above!

Then, we assign a new key 'foo' with a value 'bar' into that hash.

Indeed, check it out, we've effectively altered the default instead of a[:b]:

irb(main):081:0> a[:b].default
=> {"foo"=>"bar"}

Oops!

Why is only one value in my hash being changed?

When I run your code I get this as the result.

{:acrobatics=>nil, :athletics=>nil, :engineering=>nil, :endurance=>2, :heal=>nil, :history=>nil, :influence=>nil, :insight=>nil, :magicka=>nil, :perception=>nil, :riding=>nil, :stealth=>nil, :streetwise=>nil, :thievery=>nil}

That's because map returns last statement executed. In addition you actually only set a value for skill when it's matches the sub skill otherwise, it is set to nil.

So whats happening in your code is that each iteration is returning the following which is the result of the last statement in the block passed into map.

[:acrobatics, nil]
[:athletics, nil]
[:engineering, nil]
[:endurance, 2]
[:heal, nil]
[:history, nil]
[:influence, nil]
[:insight, nil]
[:magicka, nil]
[:perception, nil]
[:riding, nil]
[:stealth, nil]
[:streetwise, nil]
[:thievery, nil]

The final result being an array that looks like this.

[[:acrobatics, nil], [:athletics, nil], [:engineering, nil], [:endurance, 2], [:heal, nil], [:history, nil], [:influence, nil], [:insight, nil], [:magicka, nil], [:perception, nil], [:riding, nil], [:stealth, nil], [:streetwise, nil], [:thievery, nil]]

Which is finally mapped to a new hash

{:acrobatics=>nil, :athletics=>nil, :engineering=>nil, :endurance=>2, :heal=>nil, :history=>nil, :influence=>nil, :insight=>nil, :magicka=>nil, :perception=>nil, :riding=>nil, :stealth=>nil, :streetwise=>nil, :thievery=>nil}

The reason you get all those nil's is because in your statements the result of the case were the if statement is not true is nil.
For example:

[skill (mod += 2 if skill.to_s == player[:caste][:skill])]

will return [the_skill, nil] for the cases were skill.to_s == player[:caste][:skill] is not true

To see what's happening try this in irb.

x = 0
=> 0
x += 1 if false
=> nil
x += 1 if true
=> 1

You could get past that using something like this.

[skill, skill.to_s == player[:caste][:skill] ? mod + 2 : mod ]

or using the above example:

x = 0
=> 0
x = false ? x + 1 : x
=> 0
x = true ? x + 1 : x
=> 1

The following modified version of your code should work.

player[:skills] = player[:skills].map do |skill, mod|
[skill, skill.to_s == player[:caste][:skill] || skill.to_s == player[:sub][:skill] ? mod + 2 : mod ]
end.to_h

However, here is a slightly more verbose, but hopefully much easier to follow way to accomplish what you want to do and allows for added modifications in the future with out the code getting too confusing.

player = {
caste: {skill: "athletics"},
sub: {skill: "endurance"},
skills: {acrobatics: 0, athletics: 0, engineering: 0, endurance: 0, heal: 0, history: 0, influence: 0, insight: 0, magicka: 0, perception: 0, riding: 0, stealth: 0, streetwise: 0, thievery: 0},
}

player_caste_skill = player[:caste][:skill]
player_sub_skill = player[:sub][:skill]
current_skills = player[:skills]
updated_skills = {}
current_skills.each_pair do |skill, prev_value|
new_value = prev_value
case skill.to_s
when player_caste_skill, player_sub_skill
new_value = prev_value + 2
when "some_other_skill"
new_value = prev_value + 3
end
updated_skills[skill] = new_value
end
puts current_skills
puts updated_skills


Related Topics



Leave a reply



Submit