Modifying the default hash value
Hash's default value doesn't work like you're expecting it to. When you say h[k]
, the process goes like this:
- If we have a
k
key, return its value. - If we have a default value for the Hash, return that default value.
- If we have a block for providing default values, execute the block and return its return value.
Note that (2) and (3) say nothing at all about inserting k
into the Hash. The default value essentially turns h[k]
into this:
h.has_key?(k) ? h[k] : the_default_value
So simply accessing a non-existant key and getting the default value back won't add the missing key to the Hash.
Furthermore, anything of the form:
Hash.new([ ... ])
# or
Hash.new({ ... })
is almost always a mistake as you'll be sharing exactly the same default Array or Hash for for all default values. For example, if you do this:
h = Hash.new(['a'])
h[:k].push('b')
Then h[:i]
, h[:j]
, ... will all return ['a', 'b']
and that's rarely what you want.
I think you're looking for the block form of the default value:
h = Hash.new { |h, k| h[k] = [ 'alright' ] }
That will do two things:
- Accessing a non-existent key will add that key to the Hash and it will have the provided Array as its value.
- All of the default values will be distinct objects so altering one will not alter the rest.
Change default hash function for block hashing
I found the solution.
It IS POSSIBLE to do so, it can be done by replacing hashing function in
protos/common/block.go
file, as i said in my post ;)
Working with Hashes that have a default value
0 will be the fallback if you try to access a key in the hash that doesn't exist
For example:
count = Hash.new
-> count['key'] => nil
vs
count = Hash.new(0)
-> count['key'] => 0
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
Default value is the same object in hash data type in ruby
hash = Hash.new([])
This statement creates an empty hash that has a default value of an empty array. If hash
does not have a key k
, hash[k]
returns the default value, []
. This is important: simply returning the default value does not modify the hash.
When you write:
hash[:one] << "uno" #=> ["uno"]
before hash
has a key :one
, hash[:one]
is replaced by the default value, so we have:
[] << "uno" #=> ["uno"]
which explains why hash
is not changed:
hash #=> {}
Now write:
hash[:two] << "dos" #=> ["uno", "dos"]
hash #=> {}
To see why we get this result, let's re-write the above as follows:
hash = Hash.new([]) #=> {}
hash.default #=> []
hash.default.object_id #=> 70209931469620
a = (hash[:one] << "uno") #=> ["uno"]
a.object_id #=> 70209931469620
hash.default #=> ["uno"]
b = (hash[:two] << "dos") #=> ["uno", "dos"]
b.object_id #=> 70209931469620
hash.default #=> ["uno", "dos"]
So you see that the default array whose object_id
is 70209931469620
is the single array to which "uno" and "dos" are appended.
If we had instead written:
hash[:one] = hash[:one] << "uno"
#=> hash[:one] = [] << "uno" => ["uno"]
hash #=> { :one=>"uno" }
we get what we were hoping for, but not so fast:
hash[:two] = hash[:two] << "dos"
#=> ["uno", "dos"]
hash
#=> {:one=>["uno", "dos"], :two=>["uno", "dos"]}
which is still not what we want because both keys have values that are the same array.
hash = Hash.new {|hash, key| hash[key] = [] }
This statements causes the block to be executed when hash
does not have a key key
. This does change the hash, by adding a key value pair1.
So now:
hash[:one] << "uno"
causes the block:
{ |h,k| h[k] = [] }
to make the assignment:
hash[:one] = []
after which:
hash[:one] << "uno"
appends "uno"
to an empty array that is the value for the key :one
, which we can verify:
hash #=> { :one=>"uno" }
This has the same effect as writing:
hash[:one] = (hash[:one] || []) << "uno"
(the expanded version of (hash[:one] ||= []) << "uno"
) when there is no default value.
Similarly,
hash[:two] << "dos" #=> ["dos"]
hash #=> {:one=>["uno"], :two=>["dos"]}
which is usually the result we want.
1 The block need not change the hash, however. The block can contain any code, including, for example, { puts "Have a nice day" }
.
Modifying reference to hash value in Ruby
OK, this has been an excellent learning exercise!
The problem I was having was in two parts:
the JSON output from
JSON.parse
was a Hash, but the Hash was storing Arrays, so my code was breaking. Looking at the sample data rows above, it includes some empty arrays:... [],[],[] ...
.I misunderstood how each was working with a Hash, I assumed
key, value
(similar to jquery each) but thekey, value
in the original each statement actually evaluated to the first two array elements.
So here is my amended code:
data['cachedBook']['rows'].map! { |row|
fullname = Faker::Name.name
namea = fullname.split(' ')
row.each { |val|
if val.class == Hash
newval = val.clone
if ["Ms.", "Mr.", "Dr.", "Miss", "Mrs."].any? { |needle| fullname.include? needle }
if val.key?("c") && val["c"] == "LN"
newval["v"] = namea[1]
newval["sortval"] = namea[1]
end
if val.key?("c") && val["c"] == "FN"
newval["v"] = namea[2]
newval["sortval"] = namea[2]
end
else
if val.key?("c") && val["c"] == "LN"
newval["v"] = namea[0]
newval["sortval"] = namea[0]
end
if val.key?("c") && val["c"] == "FN"
newval["v"] = namea[1]
newval["sortval"] = namea[1]
end
end
val.merge!(newval)
end
}
}
rails nested hash with default values
The doc for Hash::new explains the three ways of initializing a hash and, in your case, you are using an object in the Hash constructor:
If obj is specified, this single object will be used for all default values.
If you want that each missing key creates it's own object, create the hash with a block, like this:
h = Hash.new { |h,k| h[k] = { count: 0, rating: 0 } }
Then:
2.6.3 :012 > h
=> {}
2.6.3 :013 > h['a'][:count] = 5
=> 5
2.6.3 :015 > h
=> {"a"=>{:count=>5, :rating=>0}}
Hash default-value creates all of the same instance
If you give Hash.new
an object (like another Hash.new
) this very object is the default value. It is shared across different keys.
default = []
hash = Hash.new(default)
hash[:one] << 1
# now default is [1] !
You want to use Hash.new with a block, so that something new happens each time a key was not found.
Like
h = Hash.new { |hash,new_key| hash[new_key] = {} }
There is great explanation about that e.g. in Ruby hash default value behavior . Your question is kind of a duplicate.
Also, as we figured out in the comments, you probably wanted to compare the object_id
and not the hash
value.
first_hash = {}
second_hash = {}
# .hash same, but different objects!
puts "initial, hash:"
puts first_hash.hash == second_hash.hash ? " - same" : " - differs"
puts "initial, object_id"
puts first_hash.object_id == second_hash.object_id ? " - same" : " - differs"
puts
# Change the world
# .hash different, and still different objects.
first_hash[:for] = "better"
puts "better world now, hash:"
puts first_hash.hash == second_hash.hash ? " - same" : " - differs"
puts "better world now, object_id"
puts first_hash.object_id == second_hash.object_id ? " - same" : " - differs"
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.
Related Topics
Why Does a Rails App on Heroku Serve Assets via All.CSS and Locally via Individual Files
Rails Shows "Warning: Can't Verify Csrf Token Authenticity" from a Restkit Post
Undefined Local Variable Based on Syntax in Ruby
Why Don't Numbers Support .Dup
How to Upload a Text File and Parse Contents into Database in Ror
What Does the Term "Vendoring" or "To Vendor" Mean for Ruby on Rails
How to Preload Concerns in a Rails Initializer Using Rails 6/Zeitwerk
Paperclip Error: Model Missing Required Attr_Accessor for 'Avatar_File_Name'
How to Get a Linux Command Output to Chef Attribute
Regular Expression "Empty Range in Char Class Error"
Enable Dropping a File Onto a Ruby Script
Generate Nested Hashes from Strings and Deep Merging in Ruby
Error Using PHPstorm's SASS File Watcher
How to Convert Any Method to Infix Operator in Ruby
How to Make Id a Random 8 Digit Alphanumeric in Rails