Say I have parent class whose initializer has an argument with a default value:
class Parent
attr_reader :foo
def initialize(foo: 123)
@foo = foo
end
end
I want to create a subclass that has the same default value for foo
. I can do this if I repeat the declaration:
class Child < Parent
attr_reader :bar
def initialize(foo: 123, bar: 456)
super(foo: foo)
@bar = bar
end
end
However, this means I have to write 123
twice. If I try to avoid repeating it by leaving it out --
class Child < Parent
attr_reader :bar
def initialize(foo:, bar: 456)
super(foo: foo)
@bar = bar
end
end
-- this now means that the formerly optional, defaulted foo
is now required by the subclass, and I don't actually get any benefit from the default value at all.
I thought I might be able to default it to nil
in the subclass --
class Child < Parent
attr_reader :bar
def initialize(foo: nil, bar: 456)
super(foo: foo)
@bar = bar
end
end
-- but no; Child.new(bar:789).foo
is now nil
, when what I wanted was 123
.
I can't leave the leave the argument out entirely, either --
class Child < Parent
attr_reader :bar
def initialize(bar: 456)
super(foo: foo)
@bar = bar
end
end
-- because then if I do try to specify it (Child.new(foo: 345, bar:789)
) I get unknown keyword: foo (ArgumentError)
.
Is there any way to actually leave an argument out, as opposed to giving it a default value? And/or a way to allow an initializer to take arbitrary additional named paraemetrs and pass them to its superclass initializer?
Update: I came up with the following hack (hand-rolling my own 'default parameters', basically) but I'm not very happy about it.
class Parent
attr_reader :foo
def initialize(foo: nil)
@foo = foo || 123 # faking 'default'-ness
end
end
class Child < Parent
attr_reader :bar
def initialize(foo: nil, bar: 456)
super(foo: foo)
@bar = bar
end
end
Surely there's some more Ruby-ish way to do this?
In Ruby 2.0+, you can use the double splat operator.
def initialize(bar: 456, **args)
super(**args)
@bar = bar
end
An example:
[1] pry(main)> class Parent
[1] pry(main)* def initialize(a: 456)
[1] pry(main)* @a = a
[1] pry(main)* end
[1] pry(main)* end
=> :initialize
[2] pry(main)> class Child < Parent
[2] pry(main)* def initialize(b: 789, **args)
[2] pry(main)* super(**args)
[2] pry(main)* @b = b
[2] pry(main)* end
[2] pry(main)* end
=> :initialize
[3] pry(main)> ch = Child.new(b: 3)
=> #<Child:0x007fc00513b128 @a=456, @b=3>
[4] pry(main)> ch = Child.new(b: 3, a: 6829)
=> #<Child:0x007fc00524a550 @a=6829, @b=3>
The double splat operator is similar to the single splat operator, but instead of capturing all of the extra args into an array, it captures them into a hash. Then when used as an argument to super, the double splat flattens the hash into named parameters, kind of like the single splat does for arrays.