This question is about Ruby 2.2.
Let's say I have a method which takes positional and named arguments.
class Parent
def foo(positional, named1: "parent", named2: "parent")
puts positional.inspect
puts named1.inspect
puts named2.inspect
end
end
A subclass wants to both override some defaults, and add its own named arguments. How would I best do this? Ideally, it would not have to know details of the signature of the parent, in case the parent wants to add some optional positional arguments. My first attempt was this.
class Child < Parent
def foo(*args, named1: "child", named3: "child" )
super
end
end
But that blows up because the unknown named3:
is passed to the parent.
Child.new.foo({ this: 23 })
/Users/schwern/tmp/test.rb:10:in `foo': unknown keyword: this (ArgumentError)
from /Users/schwern/tmp/test.rb:15:in `<main>'
I tried explicitly passing arguments to super, but that didn't work either. It seems the first positional argument is getting treated as a named one.
class Child < Parent
def foo(*args, named1: "child", named3: "child" )
super(*args, named1: "child")
end
end
Child.new.foo({ this: 23 })
/Users/schwern/tmp/test.rb:10:in `foo': unknown keyword: this (ArgumentError)
from /Users/schwern/tmp/test.rb:15:in `<main>'
I can make Child know about the first positional parameter, that works...
class Child < Parent
def foo(arg, named1: "child", named3: "child" )
super(arg, named1: "child")
end
end
Child.new.foo({ this: 23 })
Parent.new.foo({ this: 23 })
{:this=>23}
"child"
"parent"
{:this=>23}
"parent"
"parent"
...until I pass in a named parameter.
Child.new.foo({ this: 23 }, named2: "caller")
Parent.new.foo({ this: 23 }, named2: "caller")
/Users/schwern/tmp/test.rb:10:in `foo': unknown keyword: named2 (ArgumentError)
from /Users/schwern/tmp/test.rb:15:in `<main>'
How do I make this work and retain the benefit of named parameter checks? I'm open to turning the positional parameter into a named one.
The problem here is that since the parent doesn't know anything about the child's arguments, it has no way of knowing whether the first argument you pass to it was meant to be a positional argument, or whether it was intended to provide keyword arguments to the parent method. This is because of a historical feature where Ruby allowed hashes to be passed as keyword-argument style parameters. For example:
def some_method(options={})
puts options.inspect
end
some_method(arg1: "Some argument", arg2: "Some other argument")
Prints:
{:arg1=>"Some argument", :arg2=>"Some other argument"}
If Ruby disallowed that syntax (which would break backwards compatibility with existing programs), you could write your child method like this using the double splat operator:
class Child < Parent
def foo(*args, named1: "child", named2: "child", **keyword_args)
puts "Passing to parent: #{[*args, named1: named1, **keyword_args].inspect}"
super(*args, named1: named1, **keyword_args)
end
end
In fact, this works fine when you pass keyword arguments in addition to the positional one:
Child.new.foo({ this: 23 }, named2: "caller")
Prints:
Passing to parent: [{:this=>23}, {:named1=>"child"}]
{:this=>23}
"child"
"parent"
However, since Ruby can't tell the difference between positional arguments and keyword arguments when you only pass a single hash, Child.new.foo({ this: 23 })
results in this: 23
getting interpreted as a keyword argument by the child, and the parent method ends up interpreting both keyword arguments forwarded to it as a single positional argument (a hash) instead:
Child.new.foo({this: 23})
Prints:
Passing to parent: [{:named1=>"child", :this=>23}]
{:named1=>"child", :this=>23}
"parent"
"parent"
There are a few ways you can fix this but none of them are exactly ideal.
As you tried to do in your third example, you could tell the child that the first argument passed will always be a positional argument, and that the rest will be keyword args:
class Child < Parent
def foo(arg, named1: "child", named2: "child", **keyword_args)
puts "Passing to parent: #{[arg, named1: named1, **keyword_args].inspect}"
super(arg, named1: named1, **keyword_args)
end
end
Child.new.foo({this: 23})
Child.new.foo({this: 23}, named1: "custom")
Prints:
Passing to parent: [{:this=>23}, {:named1=>"child"}]
{:this=>23}
"child"
"parent"
Passing to parent: [{:this=>23}, {:named1=>"custom"}]
{:this=>23}
"custom"
"parent"
Switch entirely to using named arguments. This avoids the problem entirely:
class Parent
def foo(positional:, named1: "parent", named2: "parent")
puts positional.inspect
puts named1.inspect
puts named2.inspect
end
end
class Child < Parent
def foo(named1: "child", named3: "child", **args)
super(**args, named1: named1)
end
end
Child.new.foo(positional: {this: 23})
Child.new.foo(positional: {this: 23}, named2: "custom")
Prints:
{:this=>23}
"child"
"parent"
{:this=>23}
"child"
"custom"
Write some code to figure everything out programmatically.
This solution would likely be pretty complex and would depend a lot on exactly how you want it to work, but the idea is that you would use Module#instance_method
, and UnboundMethod#parameters
to read the signature of the parent's foo method and pass arguments to it accordingly. Unless you really need to do it this way, I'd recommend using one of the other solutions instead.
From what I can tell, you want to:
I think you problem can be solved by capturing keyword arguments which are to be passed directly to the parent's method in a separate variable in the child's method, kwargs
, like this:
class Parent
def foo(positional, parent_kw1: "parent", parent_kw2: "parent")
puts "Positional: " + positional.inspect
puts "parent_kw1: " + parent_kw1.inspect
puts "parent_kw2: " + parent_kw2.inspect
end
end
class Child < Parent
def foo(*args, parent_kw1: "child", child_kw1: "child", **kwargs)
# Here you can use `child_kw1`.
# It will not get passed to the parent method.
puts "child_kw1: " + child_kw1.inspect
# You can also use `parent_kw1`, which will get passed
# to the parent method along with any keyword arguments in
# `kwargs` and any positional arguments in `args`.
super(*args, parent_kw1: parent_kw1, **kwargs)
end
end
Child.new.foo({this: 23}, parent_kw2: 'ABCDEF', child_kw1: 'GHIJKL')
This prints:
child_kw1: "GHIJKL"
Positional: {:this=>23}
parent_kw1: "child"
parent_kw2: "ABCDEF"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With