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

Viewed 110

I want to pass an optional argument (any datatype, even a Hash) followed by a list of keyword arguments (which can be empty) to a method.

This is what I got:

def my_method(subject = nil, **options)
  [subject, options]
end

The method output is as expected in the following cases:

  • nothing

    my_method()
    # => [nil, {}]
    
  • a subject

    my_method('foo')
    # => ["foo", {}]
    
  • a literal subject with options

    my_method('foo', bar: 'bar')
    # => ["foo", {:bar=>"bar"}]
    
  • a Hash as subject with options

    my_method({foo: 'foo'}, bar: 'bar')
    # => [{:foo=>"foo"}, {:bar=>"bar"}]
    
  • no subject, only options

    my_method(bar: 'bar')
    # => [nil, {:bar=>"bar"}]
    

When passing a Hash as subject and no options, the desired outcome is:

my_method({foo: 'foo'})
# => [{:foo=>"foo"}, {}]

But I get the following; I don't get the correct subject:

my_method({foo: 'foo'})
# => [nil, {:foo=>"foo"}]

Is my_method(foo: 'foo') equivalent to my_method({foo: 'foo'})? Any ideas on how I could get the desired outcome?

3 Answers

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

As far as I know this is a limitation of how Ruby passes arguments. It can't tell the difference between my_method({foo: 'foo'}) and my_method(foo: 'foo')

Why not hack around it with this?

if subject.is_a?(Hash) && options.empty?
  subject, options = nil, subject
end

This assumes that subject shouldn't be a hash though.

Maybe not the best answer but you can try with keyword argument :

def my_method(subject: nil, **options)
  [ subject, options ]
end

Then

my_method(subject: {foo: 'foo'})

=> [{:foo=>"foo"}, {}]

Alternative :

You can use

my_method({foo: 'foo'}, {})
Related