Project

General

Profile

Actions

Bug #20440

closed

`super` from child class duplicating a keyword argument as a positional Hash

Added by ozydingo (Andrew Schwartz) 8 months ago. Updated 8 months ago.

Status:
Closed
Assignee:
-
Target version:
-
[ruby-core:117624]

Description

Apologies for the verbose title, but that's the specific set of conditions that AFAICT are required to reproduce the bug!

Here's the simplest setup I can reproduce:

class Base
  def foo(*args, x: 1)
    puts "Base: calling foo with args: #{args}, x: #{x}"
  end

  def foo!(x: 1)
    puts "Base: calling foo! with x: #{x}"
    foo(x: x)
  end
end

class Child < Base
  def foo(*)
    puts "Child: calling foo"
    super
  end
end

When I call Child.new.foo!, I expect it to call the base class method foo!, which will use the default keyword arg x: 1; then the child method foo with x: 1, and finally the base method foo with x: 1. However, this is not what I observe:

Child.new.foo!

Base: calling foo! with x: 1
Child: calling foo
Base: calling foo with args: [{:x=>1}], x: 1

So when the child foo method called super, it passed not only x: 1 as a keyword arg, but also {x: 1} as a Hash positional arg to the super method.

This is breaking my upgrade to Ruby 3.0 as I have a similar setup but without the *args param, this I am getting the error "wrong number of arguments (given 1, expected 0)".

Updated by ozydingo (Andrew Schwartz) 8 months ago · Edited

In fact it seems we can simplify this to just calling Child.new.foo(x: 1); no need for the base class foo! method.

Child.new.foo(x: 1)

Child: calling foo
Base: calling foo with args: [{:x=>1}], x: 1

Apologies if I'm misunderstanding * and super, but my understanding is super (without specifying arguments) should be passing all args as they are to the super method, and * should accept any combination of args, is that correct?

For example, defining the child class as

class Child < Base
  def foo(x: 1)
    super
  end
end

produces my expected behavior:

Base: calling foo with args: [], x: 1

It is surprising, then, that using * in the parameter list instead of explicitly naming the keyword arg causes the same call to pass both a Hash and a keyword arg to super.

Updated by nobu (Nobuyoshi Nakada) 8 months ago

You need to make foo ruby2_keywords to let it work as same as 2.7 or earlier.

class Child < Base
  ruby2_keywords def foo(*)
    puts "Child: calling foo"
    super
  end
end
Child.new.foo!
Base: calling foo! with x: 1
Child: calling foo
Base: calling foo with args: [], x: 1

Note that require 'ruby2_keyword' is necessary before ruby 2.7.

Updated by Eregon (Benoit Daloze) 8 months ago

See https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/
it also explains (*, **) and (...) which are better if you don't need compatibility with Ruby < 2.7.

Actions #4

Updated by Eregon (Benoit Daloze) 8 months ago

  • Status changed from Open to Closed

Updated by ozydingo (Andrew Schwartz) 8 months ago

Thanks both. I understand that Ruby 3 requires explicit handling of keyword arguments. What still seems off to me is that super is modifying the arguments. The child method is being passed a keyword argument, and super is forwarding keywords arguments and a Hash positional argument. Should it not be the case that either the method defined with only * does not accept keyword arguments or that super preserves the form of the arguments that were passed?

Actions #6

Updated by ozydingo (Andrew Schwartz) 8 months ago

  • Subject changed from `super` from child class passing keyword arg as Hash if in a method with passthrough args called from base class to `super` from child class duplicating a keyword argument as a positional Hash

Updated by zverok (Victor Shepelev) 8 months ago

What still seems off to me is that super is modifying the arguments.

If I understand correctly, what “modifies” the argument is child’s foo signature:

def foo(*) # <= this says “accept only positional args” (implicitly converting keyword ones to hash)
  super # <= this implicitly has a signature same as foo: super(*), passing only positional ones to the parent
end

The way to “fix” the code (if you own it) is this:

class Child < Base
  def foo(*, **) # the declaration that leaves keyword ones and positional ones separated
    puts "Child: calling foo"
    super # super is implicitly called as super(*, **), passing them separately
  end
end

The printed output:

Base: calling foo! with x: 1
Child: calling foo
Base: calling foo with args: [], x: 1

Updated by ozydingo (Andrew Schwartz) 8 months ago

Ok I see it now; super isn't passing the args as both forms, it's passing only as a positional Hash. The x: 1 is coming from my default kwarg, which I was blinded to as I attempted to reduce the example to a general form. Thanks all!

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0