While a method with a default value profile can accept nil (besides a hash):
def f(options = {})
options
end
f(hoge: "AAA", foo: "BBB") #=> {:hoge=>"AAA", :foo=>"BBB"}
f(nil) #=> nil
A method with double splat raises an error with nil:
def f(**options)
options
end
f(hoge: "AAA", foo: "BBB") #=> {:hoge=>"AAA", :foo=>"BBB"}
f(nil) # => wrong number of arguments (1 for 0) (ArgumentError)
When should I use double splat and when should I use = {}?
If the input to the method MUST be options hash, then, use double splat operator **.
Using options = {} only declares the default value to be empty hash, however, it does not necessarily guarantee that caller will pass hash - she may pass non-hash values and nil.
If the function was implemented using double splat (**) - as evident in examples you have provided - then non-hash and nil values will not be accepted and will be reported as error.
Related
It seems that a double-splatted block parameter calls to_ary on an object that is passed, which does not happen with lambda parameters and method parameters. This was confirmed as follows.
First, I prepared an object obj on which a method to_ary is defined, which returns something other than an array (i.e., a string).
obj = Object.new
def obj.to_ary; "baz" end
Then, I passed this obj to various constructions that have a double splatted parameter:
instance_exec(obj){|**foo|}
# >> TypeError: can't convert Object to Array (Object#to_ary gives String)
->(**foo){}.call(obj)
# >> ArgumentError: wrong number of arguments (given 1, expected 0)
def bar(**foo); end; bar(obj)
# >> ArgumentError: wrong number of arguments (given 1, expected 0)
As can be observed above, only code block tries to convert obj to an array by calling a (potential) to_ary method.
Why does a double-splatted parameter for a code block behave differently from those for a lambda expression or a method definition?
I don't have full answers to your questions, but I'll share what I've found out.
Short version
Procs allow to be called with number of arguments different than defined in the signature. If the argument list doesn't match the definition, #to_ary is called to make implicit conversion. Lambdas and methods require number of args matching their signature. No conversions are performed and that's why #to_ary is not called.
Long version
What you describe is a difference between handling params by lambdas (and methods) and procs (and blocks). Take a look at this example:
obj = Object.new
def obj.to_ary; "baz" end
lambda{|**foo| print foo}.call(obj)
# >> ArgumentError: wrong number of arguments (given 1, expected 0)
proc{|**foo| print foo}.call(obj)
# >> TypeError: can't convert Object to Array (Object#to_ary gives String)
Proc doesn't require the same number of args as it defines, and #to_ary is called (as you probably know):
For procs created using lambda or ->(), an error is generated if wrong number of parameters are passed to the proc. For procs created using Proc.new or Kernel.proc, extra parameters are silently discarded and missing parameters are set to nil. (Docs)
What is more, Proc adjusts passed arguments to fit the signature:
proc{|head, *tail| print head; print tail}.call([1,2,3])
# >> 1[2, 3]=> nil
Sources: makandra, SO question.
#to_ary is used for this adjustment (and it's reasonable, as #to_ary is for implicit conversions):
obj2 = Class.new{def to_ary; [1,2,3]; end}.new
proc{|head, *tail| print head; print tail}.call(obj2)
# >> 1[2, 3]=> nil
It's described in detail in a ruby tracker.
You can see that [1,2,3] was split to head=1 and tail=[2,3]. It's the same behaviour as in multi assignment:
head, *tail = [1, 2, 3]
# => [1, 2, 3]
tail
# => [2, 3]
As you have noticed, #to_ary is also called when when a proc has double-splatted keyword args:
proc{|head, **tail| print head; print tail}.call(obj2)
# >> 1{}=> nil
proc{|**tail| print tail}.call(obj2)
# >> {}=> nil
In the first case, an array of [1, 2, 3] returned by obj2.to_ary was split to head=1 and empty tail, as **tail wasn't able to match an array of[2, 3].
Lambdas and methods don't have this behaviour. They require strict number of params. There is no implicit conversion, so #to_ary is not called.
I think that this difference is implemented in these two lines of the Ruby soruce:
opt_pc = vm_yield_setup_args(ec, iseq, argc, sp, passed_block_handler,
(is_lambda ? arg_setup_method : arg_setup_block));
and in this function. I guess #to_ary is called somewhere in vm_callee_setup_block_arg_arg0_splat, most probably in RARRAY_AREF. I would love to read a commentary of this code to understand what happens inside.
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.
Given this method definition:
def foo(a = nil, b: nil)
p a: a, b: b
end
When I invoke the method with a single hash argument, the hash is always implicitly converted to keyword arguments, regardless of **:
hash = {b: 1}
foo(hash) #=> {:a=>nil, :b=>1}
foo(**hash) #=> {:a=>nil, :b=>1}
I can pass another (empty) hash as a workaround:
foo(hash, {}) #=> {:a=>{:b=>1}, :b=>nil}
But, this looks pretty cumbersome and awkward.
I would have expected Ruby to handle this more like arrays are handled, i.e.:
foo(hash) #=> {:a=>{:b=>1}, :b=>nil}
foo(**hash) #=> {:a=>nil, :b=>1}
And using literals:
foo({b: 1}) #=> {:a=>{:b=>1}, :b=>nil}
foo(b: 1) #=> {:a=>nil, :b=>1}
foo(**{b: 1}) #=> {:a=>nil, :b=>1}
The current implementation looks like a flaw and the way I was expecting it to work seems obvious.
Is this an overlooked edge case? I don't think so. There's probably a good reason that it wasn't implemented this way.
Can someone enlighten me, please?
As for the lack of ** part:
My guess is that, to make method invocation simple, Ruby always once interprets the key: value form without the braces as a hash with omitted braces, whether it is actually going to be interpreted as such hash or as keyword arguments.
Then, in order to interpret that as keyword arguments, ** is implicitly applied to it.
Therefore, if you had passed an explicit hash, it will not make difference to the process above, and there is room for it to be interpreted either as an actual hash or as keyword arguments.
What happens when you do pass ** explicitly like:
method(**{key: value})
is that the hash is decomposed:
method(key: value)
then is interpreted as a hash with omitted braces:
method({key: value})
then is interpreted either as a hash or as a keyword argument.
As for keyword arguments having priority over other arguments, see this post on Ruby core: https://bugs.ruby-lang.org/issues/11967.
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 ?
Let's say we have this code:
def something(*someargs)
return *someargs.join(",")
end
Now, I found you can reference *someargs just like any other variable anywhere in the method definition. But I tried this...returning *someargs as a string, separated with a comma. Yet, when I call this method:
a = something(4, 5)
p a.class # => Array
p a #> ["4,5"]
why does something(4,5) still returns an array? If I do something like this:
[4, 5].join(",")
the result will be a string not in an array. So my question would be, how do I make the "something" method return an actual string which contains all the arguments as a string. And it's weird because if I do *someargs.class, the result is "Array", yet it doesn't behave like a typical array...
Try below :
def something(*someargs)
return someargs.join(",")
end
a = something(4, 5)
p a.class # => String
p a # => "4,5"
One example to explain your case:
a = *"12,11"
p a # => ["12,11"]
So when you did return *someargs.join(","), someargs.join(",") created the string as "4,5".But now you are using splat operator(*) again on the evaluated string "4,5" with the assignment operation like a = *"4,5". So finally you are getting ["4,5"].
Read more scenarios with splat operators here - Splat Operator in Ruby
Hope that helps.
An object prepended with a splat *... is not an object. You cannot reference such thing, nor can you pass it as an argument to a method because there is no such thing. However, if you have a method that can take multiple arguments, for example puts, then you can do something like:
puts *["foo", "bar"]
In this case, there is no such thing as *["foo", "bar"]. The splat operator is expanding it into multiple arguments. It is equivalent to:
puts "foo", "bar"
Regarding why someargs remains to be an array after someargs.join(","). That is because join is not a destructive method. It does not do anything to the receiver. Furthermore, an object cannot change its class by a destructive method. The only way to change the reference of someargs from an array to a string is to reassign it.