Ruby double splat parameter is too greedy with Hash argument

806 Views Asked by At

In Ruby 2.4.1, I have a method like this:

def example(*args, **kwargs)
  p args
  p kwargs
end

I can pass in positional arguments that are not Hash just fine:

irb(main):001:0> example("Greetings")
["Greetings"]
{}

And if I want to use named parameters, that's fine, too:

irb(main):002:0> example(something: 42)
[]
{:something=>42}

But trying to pass a Hash as a positional argument, this happens:

irb(main):002:0> example({something: 42})
[]
{:something=>42}

I want *args to take {something: 42}, not **kwargs.

The positional arguments have to be optional, but even if *args were arg=nil, the double-splat is still too greedy:

irb(main):001:0> def example(arg=nil, **kwargs)
irb(main):002:1>   p arg
irb(main):003:1>   p kwargs
irb(main):004:1> end
=> :example

irb(main):005:0> example({"key":"value"})
nil
{:key=>"value"}

irb(main):006:0> example({"key":"value"}, this_is: "in kwargs")
{:key=>"value"}
{:this_is=>"in kwargs"}

How can I pass a Hash as a positional argument when the method takes ** as well?

1

There are 1 best solutions below

0
On BEST ANSWER

That's always tricky to manage because the **kwargs here will aggressively take ownership of that hash. This also presents some confusion on the part of the caller since they'll have to be careful when calling it regardless of what you do:

# Is this options or the body? It's not clear.
example({ payload: true })

I'd recommend switching to something more explicit, like:

def example(**kwargs)
  body = kwargs.delete(:body)
end

Then calling it like this:

example(body: { ... }, user: "username", ...)

If you can spell out the allowed arguments, even better:

def example(body: nil, user: nil, password: nil)
  # ...
end