Is there a way to initialize hash's values lazily? - ruby

I have a hash like:
h = {
a: '/users/sign_up',
b: "/user/#{#user.id]}"
}
Later I do h[:b].
Hash values are initialized when hash itself is initialized. But I'd want #user.id to be invoked every time when h[:b] is invoked.
It seems to be not possible to do it with Ruby's hash. But is there some workaround to do it?

You can use lambdas for the values of the hash, and call the lambda when the actual value is needed, e.g.:
h = {
a: ->{'/users/sign_up'},
b: ->{"/user/#{#user.id}"}
}
h[:b].call

h = {}
h.default_proc = proc do |hash, key|
key == :b ? "/user/#{#user.id}" : nil
end
h[:a] #=> nil
h[:b] #=> "/user/<id>"

Related

What is an elegant Ruby way to have a condition of 'nil'?

Is there a more elegant way to write this code?
def create_a_hash_from_a_collection
my_hash = {}
collection_of_hashes.each do |key, value|
my_hash[key] = {} if my_hash[key].nil?
my_hash[key] = value
end
my_hash
end
The line that seems clumsy to me is this:
my_hash[key] = {} if my_hash[key].nil?
Is there a shorthand way of expressing it?
If you want to have a brand new hash for each key in your initial hash you need to initialise it with a block:
hash = Hash.new { |hash, key| hash[key] = {} }
hash[:foo].object_id == hash[:bar].object_id #=> false
Otherwise, if you do this, It will be always the same default hash
hash = Hash.new({})
hash[:foo].object_id == hash[:bar].object_id #=> true
You can use ||= operator which does exactly what you want
my_hash[key] ||= {}
The rest of my answer here is because I'm not sure what a "collection of hashes" is, so my best guess is that it would be an array of hashes. If I'm wrong, let me know and disregard the rest of this answer.
It seems that the rest of your method may not do what it sounds like you're trying to do. Consider:
#collection_of_hashes = [{foo: 'bar'}, {baz: 'qux'}]
def create_a_hash_from_a_collection
my_hash = {}
#collection_of_hashes.each do |key, value|
# this is not actually doing anything here and returns same with or
# without the following line
# my_hash[key] ||= {}
my_hash[key] = value
end
my_hash
end
#=> {{:foo=>"bar"}=>nil, {:baz=>"qux"}=>nil}
But what you probably want is
def create_a_hash_from_a_collection
my_hash = {}
#collection_of_hashes.each do |hash|
hash.keys.each do |k|
my_hash[k] = hash[k]
end
end
my_hash
end
#=> {:foo=>"bar", :baz=>"qux"}
But also keep in mind, if any of your "collection of hashes" which we would tend to assume would be an array of hashes, contain the same key, which one wins? This code, it would be the last item in the array's key value. What is the actual goal of your method?
I guess what you want is to initialise your my_hash with a default value, so then you don't need to check if it's nil or not.
That can be done using the Hash.new constructor, compare:
my_hash = {}
puts my_hash['no_existing_key'] #=> nil
my_hash = Hash.new({})
puts my_hash['no_existing_key'] #=> {}
You then can reduce your code to:
def create_a_hash_from_a_collection
my_hash = Hash.new({})
collection_of_hashes.each do |key, value|
my_hash[key] = value
end
my_hash
end
Since you are assigning value anyway, maybe you could use my_hash[key] = value || {}?
So, if value to assign is nil, the value of that key becomes {}.

How to assign to deeply nested Hash without using many 'nil' guards

I have a nested hash, to which I need to add more deeply nested property/value pairs.
Sample A:
a = {}
a['x']['y']['z'] << 8
Normally I'd have to do this:
Sample B:
a = {}
a['x'] ||= a['x'] = {}
a['x']['y'] ||= a['x']['y'] = {}
a['x']['y']['z'] ||= a['x']['y']['z'] = []
Otherwise, I will get undefined method '<<' for nil:NillClass.
Is there some type of shorthand or function along the lines of code A instead of code B?
The most elegant solution for the deep-nested hash of any depth would be:
hash = Hash.new { |h, k| h[k] = h.dup.clear }
or, even better (credits to #Stefan)
hash = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
That way one might access any level:
hash[:a1][:a2][:a3][:a4] = :foo
#⇒ {:a1=>{:a2=>{:a3=>{:a4=>:foo}}}}
The idea is to clone the default_proc within the hash itself.
To give a little backgroud, there's a method Hash#dig which is present since ruby 2.3. With this you can safely attempt to read any number of keys:
{a: {b: {c: 1}}}.dig :a, :b, :c # => 1
{}.dig :a, :b, :c # => nil
of course, this doesn't solve your problem. You're looking for a write version. This has been proposed but rejected in Ruby core in the form of Hash#bury.
This method does almost exactly what you are looking for, however it can only set nested hash values and not append to nested arrays:
# start with empty hash
hash = {}
# define the inner array
hash.bury :x, :y, :z, []
# add to the inner array
hash[:x][:y][:z] << :some_val
You can get this method through the ruby-bury gem, or alternatively you can take their implementation from their source code
Here are a couple of ways that could be done.
arr = [:a1, :a2, :a3, :a4, :foo]
Use Enumerable#reduce (aka inject)
def hashify(arr)
arr[0..-3].reverse_each.reduce(arr[-2]=>arr[-1]) { |h,x| { x=>h } }
end
hashify(arr)
#=> {:a1=>{:a2=>{:a3=>{:a4=>:foo}}}}
Use recursion
def hashify(arr)
first, *rest = arr
rest.size == 1 ? { first=>rest.first } : { first=>hashify(rest) }
end
hashify(arr)
#=> {:a1=>{:a2=>{:a3=>{:a4=>:foo}}}}
You can consider using the get and set methods from the rodash gem in order to set a deeply nested value into a hash with a default value.
require 'rodash'
a = {}
key = ['x', 'y', 'z']
default_value = []
value = 8
current_value = Rodash.get(a, key, default_value)
Rodash.set(a, key, current_value << value)
a
# => {"x"=>{"y"=>{"z"=>[8]}}}
I use the following to deeply set/initialize arrays/hashes using a list of keys. If they keys are ints, it assumes indexing into an array, otherwise it assumes a hash:
def deep_set(target, path, value)
key = path.shift
return target[key] = value if path.empty?
child = target[key]
return deep_set(child, path, value) if child
deep_set(
target[key] = path[0].is_a?(Integer) ? [] : {},
path,
value,
)
end
Here is an example of using it:
target = {first: [:old_data]}
deep_set(target, [:first, 1, 1, :lol], 'foo')
puts target # {:first=>[:old_data, [nil, {:lol=>"foo"}]]}
It uses the fact that ruby allows you to set-and-expand arrays on the fly, which is a bit wonky, but works nicely here.

#dig wrapper method to accept a dynamic number of variables

I'm trying to write a method that accepts an unknown number of arguments and performs a Hash#dig on them.
def unknown_dig(hash, *args)
# do some magic?
hash.dig(non_array_args)
end
#example usage
unknown_dig(hash, 'a', 'b', 'c')
Is this possible?
Hash#dig was bestowed upon us in Ruby v2.3. To support earlier Ruby versions you can use Enumerable#reduce (aka inject). This is how we did it when I was a kid.
def dig_it(h,*keys)
keys.reduce(h) { |obj,k| obj && obj[k] }
end
h = { a: { b: 1 } }
dig_it(h, :a, :b)
#=> 1
dig_it(h, :a)
#=> {:b=>1}
dig_it(h, :a, :c)
#=> nil
dig_it(h, :c, :b)
#=> nil
If obj is a hash, as it is initially (h), when k is passed to the block obj[k] #=> nil if obj does not have a key k (or if obj has a key k whose value if nil), in which case obj && obj[k] #=> obj && nil #=> nil. The block calculation will therefore be obj && obj[k] #=> nil && obj[k] #=> nil for each of the remaining elements of keys that are passed to the block. (nil[k] would raise an exception but it is never executed.) If the hash obj has a key k with value false, the same outcome will result except false (rather than nil) will be returned.
Hash#dig (defined in Ruby 2.3) already does this:
hash = { a: { b: 1 } }
hash.dig(:a, :b) == hash.dig(*[:a, :b])
If you want to it more functional style (where you pass the hash as an argument instead of calling the method on it), it's easy:
def hash_dig(hash, *args)
hash.dig(*args)
end

Add something to hash value if key exists?

I have a Hash in Ruby:
hash = Hash.new
It has some key value pairs in it, say:
hash[1] = "One"
hash[2] = "Two"
If the hash contains a key 2, then I want to add "Bananas" to its value. If the hash doesn't have a key 2, I want to create a new key value pair 2=>"Bananas".
I know I can do this by first checkng whether the hash has the key 2 by using has_key? and then act accordingly. But this requires an if statement and more than one line.
So is there a simple, elegant one-liner for achieving this?
This works:
hash[2] = (hash[2] || '') + 'Bananas'
If you want all keys to behave this way, you could use the "default" feature of Ruby's Hash:
hash = {}
hash.default_proc = proc { '' }
hash[2] += 'Bananas'
You could set the default value of the hash to an empty string, then make use of the << operator to concat whatever new values are passed:
h = Hash.new("")
#=> {}
h[2] << "Bananas"
#=> "Bananas"
h
#=> {2=>"Bananas"}
h[2] << "Bananas"
#=> "BananasBananas"
Per #rodrigo.garcia's comment, another side effect of this approach is that Hash.new() sets the default return value for the hash (which may or may not be what you want). In the example above, that default value is an empty string, but it doesn't have to be:
h2 = Hash.new(2)
#=> {}
h2[5]
#=> 2
(hash[2] ||= "").concat("Bananas")
Technically, both your roads lead to the same place. So Hash[2] = "bananas" produces the same result as first checking the hash for key 2. However, if you actually need the process of checking the hash for some reason a way to do that is use the .has_key? method, and a basic if conditional.
Suppose there is a hash,
`Hash = { 1 => "One", 2 => "Two" }`
setup a block of code based on the truth-value of a key search,
if hash.has_key?(2) == true
hash[2] = "bananas"
else
hash[2] = "bananas"
end
or more simply,
hash.has_key?(2) == true ? hash[2] = "bananas" : hash[2] = "bananas"
You can initialise the hash with a block and then directly concatenate:
hash = Hash.new {|hash, key| hash[key] = ""}
hash[1] << "One"
hash[2] << "Two"
hash[2] << "Bananas"
{1=>"One", 2=>"TwoBananas"}

How do I add a key/value pair to the beginning of a hash?

My code is:
hash = { two: 2, three: 3 }
def hash_add(hash, new_key, new_value)
temp_hash = {}
temp_hash[new_key.to_sym] = new_value
temp_hash.merge!(hash)
hash = temp_hash
puts hash
end
hash_add(hash, 'one', 1)
Within the method, puts hash returns { :one => 1, :two => 2, :three => 3 }, but when hash1 is put to the method, it remains unchanged afterward. It's like the assignment isn't carrying itself outside of the function.
I guess I could return the updated hash and set the hash I want to change to it outside the method:
hash = hash_add(hash, 'one', 1)
But I just don't see why the assignment I give to the hash does not stick outside of the method.
I have this, which works:
def hash_add(hash, new_key, new_value)
temp_hash = {}
temp_hash[new_key.to_sym] = new_value
temp_hash.merge!(hash)
hash.clear
temp_hash.each do |key, value|
hash[key] = value
end
end
Which gives me what I'm wanting when this method is called, but it just seems a little excessive to have to rebuild the hash like that.
How about this?
hash1 = { two: 2, three: 3 }
#add a new key,value
hash1 = Hash[:one,1].merge!(hash1) #=> {:one=>1, :two=>2, :three=>3}
Example #2:
h = { two: 2, three: 3 }
def hash_add(h,k,v)
Hash[k.to_sym,v].merge!(h)
end
h = hash_add(h, 'one', 1) #=> {:one=>1, :two=>2, :three=>3}
Ruby passes objects to methods by value, but the value is the reference to the object, so when you set hash=temp_hash within the add_hash method, that change only applies inside the method. The value of hash outside the method is unchanged.
def hash_add(hash, new_key, new_value)
temp_hash = {}
temp_hash[new_key.to_sym] = new_value
temp_hash.merge!(hash)
hash = temp_hash
hash
end
h2 = hash_add(hash, 'one', 1)
hash
=> {:two=>2, :three=>3}
h2
=>{:one=>1, :two=>2, :three=>3}
If you want hash to be updated, you need to replace the contents of hash rather than re-point hash at a new object as you did with the clear and re-adding the values. You can also do it with the replace method.
def hash_add(hash, new_key, new_value)
temp_hash = {}
temp_hash[new_key.to_sym] = new_value
temp_hash.merge!(hash)
hash.replace temp_hash
end
There are some good diagrams about pass by value in "Is Ruby pass by reference or by value?"
NOTE: this answer is old from times when Ruby 1.8 was still around.
In general, the class Hash in Ruby does not provide ordering. Behavior might differ between Ruby versions / implementations.
See also: Hash ordering preserved between iterations if not modified?
If you want ordering, you need to use the class OrderedHash which is provided through ActiveSupport
See: http://apidock.com/rails/ActiveSupport/OrderedHash
At the end of the function you are just putsing the hash, not returning it. Perhaps if you changed puts hash to return hash it would work (I haven't tried it myself).
temp_hash is a local variable, which gets deleted once the function returns.

Resources