Search code examples
rubymethodsparameter-passingruby-hashdouble-splat

Ruby double splat parameter is too greedy with Hash argument


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?


Solution

  • 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