How to prevent auto expansion of hash arguments? - ruby

Why does Ruby 2.3.1 auto-expand my hash argument when I don't provide an optional named argument (:to) and how do I prevent that?
def qsend(*args, to: nil, **opts)
puts "args: #{args}, opts: #{opts}"
end
qsend({ a: 1, b: 2, c: 3 })
qsend({ a: 1, b: 2, c: 3, d: 4 }, to: "me")
Output:
args: [], opts: {:a=>1, :b=>2, :c=>3}
args: [{:a=>1, :b=>2, :c=>3, :d=>4}], opts: {}
When I remove args from the argument list, my hash is not dropped into opts in the 2nd call, but an error is raised: "wrong number of arguments (given 1, expected 0) (ArgumentError)". And when I remove opts from the argument list, my hash is not dropped into args in the 1st call - an error is being thrown again, this time: "unknown keywords: a, b, c (ArgumentError)". Using args = {} instead of *args makes no difference.
The following call works (hash in 1st call will be dropped into args), but is there no better way?
qsend({ a: 1, b: 2, c: 3 }, {})
I would prefer to solve the problem in the method definition.
edit: method renamed, output fixed.

Related

Destructure a Hash in block arguments in Ruby 2.7

This:
[{a: 1, b: 2}, {a: 3, b: 4}].each do |a:, b:| p a end
Raises the following warning in Ruby 2.7
warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
I understand that each is passing a hash to the block, and the block now accepts |a:, b:| as named arguments but, is there any way to correctly destructure the hash in this context?
I'm uncertain, but I think perhaps the idea is to use pattern matching for hash destructuring? For example:
{a: 1, b: 2}.tap do |args|
args in {a: a, b: b} # !!!
p a
end
Currently by default however, this will display a warning (which can be disabled):
Pattern matching is experimental, and the behavior may change in future versions of Ruby!
If you already know that you have two keys in each Hash as per your example, why not this?
[{a: 1, b: 2}, {a: 3, b: 4}].each do |h|
a, b = h.values
p a
end
In Ruby 3 you can use the rightward assignment operator =>:
{a: 1, b: 2}.tap do |args|
args => { a:, b: }
p a
end

How use dig method several for children object

I have an hash
{:result=>
{:"1"=>
[{:"1"=>1,
:"2"=>"4698192612070913717",
:"5"=>
{:"1"=>{:"1"=>"1.0.0"},
:"2"=>
{:"1"=>1,
:"2"=>"1525341956127",
:"3"=>1000000000,
:"4"=>0,
:"5"=>{:"1"=>1000000000}},
:"3"=>["17"],
:"6"=>"4704522736971289334",
:"8"=>["4618851880555471022"],
:"9"=>[1]},
:"6"=>{:"3"=>{:"2"=>false}, :"4"=>{:"1"=>false}},
:"7"=>{:"1"=>1},
:"8"=>"production"},
{:"1"=>4,
:"2"=>"4700283765268993541",
:"6"=>{:"2"=>{:"1"=>200}, :"3"=>{:"2"=>false}, :"4"=>{:"1"=>false}},
:"8"=>"beta"},
{:"1"=>5,
:"2"=>"4699074054925986704",
:"6"=>{:"2"=>{:"1"=>100}, :"3"=>{:"2"=>false}, :"4"=>{:"1"=>false}},
:"8"=>"alpha"},
{:"1"=>10,
:"2"=>"4697702456121346981",
:"6"=>{:"2"=>{:"1"=>50}, :"3"=>{:"2"=>false}, :"4"=>{:"1"=>false}},
:"8"=>"internal"}],
:"3"=>{:"1"=>true, :"2"=>{:"1"=>{:"1"=>false}, :"2"=>{:"1"=>false}}},
:"4"=>false},
:xsrf=>"AMtNNDFJl06mR54j2zxFjYIYfGQR22sUKA:1528830206790"}
I am looking for simple way to return a value or nil
I have tried this
result[:'result'][:'1'][1].dig(:'5').dig(:'1').dig(:'1')
but it's not working
What can I do to avoid this
if result[:'result'][:'1'][1].dig(:'5')
puts result[:'result'][:'1'][1][:'5'][:'1'][:'1']
end
The idea behind dig is that you can go several levels deep into a hash at the same time and return nil if the key doesn't exist at any level during the 'digging'. So result[:'result'][:'1'][1].dig(:'5', :'1', :'1') will do what you are looking for and clean up your code as well. In fact, you could make it a little safer if you wanted by doing result.dig(:result, :'1', 1, :'5', :'1', :'1')
dig is not a single method, but a family of four methods, all of which made their debut in Ruby v2.3: Array#dig, Hash#dig, Struct#dig and OpenStruct#dig.
For example,
h = { a: [1, { c: 2, d: 3 }], b: 2 }
h.dig(:a, 1, :d)
#=> 3
employs Hash#dig because dig's receiver is a hash. Moreover, one might expect that when, in an intermediate calculation, dig has unearthed [1, { c: 2, d: 3 }] it will pass the shovel to Array#dig for further excavation.
Suppose
h = { a: [1, 2] }
Then
h.dig(:a, 1) #=> 2
h.dig(:a).dig(1) #=> 2
Does that mean the two are equivalent? Try this:
h.dig('cat', 1) #=> nil
h.dig('cat').dig(1) #=> NoMethodError: undefined method `dig' for nil:NilClass
The exception is due to the fact that h.dig('cat') #=> nil and NilClass has no instance method dig, so nil.dig(1) raises the exception. No, the two expressions are not equivalent.
If the value of the variable result is the OPs hash, we have (as pointed out by #Isaiah) the following.
result.dig(:result, :'1', 0, :"5", :"1", :"1")
#=> "1.0.0"
result.dig(:result, :'1', 0, :cat, :"1", :"1")
#=> nil
Note that dig will still raise an exception if the wrong data type is used:
[1, 2].dig(:a)
#=> TypeError: no implicit conversion of Symbol into Integer
To support versions of Ruby prior to 2.3 (where dig is not available) we can write the following, using Enumerable#reduce (aka inject).
arr = [:result, :'1', 0, :"5", :"1", :"1"]
arr.reduce(result) { |memo, obj| memo && memo[obj] }
#=> "1.0.0"
arr = [:result, :'1', 0, :cat, :"1", :"1"]
arr.reduce(result) { |memo, obj| memo && memo[obj] }
#=> nil

Methods with optional and keyword arguments

def test(a, b = {}, c: 1)
[a, b, c]
end
test 1, {hello: :world}
I would expect the output of this to be:
[1, {hello: :word}, 1]
But instead I got:
ArgumentError: unknown keyword: hello
I've read some pages like https://makandracards.com/makandra/36011-ruby-do-not-mix-optional-and-keyword-arguments
that suggest to dont mix with optional arguments a keywords argument.
Is possible to make this work?
If you want this exact signature, you should probably use double-splat for b parameter:
def test(a, c: 1, **b)
[a, b, c]
end
test 1, {hello: :world}
#⇒ [1, {hello: :word}, 1]
There are issues distinguishing named keyword parameters and the hash itself when the latter is passed immediately before named keywords.

How to use &proc argument inside method

Array#max_by returns only a single value, but I want to have all values that have the max value.
hashes = [{a: 1, b:2}, {a:2, b:3}, {a:1, b:3}]
max = hashes.map{|h| h[:b]}.max
hashes.select{|h| h[:b] == max}
# => [{a: 2, b: 3}, {a: 1, b: 3}]
This code works fine, and I want to add it to Array class.
class Array
def max_values_by(&proc)
max = map(&proc).max
# I don't know how to use `select` here.
end
end
How to access the value of the &proc argument?
Use the proc in the block passed to select by calling it with call:
class Array
def max_values_by(&proc)
max = map(&proc).max
select { |h| proc.call(h) == max }
end
end
hashes.max_values_by { |h| h[:b] }
=> [{a: 2, b: 3}, {a: 1, b: 3}]
or with yield, which gives identical results:
def max_values_by(&proc)
max = map(&proc).max
select { |h| yield(h) == max }
end
Although proc.call is a little longer than yield, I prefer it in this case because it makes it clearer that the same block is being used in two places in the method, and because it's weird to use both the implicit block passing of yield and the explicit passing of &proc in the same method.
#DaveSchweisguth suggests a great implementation using select, like you requested. Another way of achieving the same result is by using group_by, like this:
>> hashes.group_by{|h| h[:b]}.max.last
=> [{:a=>2, :b=>3}, {:a=>1, :b=>3}]
or monkey-patched into Array as:
class Array
def max_values_by(&proc)
group_by(&proc).max.last
end
end

is it possible to use hash keys as a first level variables inside ruby method?

I'll try to explain myself a little bit more
# Lets say I have this hash
options = {a: 1, b: 2}
# and here I'm calling the method
some_method(options)
def some_method(options)
# now instead of using options[:a] I'd like to simply use a.
options.delete_nesting_and_create_vars
a + b # :a + :b also good.
thanks!
Is it possible using Ruby2 splat parameters:
options = {a: 1, b: 2}
def some_method1(a:, b:)
a + b
end
or:
def some_method2(**options)
options[:a] + options[:b]
end
some_method1 **options
#⇒ 3
some_method2 **options
#⇒ 3
If your options are fixed, like only :a and :b are the only keys, you can write the method like this:
def some_method(a:, b:)
a + b
end
options = {a: 1, b: 2}
some_method(options) #=> 3

Resources