5

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?

sawa
  • 165,429
  • 45
  • 277
  • 381
Taschetto
  • 321
  • 3
  • 14

3 Answers3

3

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}, {}]
ray
  • 5,454
  • 1
  • 18
  • 40
1

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.

Max
  • 21,123
  • 5
  • 49
  • 71
  • Thanks for your answer. Unfortunately I need to pass a hash to `subject` sometimes. – Taschetto Jan 10 '19 at 13:57
  • @Taschetto Ok... but how would you distinguish a nil subject and just options from a Hash subject and no options? – Max Jan 10 '19 at 13:58
  • `my_method({foo: 'foo'})` should be a hash subject and no options; `my_method(foo: 'foo')` should be a nil subject and just options. But maybe I'm getting it wrong. – Taschetto Jan 10 '19 at 13:59
  • @Taschetto I'm pretty sure that's a limitation of Ruby. It cannot distinguish between those two cases. I would just make `subject` non-optional in that case. – Max Jan 10 '19 at 14:00
0

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'}, {})
Xero
  • 3,951
  • 4
  • 41
  • 73
  • 1
    Thanks for you answer. Yes, that works. But in my case I'd have to refactor a large number of method calls. I need to maintain the method interface. – Taschetto Jan 10 '19 at 13:56
  • @Taschetto then my_method({foo: 'foo'}, {}) can be a solution ? – Xero Jan 10 '19 at 14:00