Search code examples
rubysuperclassnamed-parameters

Avoid repeating named argument defaults when calling superclass initializer in Ruby (2.1+)


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?


Solution

  • 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.