Ruby automatically expands Hash into keyword arguments without double splat - ruby

The below Ruby code results in: unknown keyword: a (ArgumentError):
def test(x={}, y: true); end
test({a:1})
Why? I would expect this to happen with test(**{a:1}), but I don't understand why my hash is being automagically expanded without the double splat.

Since x is optional, hash moves over to kwarg argument.
Unspecified keywords raise error in that case:
def foo(name:)
p name
end
foo # raises "ArgumentError: missing keyword: name" as expected
foo({name: 'Joe', age: 10}) # raises "ArgumentError: unknown keyword: age"
Check out this article

I'd also find it a bug as it behaves quite inconsistently, only for hashes with keys of Symbol type:
test({a:1}) # raises ArgumentError
test({'a' => 1}) # nil, properly assigned to optional argument x
method(:test).parameters
=> [[:opt, :x], [:key, :y]]
You may pass both arguments and it starts to assign them properly, but this is not a solution.
test({a:1}, {y:false}) # nil
Any reason why this is not a bug but an expected behavior ?

Related

Mixing keyword argument and arguments with default values duplicates the hash?

So i discovered this ruby behaviour, which kept me going crazy for over an hour. When I pass a hash to a function which has a default value for hash AND a keyword argument, it seems like the reference doesn't get passed correctly. As soon as I take away the default value OR the keyword argument, the function behaves as expected. Am I missing some obvious ruby rule here?
def change_hash(h={}, rand: om)
h['hey'] = true
end
k = {}
change_hash(k)
k
#=> {}
It works fine as soon as I take out the default or the keyword arg.
def change_hash(h, rand: om)
h['hey'] = true
end
k = {}
change_hash(k)
k
#=> {'hey' => true}
def change_hash(h={})
h['hey'] = true
end
k = {}
change_hash(k)
k
#=> {'hey' => true}
EDIT
Thanks for your answers. Most of you pointed out that ruby parses the hash as a keyword argument in some cases. However, I am talking about the case when a hash has string keys. When I pass the hash, it seems like the value that gets passed is correct. But modifying the hash inside the function doesn't modify the original hash.
def change_hash(hash={}, another_arg: 300)
puts "another_arg: #{another_arg}"
puts "hash: #{hash}"
hash['hey'] = 3
end
my_hash = {"o" => 3}
change_hash(my_hash)
puts my_hash
Prints out
another_arg: 300
hash: {"o"=>3}
{"o"=>3}
TL;DR ruby allows passing hash as a keyword argument as well as “expanded inplace hash.” Since change_hash(rand: :om) must be routed to keyword argument, so should change_hash({rand: :om}) and, hence, change_hash({}).
Since ruby allows default arguments in any position, the parser takes care of default arguments in the first place. That means, that the default arguments are greedy and the most amount of defaults will take a place.
On the other hand, since ruby lacks pattern-matching feature for function clauses, parsing the given argument to decide whether it should be passed as double-splat or not would lead to huge performance penalties. Since the call with an explicit keyword argument (change_hash(rand: :om)) should definitely pass :om to keyword argument, and we are allowed to pass an explicit hash {rand: :om} as a keyword argument, Ruby has nothing to do but to accept any hash as a keyword argument.
Ruby will split the single hash argument between hash and rand:
k = {"a" => 42, rand: 42}
def change_hash(h={}, rand: :om)
h[:foo] = 42
puts h.inspect
end
change_hash(k);
puts k.inspect
#⇒ {"a"=>42, :foo=>42}
#⇒ {"a"=>42, :rand=>42}
That split feature requires the argument being cloned before passing. That is why the original hash is not being modified.
This is particularly tricky case in Ruby indeed.
In your example you have optional argument which is a hash and you have an optional keyword argument at the same time. In this situation if you pass only one hash, Ruby interprets it as a hash which contains keyword arguments. Here is the code to clarify:
change_hash({rand1: 'om'})
# ArgumentError: unknown keyword: rand1
To work around this you can pass two separate hashes into the method with second one (the one for keyword arguments) being empty:
def change_hash(h={}, rand: 'om')
h['hey'] = true
end
k = {}
change_hash(k, {})
k
#=> {'hey' => true}
From the practical point of view it is better to avoid metdhod signature like that in production code, because it is very easy to make an error while using the method.

Pass Ruby 2 keyword arguments without specifying them explicitly

How I can pass the keyword arguments to a call to another method in my code without specifying each of them.
Consider the following method with the new first-class support for kwargs:
def foo(obj, bar: 'a', baz: 'b')
another_method(bar, baz)
end
Consider the old Ruby options hash syntax:
def foo(obj, options = {})
another_method(options)
end
I want to pass the keyword arguments without specifying each of them explicitly as with the case of the old options hash syntax. Is that even possible?
You can use the double-splat operator:
def a(q: '1', w: '2')
p q
p w
end
def b(**options)
a(options)
end
b
# => 1
# => 2
b(q: '3', w: '4')
# => 3
# => 4
Keyword arguments work similarly to your "old Ruby options hash syntax". A hash's keys will substitute as keywords and their values as arguments. This should work fine:
def foo(obj, bar: 'a', baz: 'b')
another_method(bar, baz)
end
arg_hash = { bar: 'alt bar', baz: 'alt baz' }
foo(Object.new, arg_hash)
Some things to be aware of
If your hash keys are strings, such as { 'bar' => 'alt bar', 'baz' => 'alt baz' }, this won't work. You'll get an ArgumentError: wrong number of arguments (2 for 1) error. This is easy enough to fix in Rails by calling .symbolize_keys on the hash. If you're not in Rails, you'll have to convert the keys manually or reimplement that method.
Also, in Rails, a HashWithIndifferentAccess (such as the params hash in a controller), although it has both strings and symbols for keys, can't be used in this way. If you pass params, it will look at the string keys instead and you'll get the same error as above. I'm not sure why this is the case, but my guess it's because the params hash starts with strings and adds symbols so the symbols are secondary and not the default. Total guess on that though.

Ruby strange behavior in keyword arguments mixed with positional

Following code:
class Test
attr_reader :args
def initialize(arg1={}, arg2: 'value2')
#args = [arg1, arg2]
end
end
t = Test.new({key1: 'value1'})
puts t.args
I've expected to get printed out array with content [{key1: 'value1'}, 'value2']
but I'm getting:
test.rb:11:in `new': unknown keywords: val1, val2 (ArgumentError)
from test.rb:11:in `<main>'
To be more strange, with the same testing class invoked with {val1: 'value', val2: 'value2'}, arg2: 1) as arguments i'm getting output: {:val1=>"value", :val2=>"value2"}
Where is the source of this behaviour, i'm missing something or it's a bug?
Ruby versions 2.1.1 and 2.1.2 was tested.
I'm currently using Ruby 2.1.0p0.
The "problem" can be simplified a little with the following example:
def foo(arg1 = {}, arg2: 'value1')
[arg1, arg2]
end
Here, the method foo has one OPTIONAL argument arg1 (with default {}) and one OPTIONAL keyword argument, arg2.
If you call:
foo({key1: 'value1'})
You get the error:
ArgumentError: unknown keyword: key1
from (irb):17
from /home/mark/.rvm/rubies/ruby-2.1.0/bin/irb:11:in `<main>'
The reason is that Ruby is attempting to match the only argument you gave (with keyword key1) to the only OPTIONAL keyword argument which is keyword arg2. They don't match, thus the error.
In the next example:
foo({val1: 'value', val2: 'value2'}, arg2: 1)
We get the result:
=> [{:val1=>"value", :val2=>"value2"}, 1]
This makes sense because I provided two arguments. Ruby can match arg2: 1 to the second keyword argument and accepts {val1: 'value', val2: 'value2'} as a substitute for the first optional argument.
I do not consider the behaviors above a bug.
Obviously, parameters resolution works the other way around from what you expected. In addition to konsolebox' answer, you can fix it by calling the constructor with an additional empty hash:
Test.new({key1: 'value1'}, {})
Do it this way instead:
class Test
attr_reader :args
def initialize(arg1={}, arg2 = 'value2') ## Changed : to =.
#args = [arg1, arg2]
end
end
t = Test.new({key1: 'value1'})
puts t.args.inspect
Output:
[{:key1=>"value1"}, "value2"]

Couldn't understand the difference between `puts{}.class` and `puts({}.class)`

As the anonymous block and hash block looks like approximately same. I was doing kind of playing with it. And doing do I reached to some serious observations as below:
{}.class
#=> Hash
Okay,It's cool. empty block is considered as Hash.
print{}.class
#=> NilClass
puts {}.class
#=> NilClass
Now why the above code showing the same as NilClass,but the below code shows the Hash again ?
puts ({}.class)
#Hash
#=> nil
print({}.class)
#Hash=> nil
Could anyone help me here to understand that what's going one above?
I completely disagree with the point of #Lindydancer
How would you explain the below lines:
print {}.class
#NilClass
print [].class
#Array=> nil
print (1..2).class
#Range=> nil
Why not the same with the below print [].class and print (1..2).class?
EDIT
When ambiguity happens with local variable and method call, Ruby throws an error about the fact as below :
name
#NameError: undefined local variable or method `name' for main:Object
# from (irb):1
# from C:/Ruby193/bin/irb:12:in `<main>'
Now not the same happens with {} (as there is also an ambiguity between empty code block or Hash block). As IRB also here not sure if it's a empty block or Hash. Then why the error didn't throw up when IRB encountered print {}.class or {}.class?
The precedence rules of ruby makes print{}.class interpreted as (print{}).class. As print apparently returns a nil the class method returns #NilClass.
EDIT: As been discussed on other answers and in the updates to the question, print{} it of course interpreted as calling print with a block, not a hash. However, this is still about precedence as {} binds stronger than [] and (1..2) (and stronger than do ... end for that matter).
{} in this case is recognized as block passed to print, while [] unambiguously means empty array.
print {}.class # => NilClass
print do;end.class # => NilClass
You are running into some nuances of Ruby, where characters mean different things depending on context. How the source code is interpreted follows rules, one of which is that {} is a closure block if it follows a method call, and otherwise a Hash constructor.
It's common throughout the language to see characters mean different things depending on context or position within the statement.
Examples:
Parens () used for method call or for precedence
print(1..5).class => NilClass
print (1..5).class => Range <returns nil>
Square brackets [] used to call :[] method or for Array
print[].class => NoMethodError: undefined method `[]' for nil:NilClass
print([].class) => Array <returns nil>
Asterisk * used for multiplication or splatting
1 * 5 => 5
[*1..5] => [1, 2, 3, 4, 5]
Ampersand & used for symbol -> proc or logical and
0 & 1 => 0
[1, 2, 3].map(&:to_s) => ["1", "2", "3"]
Or in your case, braces used for block closures or for a hash
... hope it makes sense now ...

Named parameters in Ruby 2

I don't understand completely how named parameters in Ruby 2.0 work.
def test(var1, var2, var3)
puts "#{var1} #{var2} #{var3}"
end
test(var3:"var3-new", var1: 1111, var2: 2222) #wrong number of arguments (1 for 3) (ArgumentError)
it's treated like a hash. And it's very funny because to use named parameters in Ruby 2.0 I must set default values for them:
def test(var1: "var1", var2: "var2", var3: "var3")
puts "#{var1} #{var2} #{var3}"
end
test(var3:"var3-new", var1: 1111, var2: 2222) # ok => 1111 2222 var3-new
which very similar to the behaviour which Ruby had before with default parameters' values:
def test(var1="var1", var2="var2", var3="var3")
puts "#{var1} #{var2} #{var3}"
end
test(var3:"var3-new", var1: 1111, var2: 2222) # ok but ... {:var3=>"var3-new", :var1=>1111, :var2=>2222} var2 var3
I know why is that happening and almost how it works.
But I'm just curious, must I use default values for parameters if I use named parameters?
And, can anybody tell me what's the difference between these two then?
def test1(var1="default value123")
#.......
end
def test1(var1:"default value123")
#.......
end
I think that the answer to your updated question can be explained with explicit examples. In the example below you have optional parameters in an explicit order:
def show_name_and_address(name="Someone", address="Somewhere")
puts "#{name}, #{address}"
end
show_name_and_address
#=> 'Someone, Somewhere'
show_name_and_address('Andy')
#=> 'Andy, Somewhere'
The named parameter approach is different. It still allows you to provide defaults but it allows the caller to determine which, if any, of the parameters to provide:
def show_name_and_address(name: "Someone", address: "Somewhere")
puts "#{name}, #{address}"
end
show_name_and_address
#=> 'Someone, Somewhere'
show_name_and_address(name: 'Andy')
#=> 'Andy, Somewhere'
show_name_and_address(address: 'USA')
#=> 'Someone, USA'
While it's true that the two approaches are similar when provided with no parameters, they differ when the user provides parameters to the method. With named parameters the caller can specify which parameter is being provided. Specifically, the last example (providing only the address) is not quite achievable in the first example; you can get similar results ONLY by supplying BOTH parameters to the method. This makes the named parameters approach much more flexible.
The last example you posted is misleading. I disagree that the behavior is similar to the one before. The last example passes the argument hash in as the first optional parameter, which is a different thing!
If you do not want to have a default value, you can use nil.
If you want to read a good writeup, see "Ruby 2 Keyword Arguments".
As of Ruby 2.1.0, you no longer have to set default values for named parameters. If you omit the default value for a parameter, the caller will be required to provide it.
def concatenate(val1: 'default', val2:)
"#{val1} #{val2}"
end
concatenate(val2: 'argument')
#=> "default argument"
concatenate(val1: 'change')
#=> ArgumentError: missing keyword: val2
Given:
def test1(var1="default value123")
var1
end
def test2(var1:"default value123")
var1
end
They'll behave the same way when not passed an argument:
test1
#=> "default value123"
test2
#=> "default value123"
But they'll behave much differently when an argument is passed:
test1("something else")
#=> "something else"
test2("something else")
#=> ArgumentError: wrong number of arguments (1 for 0)
test1(var1: "something else")
#=> {:var1=>"something else"}
test2(var1: "something else")
#=> "something else"
I agree with you that it's weird to require default values as the price for using named parameters, and evidently the Ruby maintainers agree with us! Ruby 2.1 will drop the default value requirement as of 2.1.0-preview1.
This is present in all the other answers, but I want to extract this essence.
There are four kinds of parameter:
Required
Optional
Positional
def PR(a)
def PO(a=1)
Keyword
def KR(a:)
def KO(a:1)
When defining a function, positional arguments are specified before keyword arguments, and required arguments before optional ones.
irb(main):006:0> def argtest(a,b=2,c:,d:4)
irb(main):007:1> p [a,b,c,d]
irb(main):008:1> end
=> :argtest
irb(main):009:0> argtest(1,c: 3)
=> [1, 2, 3, 4]
irb(main):010:0> argtest(1,20,c: 3,d: 40)
=> [1, 20, 3, 40]
EDIT: the required keyword argument (without a default value) is new as of Ruby 2.1.0, as mentioned by others.
Leaving this here because it helped me a lot.
Example
Suppose you have this:
def foo(thing, to_print)
if to_print
puts thing
end
end
# this works
foo("hi", true)
# hi
# => nil
so you try adding the argument names, like so:
foo(thing: "hi", to_print: true)
# foo(thing: "hi", to_print: true)
# ArgumentError: wrong number of arguments (given 1, expected 2)
# from (pry):42:in `foo'
but unfortunately it errors.
Solution
Just add a : to the end of each argument:
def foo2(thing:, to_print:)
if to_print
puts thing
end
end
foo2(thing: "hi", to_print: true)
# hi
# => nil
And it works!
According to "Ruby 2.0.0 by Example" you must have defaults:
In Ruby 2.0.0, keyword arguments must have defaults, or else must be captured by **extra at the end.
def test(a = 1, b: 2, c: 3)
p [a,b,c]
end
test #=> [1,2,3]
test 10 #=> [10,2,3]
test c:30 #=> [1,2,30] <- this is where named parameters become handy.
You can define the default value and the name of the parameter and then call the method the way you would call it if you had hash-based "named" parameters but without the need to define defaults in your method.
You would need this in your method for each "named parameter" if you were using a hash.
b = options_hash[:b] || 2
as in:
def test(a = 1, options_hash)
b = options_hash[:b] || 2
c = options_hash[:c] || 3
p [a,b,c]
end
You can define named parameters like
def test(var1: var1, var2: var2, var3: var3)
puts "#{var1} #{var2} #{var3}"
end
If you don't pass one of the parameters, then Ruby will complain about an undefined local variable or method.

Resources