How to Define a Method in Ruby Using Splat and an Optional Hash at the Same Time

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.

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

What does the double-splat do in a method call?

One might need to destructure the input parameters. In such a case simple hash won’t work:

params = {foo: 42, bar: :baz}
def t1(foo:, **params); puts params.inspect; end
#⇒ :t1
def t2(foo:, params); puts params.inspect; end
#⇒ SyntaxError: unexpected tIDENTIFIER
def t2(params, foo:); puts params.inspect; end
#⇒ :t2

Now let’s test it:

t1 params
#⇒ {:bar=>:baz}
t2 params
#⇒ ArgumentError: missing keyword: foo
t2 **params
#⇒ ArgumentError: missing keyword: foo

That said, double splat allows transparent arguments destructuring.

If one is curious why it might be useful, foo in the example above is made a mandatory parameter in a call to the method within this syntax.


Unsplatting parameters in call to function is allowed as a sort of sanity type check to assure that all the keys are symbols:

h1 = {foo: 42}
h2 = {'foo' => 42}
def m(p); puts p.inspect; end
m **h1
#⇒ {:foo=>42}
m **h2
#⇒ TypeError: wrong argument type String (expected Symbol)

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> }

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}, {}]

splat operator on hash for keyword arguments in ruby method definition

Yeah... that's not a thing you can do.

**foo in an argument list is how you collect a kwargs hash, so it can't also be how you inject one.

More importantly, the main point of kwargs is that they explode the hash into local variables -- that can't work if it's expanding a hash at runtime.

The closest you could get would be something like:

def initialize(**values)
values = DEFAULTS.merge(values)
raise "..." unless (values.keys - DEFAULTS.keys).empty?
@a = values[:a]
@b = values[:b]
end

Ruby Methods and Optional parameters

The way optional arguments work in ruby is that you specify an equal sign, and if no argument is passed then what you specified is used. So, if no second argument is passed in the second example, then

{age: 27, weight: 160, city: "New York"}

is used. If you do use the hash syntax after the first argument, then that exact hash is passed.

The best you can do is

def my_info2(name, options = {})
options = {age: 27, weight: 160, city: "New York"}.merge(options)
...


Related Topics



Leave a reply



Submit