Passing a method that takes arguments as an argument in Ruby - ruby

I'd like to calculate the difference for various values inside 2 hashes with the same structure, as concisely as possible. Here's a simplified example of the data I'd like to compare:
hash1 = {"x" => { "y" => 20 } }
hash2 = {"x" => { "y" => 12 } }
I have a very simple method to get the value I want to compare. In reality, the hash can be nested a lot deeper than these examples, so this is mostly to keep the code readable:
def get_y(data)
data["x"]["y"]
end
I want to create a method that will calculate the difference between the 2 values, and can take a method like get_y as an argument, allowing me to re-use the code for any value in the hash. I'd like to be able to call something like this, and I'm not sure how to write the method get_delta:
get_delta(hash1, hash2, get_y) # => 8

The "Ruby way" would be to pass a block:
def get_delta_by(obj1, obj2)
yield(obj1) - yield(obj2)
end
hash1 = {"x" => { "y" => 20 } }
hash2 = {"x" => { "y" => 12 } }
get_delta_by(hash1, hash2) { |h| h["x"]["y"] }
#=> 8
A method could be passed (indirectly) via:
def get_y(data)
data["x"]["y"]
end
get_delta_by(hash1, hash2, &method(:get_y))
#=> 8

Building on Stefan's response, if you want a more flexible get method you can actually return a lambda from the function and pass arguments for what you want to get. This will let you do error handling nicely:
Starting with the basics from above...
def get_delta_by(obj1, obj2)
yield(obj1) - yield(obj2)
end
hash1 = {"x" => { "y" => 20 } }
hash2 = {"x" => { "y" => 12 } }
get_delta_by(hash1, hash2) { |h| h["x"]["y"] }
Then we can define a get_something function which takes a list of arguments for the path of the element to get:
def get_something(*args)
lambda do |data|
args.each do |arg|
begin
data = data.fetch(arg)
rescue KeyError
raise RuntimeError, "KeyError for #{arg} on path #{args.join(',')}"
end
end
return data
end
end
Finally we call the function using the ampersand to pass the lambda as a block:
lambda_getter = get_something("x","y")
get_delta_by(hash1, hash2, &lambda_getter)
That last bit can be a one liner... but wrote it as two for clarity here.

In Ruby 2.3, you can use Hash#dig method, if it meets your needs.
hash1.dig("x", "y") - hash2.dig("x", "y")
#=> 8

Related

puppet - unexpected result from 'each' in a custom function

I have a simple function which takes a JSON and 'does something' with it. The main part works good BUT the function returns not only what I want but additionally the result of .each loop!
The code:
module Puppet::Parser::Functions
newfunction(:mlh, :type => :rvalue) do |args|
lvm_default_hash = args[0]
lvm_additional_hash = args[1]
if lvm_additional_hash.keys.length == 1
if lvm_additional_hash.keys.include? 'logical_volumes'
# do stuff - we have only 'logical_volumes'
lvm_default_hash.keys.each do |key|
pv_array = Hash['physical_volumes' => lvm_default_hash[key]['physical_volumes']]
lv_hash = lvm_default_hash[key]['logical_volumes']
new_lv_hash = lvm_additional_hash['logical_volumes']
merged_lv_hash = Hash['logical_volumes' => lv_hash.merge(new_lv_hash)]
# this is what I want to return to init.pp
puts Hash[key => pv_array.merge(merged_lv_hash)]
end
end
end
end
end
Variables in the init.pp are:
$default_volume_groups = {
'sys' => {
'physical_volumes' => [
'/dev/sda2',
],
'logical_volumes' => {
'root' => {'size' => '4G'},
'swap' => {'size' => '256M'},
'var' => {'size' => '8G'},
'docker' => {'size' => '16G'},
},
},
}
and the second argument from a hieradata:
modified_volume_groups:
logical_volumes:
cloud_log:
size: '16G'
In the init.pp I have something like this to test it:
notice(mlh($default_volume_groups, $modified_volume_groups))
which gives me a result:
syslogical_volumesvarsize8Gdockersize16Gcloud_logsize16Gswapsize256Mrootsize4Gphysical_volumes/dev/sda2
Notice: Scope(Class[Ops_lvm]): sys
The "long" part before the Notice is the proper result from the puts but the Notice: Scope(): sys is this what I do not want to!
I know that this is the result of this each loop over the default_volumes_groups:
lvm_default_hash.keys.each do |key|
# some stuff
end
How to block of this unwanted result? It blows my puppet's logic because my init.pp sees this sys and not what I want.
Does someone knows how to handle such problem?
Thank you!
I found how to handle this problem but maybe someone could explain me why it works in this way :)
This does not work (short version):
module Puppet::Parser::Functions
newfunction(:mlh, :type => :rvalue) do |args|
lvm_default_hash = args[0]
lvm_additional_hash = args[1]
if lvm_additional_hash.keys.length == 1
if lvm_additional_hash.keys.include? 'logical_volumes'
lvm_default_hash.keys.each do |key|
pv_array = Hash['physical_volumes' => lvm_default_hash[key]['physical_volumes']]
lv_hash = lvm_default_hash[key]['logical_volumes']
new_lv_hash = lvm_additional_hash['logical_volumes']
merged_lv_hash = Hash['logical_volumes' => lv_hash.merge(new_lv_hash)]
puts Hash[key => pv_array.merge(merged_lv_hash)]
end
end
end
end
end
but this works:
module Puppet::Parser::Functions
newfunction(:mlh, :type => :rvalue) do |args|
lvm_default_hash = args[0]
lvm_additional_hash = args[1]
# empty Hash
hash_to_return = {}
if lvm_additional_hash.keys.length == 1
if lvm_additional_hash.keys.include? 'logical_volumes'
lvm_default_hash.keys.each do |key|
pv_array = Hash['physical_volumes' => lvm_default_hash[key]['physical_volumes']]
lv_hash = lvm_default_hash[key]['logical_volumes']
new_lv_hash = lvm_additional_hash['logical_volumes']
merged_lv_hash = Hash['logical_volumes' => lv_hash.merge(new_lv_hash)]
# assigned value in the 'each' loop we want to return to puppet
hash_to_return = Hash[key => pv_array.merge(merged_lv_hash)]
end
# returned Hash - instead of previous 'puts'
return hash_to_return
end
end
end
end
Now I have what I need!
Notice: Scope(Class[Ops_lvm]): sysphysical_volumes/de
You've got it -- the first one doesn't work because in Ruby, the return value of a block or function is the last evaluated statement. In the case of the one that didn't work, the last evaluated statement was the .each. As it turns out, each evaluates to the enumerable that it was looping through.
A simple example:
def foo
[1, 2, 3].each do |n|
puts n
end
end
If I were to run this, the return value of the function would be the array:
> foo
1
2
3
=> [1, 2, 3]
So what you have works, because the last thing evaluated is return hash_to_return. You could even just go hash_to_return and it'd work.
If you wanted to get rid of the return and clean that up a little bit (and if you're using Ruby 1.9 or above), you could replace your each line with:
lvm_default_hash.keys.each_with_object({}) do |key, hash_to_return|
This is because each_with_object evaluates to the "object" (in this case the empty hash passed into the method, and referred to as hash_to_return in the block params). If you do this you can remove the return as well as the initialization hash_to_return = {}.
Hope this helps!
Your custom function has rvalue type which means it needs to return value. If you don't specify return <something> by default, your last statement is implicitly your return.
In the example above, first one that does not work correctly, has last statement inside each block:
puts Hash[key => pv_array.merge(merged_lv_hash)]
Your second example is correct simply because you set value for hash_to_return in each block and then "return" it outside of each block. Not sure if this is the behavior you want since last assigned hash value (in last loop inside each block) will be the one that will be returned from this function.

Safely assign value to nested hash using Hash#dig or Lonely operator(&.)

h = {
data: {
user: {
value: "John Doe"
}
}
}
To assign value to the nested hash, we can use
h[:data][:user][:value] = "Bob"
However if any part in the middle is missing, it will cause error.
Something like
h.dig(:data, :user, :value) = "Bob"
won't work, since there's no Hash#dig= available yet.
To safely assign value, we can do
h.dig(:data, :user)&.[]=(:value, "Bob") # or equivalently
h.dig(:data, :user)&.store(:value, "Bob")
But is there better way to do that?
It's not without its caveats (and doesn't work if you're receiving the hash from elsewhere), but a common solution is this:
hash = Hash.new {|h,k| h[k] = h.class.new(&h.default_proc) }
hash[:data][:user][:value] = "Bob"
p hash
# => { :data => { :user => { :value => "Bob" } } }
And building on #rellampec's answer, ones that does not throw errors:
def dig_set(obj, keys, value)
key = keys.first
if keys.length == 1
obj[key] = value
else
obj[key] = {} unless obj[key]
dig_set(obj[key], keys.slice(1..-1), value)
end
end
obj = {d: 'hey'}
dig_set(obj, [:a, :b, :c], 'val')
obj #=> {d: 'hey', a: {b: {c: 'val'}}}
interesting one:
def dig_set(obj, keys, value)
if keys.length == 1
obj[keys.first] = value
else
dig_set(obj[keys.first], keys.slice(1..-1), value)
end
end
will raise an exception anyways if there's no [] or []= methods.
I found a simple solution to set the value of a nested hash, even if a parent key is missing, even if the hash already exists. Given:
x = { gojira: { guitar: { joe: 'charvel' } } }
Suppose you wanted to include mario's drum to result in:
x = { gojira: { guitar: { joe: 'charvel' }, drum: { mario: 'tama' } } }
I ended up monkey-patching Hash:
class Hash
# ensures nested hash from keys, and sets final key to value
# keys: Array of Symbol|String
# value: any
def nested_set(keys, value)
raise "DEBUG: nested_set keys must be an Array" unless keys.is_a?(Array)
final_key = keys.pop
return unless valid_key?(final_key)
position = self
for key in keys
return unless valid_key?(key)
position[key] = {} unless position[key].is_a?(Hash)
position = position[key]
end
position[final_key] = value
end
private
# returns true if key is valid
def valid_key?(key)
return true if key.is_a?(Symbol) || key.is_a?(String)
raise "DEBUG: nested_set invalid key: #{key} (#{key.class})"
end
end
usage:
x.nested_set([:instrument, :drum, :mario], 'tama')
usage for your example:
h.nested_set([:data, :user, :value], 'Bob')
any caveats i missed? any better way to write the code without sacrificing readability?
Searching for an answer to a similar question I developmentally stumbled upon an interface similar to #niels-kristian's answer, but wanted to also support a namespace definition parameter, like an xpath.
def deep_merge(memo, source)
# From: http://www.ruby-forum.com/topic/142809
# Author: Stefan Rusterholz
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
memo.merge!(source, &merger)
end
# Like Hash#dig, but for setting a value at an xpath
def bury(memo, xpath, value, delimiter=%r{\.})
xpath = xpath.split(delimiter) if xpath.respond_to?(:split)
xpath.map!{|x|x.to_s.to_sym}.push(value)
deep_merge(memo, xpath.reverse.inject { |memo, field| {field.to_sym => memo} })
end
Nested hashes are sort of like xpaths, and the opposite of dig is bury.
irb(main):014:0> memo = {:test=>"value"}
=> {:test=>"value"}
irb(main):015:0> bury(memo, 'test.this.long.path', 'value')
=> {:test=>{:this=>{:long=>{:path=>"value"}}}}
irb(main):016:0> bury(memo, [:test, 'this', 2, 4.0], 'value')
=> {:test=>{:this=>{:long=>{:path=>"value"}, :"2"=>{:"4.0"=>"value"}}}}
irb(main):017:0> bury(memo, 'test.this.long.path.even.longer', 'value')
=> {:test=>{:this=>{:long=>{:path=>{:even=>{:longer=>"value"}}}, :"2"=>{:"4.0"=>"value"}}}}
irb(main):018:0> bury(memo, 'test.this.long.other.even.longer', 'other')
=> {:test=>{:this=>{:long=>{:path=>{:even=>{:longer=>"value"}}, :other=>{:even=>{:longer=>"other"}}}, :"2"=>{:"4.0"=>"value"}}}}
A more ruby-helper-like version of #niels-kristian answer
You can use it like:
a = {}
a.bury!([:a, :b], "foo")
a # => {:a => { :b => "foo" }}
class Hash
def bury!(keys, value)
key = keys.first
if keys.length == 1
self[key] = value
else
self[key] = {} unless self[key]
self[key].bury!(keys.slice(1..-1), value)
end
self
end
end

Can ruby deep search a hash/array for a particular key?

I have some ruby code that gets a json from Jenkins that contains an array of n items. The item I want has a key called "lastBuiltRevision"
I know I can loop through the array like so
actions.each do |action|
if action["lastBuiltRevision"]
lastSuccessfulRev = action["lastBuiltRevision"]["SHA1"]
break
end
end
but that feels very clunky and devoid of the magic that I usually feel when working with ruby.
I have only been tinkering with it for roughly a week now, and I feel that I may be missing something to make this easier/faster.
Is there such a thing? or is manual iteration all I can do?
I am kind of hoping for something like
lastSuccessfulRev = action.match("lastBuildRevision/SHA1")
Using Enumerable#find:
actions = [
{'dummy' => true },
{'dummy' => true },
{'dummy' => true },
{'lastBuiltRevision' => { "SHA1" => "123abc" }},
{'dummy' => true },
]
actions.find { |h|
h.has_key? 'lastBuiltRevision'
}['lastBuiltRevision']['SHA1']
# => "123abc"
UPDATE
Above code will throw NoMethodError if there's no matched item. Use follow code if you don't want get an exception.
rev = actions.find { |h| h.has_key? 'lastBuiltRevision' }
rev = rev['lastBuiltRevision']['SHA1'] if rev
Here's another way to do it, making use of the form of Enumerable#find that takes a parameter ifnone which is called, and its return value returned, if find's block never evaluates true.
I assume the method is to return nil if either key is not found.
Code
def look_for(actions, k1, k2)
actions.find(->{{k1=>{}}}) { |e| e.key?(k1) }[k1][k2]
end
Examples
actions = [{ 'dog'=>'woof' }, { 'lastBuiltRevision'=>{ 'SHA1'=> 2 } }]
look_for(actions, 'lastBuiltRevision', 'SHA1') #=> 2
look_for(actions, 'cat, 'SHA1') #=> nil
look_for(actions, 'lastBuiltRevision', 'cat') #=> nil
Explanation
I've made find's ifnone parameter the lambda:
l = ->{{k1=>{}}}
so that:
k1 = "cats"
h = l.call #=> {"cats"=>{}}
h[k1]['SHA1'] #=> {}['SHA1']
#=> nil
Try:
action.map { |a| a["lastBuiltRevision"] }.compact.map { |lb| lb["SHA1"] }.first

Easy way to parse hashes and arrays

Typically, parsing XML or JSON returns a hash, array, or combination of them. Often, parsing through an invalid array leads to all sorts of TypeErrors, NoMethodErrors, unexpected nils, and the like.
For example, I have a response object and want to find the following element:
response['cars'][0]['engine']['5L']
If response is
{ 'foo' => { 'bar' => [1, 2, 3] } }
it will throw a NoMethodError exception, when all I want is to see is nil.
Is there a simple way to look for an element without resorting to lots of nil checks, rescues, or Rails try methods?
Casper was just before me, he used the same idea (don't know where i found it, is a time ago) but i believe my solution is more sturdy
module DeepFetch
def deep_fetch(*keys, &fetch_default)
throw_fetch_default = fetch_default && lambda {|key, coll|
args = [key, coll]
# only provide extra block args if requested
args = args.slice(0, fetch_default.arity) if fetch_default.arity >= 0
# If we need the default, we need to stop processing the loop immediately
throw :df_value, fetch_default.call(*args)
}
catch(:df_value){
keys.inject(self){|value,key|
block = throw_fetch_default && lambda{|*args|
# sneak the current collection in as an extra block arg
args << value
throw_fetch_default.call(*args)
}
value.fetch(key, &block) if value.class.method_defined? :fetch
}
}
end
# Overload [] to work with multiple keys
def [](*keys)
case keys.size
when 1 then super
else deep_fetch(*keys){|key, coll| coll[key]}
end
end
end
response = { 'foo' => { 'bar' => [1, 2, 3] } }
response.extend(DeepFetch)
p response.deep_fetch('cars') { nil } # nil
p response.deep_fetch('cars', 0) { nil } # nil
p response.deep_fetch('foo') { nil } # {"bar"=>[1, 2, 3]}
p response.deep_fetch('foo', 'bar', 0) { nil } # 1
p response.deep_fetch('foo', 'bar', 3) { nil } # nil
p response.deep_fetch('foo', 'bar', 0, 'engine') { nil } # nil
I tried to look through both the Hash documentation and also through Facets, but nothing stood out as far as I could see.
So you might want to implement your own solution. Here's one option:
class Hash
def deep_index(*args)
args.inject(self) { |e,arg|
break nil if e[arg].nil?
e[arg]
}
end
end
h1 = { 'cars' => [{'engine' => {'5L' => 'It worked'}}] }
h2 = { 'foo' => { 'bar' => [1, 2, 3] } }
p h1.deep_index('cars', 0, 'engine', '5L')
p h2.deep_index('cars', 0, 'engine', '5L')
p h2.deep_index('foo', 'bonk')
Output:
"It worked"
nil
nil
If you can live with getting an empty hash instead of nil when there is no key, then you can do it like this:
response.fetch('cars', {}).fetch(0, {}).fetch('engine', {}).fetch('5L', {})
or save some types by defining a method Hash#_:
class Hash; def _ k; fetch(k, {}) end end
response._('cars')._(0)._('engine')._('5L')
or do it at once like this:
["cars", 0, "engine", "5L"].inject(response){|h, k| h.fetch(k, {})}
For the sake of reference, there are several projects i know of that tackle the more general problem of chaining methods in the face of possible nils:
andand
ick
zucker's egonil
methodchain
probably others...
There's also been considerable discussion in the past:
Ruby nil-like object - One of many on SO
Null Objects and Falsiness - Great article by Avdi Grimm
The 28 Bytes of Ruby Joy! - Very interesting discussion following J-_-L's post
More idiomatic way to avoid errors when calling method on variable that may be nil? on ruby-talk
et cetera
Having said that, the answers already provided probably suffice for the more specific problem of chained Hash#[] access.
I would suggest an approach of injecting custom #[] method to instances we are interested in:
def weaken_checks_for_brackets_accessor inst
inst.instance_variable_set(:#original_get_element_method, inst.method(:[])) \
unless inst.instance_variable_get(:#original_get_element_method)
singleton_class = class << inst; self; end
singleton_class.send(:define_method, :[]) do |*keys|
begin
res = (inst.instance_variable_get(:#original_get_element_method).call *keys)
rescue
end
weaken_checks_for_brackets_accessor(res.nil? ? inst.class.new : res)
end
inst
end
Being called on the instance of Hash (Array is OK as all the other classes, having #[] defined), this method stores the original Hash#[] method unless it is already substituted (that’s needed to prevent stack overflow during multiple calls.) Then it injects the custom implementation of #[] method, returning empty class instance instead of nil/exception. To use the safe value retrieval:
a = { 'foo' => { 'bar' => [1, 2, 3] } }
p (weaken_checks_for_brackets_accessor a)['foo']['bar']
p "1 #{a['foo']}"
p "2 #{a['foo']['bar']}"
p "3 #{a['foo']['bar']['ghgh']}"
p "4 #{a['foo']['bar']['ghgh'][0]}"
p "5 #{a['foo']['bar']['ghgh'][0]['olala']}"
Yielding:
#⇒ [1, 2, 3]
#⇒ "1 {\"bar\"=>[1, 2, 3]}"
#⇒ "2 [1, 2, 3]"
#⇒ "3 []"
#⇒ "4 []"
#⇒ "5 []"
Since Ruby 2.3, the answer is dig

Ruby function as value of hash

I was wondering if or how it is possible to map a function to a value of a hash.
For example:
----Start Class------------
def foo(var)
return var + 2
end
hash_var = { func => foo() }
----End Class--------------
so that I could later call
Class::hash_var["func"][10]
or
Class::hash_var["func"](10)
and that would return 12?
You could use method method.
def foo(var)
return var + 2
end
hash_var = { :func => method(:foo) }
hash_var[:func].call(10)
Functions/methods are one of the few things in Ruby that are not objects, so you can't use them as keys or values in hashes. The closest thing to a function that is an object would be a proc. So you are best off using these...
The other answers pretty much listed all possible ways of how to put a proc into a hash as value, but I'll summarize it nonetheless ;)
hash = {}
hash['variant1'] = Proc.new {|var| var + 2}
hash['variant2'] = proc {|var| var + 2}
hash['variant3'] = lambda {|var| var + 2}
def func(var)
var + 2
end
hash['variant4'] = method(:func) # the *method* method returns a proc
# describing the method's body
there are also different ways to evaluate procs:
hash['variant1'].call(2) # => 4
hash['variant1'][2] # => 4
hash['variant1'].(2) # => 4
You can set the value to a Proc and call it.
hash_var = {
'func' => proc {|x| x+2}
}
hash_var['func'].call(10) #=> 12
Try using lambdas
hash_var = { :func => lambda { |var| var + 2 }}
hash_var['func'].call(5) #=> 7
Another option would be:
def foo(name)
puts "Hi #{name}"
end
def bar(numb)
puts numb + 1
end
hash_var = { func: :foo, func2: :bar }
send hash_var[:func], "Grid"
# => Hi Grid
send hash_bar[:func2], 3
# => 4
Here is some information about the #send method What does send() do in Ruby?

Resources