Optional Argument After Splat Argument

Optional argument after splat argument

Thanks @maerics and @JorgWMittag -

When you have a splat, it reserves all arguments, which is why it was not liking my second "options" argument. I fixed this issue by changing my arguments around to -

def calculate(*arguments)
options = arguments[-1].is_a?(Hash) ? arguments.pop : {}
options[:add] = true if options.empty?
return add(*arguments) if options[:add]
return subtract(*arguments) if options[:subtract]
end

optional key word argument needed after splat argument when sub-elements are not arrays

The splat operator splits the array into arguments.

However if you wrap it in an array it works again but now it's an array inside an array and still treated as a single argument passed to your method.

call([*scores]) #no error

But also to illustrate why you got the error look what happens here:

def call(*scores, alpha: nil)
puts scores.inspect
end

call(*scores[0]) #=> #[[:a, 1]]

UPDATED: Thanks to @Stefan, the reason for the error is actually that your method accepts keyword arguments, which is apparently a known bug. See Keyword arguments unpacking (splat) in Ruby

The reason your last example works is that by passing a 2nd argument to your method, the splat handles the first argument as an array instead of trying to split it up into 2 arguments.

Fore more see Ruby, Source Code of Splat?

Also see https://www.rubyguides.com/2018/07/ruby-operators/#Ruby_Splat_Operator

using splat operator to pass optional parameters does not work as expected

While a list can be unpacked for positional arguments, which you've obviously figured out, for keyword arguments, you need a dictionary.

args = [21, 32]
kwargs = {"c": "mike"}

def test_function(a, b, c=None):
print(a, b, c)

test_function(*args, **kwargs)
# (21, 32, 'mike')

This isn't inherently a bad idea, but when you do something like this, make sure you're keeping in mind how it could impact readability for someone looking at your code (including yourself in a couple months after this is no longer fresh in your mind).

Why can I have required parameters after a splat in Ruby but not optional ones?

Ruby's argument binding semantics are already pretty complex. Consider this method:

def foo(m1, m2, o1=:o1, o2=:o2, *splat, m3, m4, 
ok1: :ok1, mk1:, mk2:, ok2: :ok2, **ksplat, &blk)
local_variables.map {|var| [var, eval(var.to_s)] }.to_h
end

method(:foo).arity
# => -5

method(:foo).parameters
# => [[:req, :m1], [:req, :m2], [:opt, :o1], [:opt, :o2], [:rest, :splat],
# [:req, :m3], [:req, :m4], [:keyreq, :mk1], [:keyreq, :mk2],
# [:key, :ok1], [:key, :ok2], [:keyrest, :ksplat], [:block, :blk]]

Can you tell at first glance what the result of the following invocations will be?

foo(1, 2, 3, 4)

foo(1, 2, 3, mk1: 4, mk2: 5)

foo(1, 2, 3, 4, mk1: 5, mk2: 6)

foo(1, 2, 3, 4, 5, mk1: 6, mk2: 7)

foo(1, 2, 3, 4, 5, 6, mk1: 7, mk2: 8)

foo(1, 2, 3, 4, 5, 6, 7, mk1: 8, mk2: 9)

foo(1, 2, 3, 4, 5, 6, 7, 8, mk1: 9, mk2: 10)

foo(1, 2, 3, 4, 5, 6, 7, 8, ok1: 9, mk1: 10, mk2: 11)

foo(1, 2, 3, 4, 5, 6, 7, 8, ok1: 9, mk1: 10, mk2: 11, ok2: 12)

foo(1, 2, 3, 4, 5, 6, 7, 8, ok1: 9, mk1: 10, mk2: 11, ok2: 12, k3: 13)

foo(1, 2, 3, 4, 5, 6, 7, 8, ok1: 9, mk1: 10, mk2: 11, ok2: 12, k3: 13, k4: 14)

foo(1, 2, 3, 4, 5, 6, 7, 8,
ok1: 9, ok2: 10, mk1: 11, mk2: 12, k3: 13, k4: 14) do 15 end

Now, imagine adding optional parameters with default arguments after the splat parameter to that list. It's not impossible to find sane semantics for that, but it may lead to some non-obvious results.

Can you come up with simple, sane, backwards-compatible, and non-surprising semantics?

BTW: here's the cheatsheet for the method at the top:

foo(1, 2, 3, 4)
# ArgumentError: missing keywords: mk1, mk2

foo(1, 2, 3, mk1: 4, mk2: 5)
# ArgumentError: wrong number of arguments (3 for 4+)

foo(1, 2, 3, 4, mk1: 5, mk2: 6)
# => { m1: 1, m2: 2, o1: :o1, o2: :o2, splat: [], m3: 3, m4: 4,
# ok1: :ok1, mk1: 5, mk2: 6, ok2: :ok2, ksplat: {},
# blk: nil }

foo(1, 2, 3, 4, 5, mk1: 6, mk2: 7)
# => { m1: 1, m2: 2, o1: 3, o2: :o2, splat: [], m3: 4, m4: 5,
# ok1: :ok1, mk1: 6, mk2: 7, ok2: :ok2, ksplat: {},
# blk: nil }

foo(1, 2, 3, 4, 5, 6, mk1: 7, mk2: 8)
# => { m1: 1, m2: 2, o1: 3, o2: 4, splat: [], m3: 5, m4: 6,
# ok1: :ok1, mk1: 7, mk2: 8, ok2: :ok2, ksplat: {},
# blk: nil }

foo(1, 2, 3, 4, 5, 6, 7, mk1: 8, mk2: 9)
# => { m1: 1, m2: 2, o1: 3, o2: 4, splat: [5], m3: 6, m4: 7,
# ok1: :ok1, mk1: 8, mk2: 9, ok2: :ok2, ksplat: {},
# blk: nil }

foo(1, 2, 3, 4, 5, 6, 7, 8, mk1: 9, mk2: 10)
# => { m1: 1, m2: 2, o1: 3, o2: 4, splat: [5, 6], m3: 7, m4: 8,
# ok1: :ok1, mk1: 9, mk2: 10, ok2: :ok2, ksplat: {},
# blk: nil }

foo(1, 2, 3, 4, 5, 6, 7, 8, ok1: 9, mk1: 10, mk2: 11)
# => { m1: 1, m2: 2, o1: 3, o2: 4, splat: [5, 6], m3: 7, m4: 8,
# ok1: 9, mk1: 10, mk2: 11, ok2: :ok2, ksplat: {},
# blk: nil }

foo(1, 2, 3, 4, 5, 6, 7, 8, ok1: 9, mk1: 10, mk2: 11, ok2: 12)
# => { m1: 1, m2: 2, o1: 3, o2: 4, splat: [5, 6], m3: 7, m4: 8,
# ok1: 9, mk1: 10, mk2: 11, ok2: 12, ksplat: {},
# blk: nil }

foo(1, 2, 3, 4, 5, 6, 7, 8, ok1: 9, mk1: 10, mk2: 11, ok2: 12, k3: 13)
# => { m1: 1, m2: 2, o1: 3, o2: 4, splat: [5, 6], m3: 7, m4: 8,
# ok1: 9, mk1: 10, mk2: 11, ok2: 12, ksplat: {k3: 13},
# blk: nil }

foo(1, 2, 3, 4, 5, 6, 7, 8, ok1: 9, mk1: 10, mk2: 11, ok2: 12, k3: 13, k4: 14)
# => { m1: 1, m2: 2, o1: 3, o2: 4, splat: [5, 6], m3: 7, m4: 8,
# ok1: 9, mk1: 10, mk2: 11, ok2: 12, ksplat: {k3: 13, k4: 14},
# blk: nil }

foo(1, 2, 3, 4, 5, 6, 7, 8,
ok1: 9, ok2: 10, mk1: 11, mk2: 12, k3: 13, k4: 14) do 15 end
# => { m1: 1, m2: 2, o1: 3, o2: 4, splat: [5, 6], m3: 7, m4: 8,
# ok1: 9, mk1: 10, mk2: 11, ok2: 12, ksplat: {k3: 13, k4: 14},
# blk: #<Proc:0xdeadbeefc00l42@(irb):15> }

Order of optional parameters in initializer

If you define optional parameters before AND after mandatory parameters, in some cases it will be impossible to decide how a goven list or arguments should map to the defined parameters.

In your case, when defining this method:

class A
def initialize(a = "default val", b, c = [])
#...
end
end

How would you handle this when giving two arguments,. i.e.

A.new 'hello', 'world'

You could then assign

a = 'hello'
b = 'world'
c = []

but you could equally set

a = 'default val'
b = 'hello'
c = 'world'

Given this unambiguity, Ruby rejects those constructs. You thus have to define all optional parameters either at the front or the back of your parameter list, while it is commonly accepted standard to define optional arguments only at the end.

If you want to be more specific about which arguments should be set with a large number of optional parameters, you can also use keyword arguments. Since you have to specify the name of the arguments when calling the method here, the order of mandatory and optional keyword arguments doesn't matter.

How to define a method in ruby using splat and an optional hash at the same time?

You can't do that. You have to think about how Ruby would be able to determine what belongs to *ary and what belongs to the optional hash. Since Ruby can't read your mind, the above argument combination (splat + optional) is impossible for it to solve logically.

You either have to rearrange your arguments:

def test(id, h, *a)

In which case h will not be optional. Or then program it manually:

def test(id, *a)
h = a.last.is_a?(Hash) ? a.pop : nil
# ^^ Or whatever rule you see as appropriate to determine if h
# should be assigned a value or not.

How to add arguments after splat arguments in method calls?

You are confusing method declaration using splat, and method call using splat.

They are independant of each other:

def regular_method(arg1, arg2, arg3)
#do something
end

def method_with_splat(arg1, *more_args)
# do something
end

arr = [1, 2, 3]

regular_method(*arr) # works!

method_with_splat(4, *arr) # works! (arg1==4, more_args==[1,2,3])

method_with_splat(4, 5, *arr) # also ok! (arg1==4, more_args==[5,1,2,3])

method_with_splat(*arr, 4, 5, 6) # just fine! (arg1==1, more_args==[2,3,4,5,6])

So, in the same spirit:

def splat_in_the_middle(arg1, *more_args, last_arg)
# do something
end

splat_in_the_middle(*arr, 4, 5, 6) # arg1==1, more_args==[2,3,4,5], last_arg==6

And expanding on that:

def splat_in_the_middle(arg1, *more_args, arg2, last_arg)
# do something
end

splat_in_the_middle(*arr, 4, 5, 6) # arg1==1, more_args==[2,3,4], arg2==5, last_arg==6

Notice the first argument is assigned to the first parameter, the last arguments are always assigned to the last parameters, and the other become elements of the more_args array.

As for your extra questions:

  1. You cannot declare more than one splat in a single method definition. The simple reason is evident from the examples above - there is no way for the argument parser to know where one splat ends, and where the other begins:

    def two_splats(arg1, *splat1, arg2, *splat2)
    # do something!
    end
    # => syntax error!

    two_splats(1, 2, 3, 4, 5, 6) # arg1==1, splat1==[????] arg2==?, splat2==[????]

    though there is no problem calling a method with more than one splat:

    method_with_splat(*arr, *arr) # arg1==1, more_args=[2,3,1,2,3]
  2. If there is good reasoning to having your splat in the middle, and the arguments are readable, and easy to understand, there is no reason not to put the splat in the middle. A very useful use-case, for example might be that the last argument is an options hash:

    def splat_in_the_middle(first_arg, *one_or_more_other_args, options)

Handling a method with an optional argument followed by a list of keyword arguments

See, you have **options as an argument which do not have any default value & first argument have default value. So understand following single argument case,

Whenever single argument is passed it is tried to assign to second argument (as first one is holding default nil) & if it fails due to type mismatch then it assign to first argument. That's how my_method(4) works.

Now Suppose you have single argument passed as hash, which match to assign to 2nd argument then of course it get assigned to second argument & first is set default nil.

If you want to make it work, then you can do following,

> my_method({sd: 4}, {})
=> [{:sd=>4}, {}]

Or you can provide argument name while passing,

> my_method(subject: {sd: 4})
=> [{:sd=>4}, {}]

How to set a default value for a splat argument in Ruby

Your attempted usage is counter the conventions around splat usage. Splats are supposed (at least in Ruby) to take up all extra (0, 1 or more) values.

If you know that you want the second value in your method arguments list to have a default value, you could take it out of the splat and list it just before the splat with a default value like this:

def a b, c=nil, *d 
# rest of code omitted
end

EDIT:
To make the answer to your question of why it doesn't work perfectly clear. It's a design decision by the language designer. Matz never intended the splat operator to work with defaults. This seems pretty sensible to me since it is intended to be used for catching an indeterminate number of variables and because the method I described reads more clearly than the possibilities you described and because all of the problems your examples solve are solvable in other ways.



Related Topics



Leave a reply



Submit