Ruby strange behavior in keyword arguments mixed with positional - ruby

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"]

Related

ruby code: why put colon in front of variable name (inside initialize method)

I came across some Ruby code,
I try to understand why the variables have colon at the end of their name inside the declaration of the initialize method.
Is there any reason for the colon?
attr_reader :var1, :var2
def initialize(var1:, var2:)
#var1 = var1
#var2 = var2
end
Those are keyword arguments.
You can use them by name and not position. E.g.
ThatClass.new(var1: 42, var2: "foo")
or
ThatClass.new(var2: "foo", var1: 42)
An article about keyword arguments by thoughtbot
It is called keyword arguments.
Keyword arguments are similar to positional arguments with default
values:
def add_values(first: 1, second: 2)
first + second
end
Arbitrary keyword arguments will be accepted with **:
def gather_arguments(first: nil, **rest)
p first, rest
end
gather_arguments first: 1, second: 2, third: 3
# prints 1 then {:second=>2, :third=>3}
When calling a method with keyword arguments the arguments may appear
in any order. If an unknown keyword argument is sent by the caller an
ArgumentError is raised.
When mixing keyword arguments and positional arguments, all positional
arguments must appear before any keyword arguments.

Ruby automatically expands Hash into keyword arguments without double splat

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 ?

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.

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.

Why non-explicit splat param plus default param is wrong syntax for method definition in Ruby 1.9?

I'd like to ask why having a splat param1 and a param2 with default value assignment in Ruby-1.9.3-p0 as below:
def my_method(*param1, param2 = "default"); end
returns
SyntaxError: (irb):1: syntax error, unexpected '=', expecting ')'
My workaround is explicitly wrap param1 in brackets like this:
def my_method((*param1), param2 = "default"); end
Many thanks
Ruby can't parse a parameter with a default after a splat. If you have default assignment in a parameter after a splat, how would Ruby know what to assign the variable to?
def my_method(*a, b = "foo"); end
Let's say I then call my_method:
my_method(1, 2, 3)
Ruby has no way of knowing whether b is missing, in which case you want b to be foo and a is [1,2,3], or if b is present in which case you want it to be 3.

Resources