Project

General

Profile

Actions

Feature #9781

closed

Feature Proposal: Method#super_method

Added by schneems (Richard Schneeman) almost 10 years ago. Updated about 9 years ago.

Status:
Closed
Target version:
[ruby-core:62202]

Description

When super is called in a method the Ruby VM knows how to find the next ancestor that has that method and call it. It is difficult to do this manually, so I propose we expose this information in Method#super_location.

Ruby Method class (http://www.ruby-doc.org/core-2.1.1/Method.html) is returned by calling Object.method and passing in a method name (http://www.ruby-doc.org/core-2.1.1/Object.html#method-i-method). This is useful for debugging:

# /tmp/code.rb
class Foo
  def bar
  end
end

puts Foo.new.method(:bar).source_location
# => ["/tmp/code.rb", 3]

The Object#method allows a ruby developer to easily track the source location of the method and makes debugging very easy. However if the code is being invoked by a call to super it is difficult to track down:

# /tmp/code.rb

class BigFoo
  def bar
  end
end

class Foo < BigFoo
  def bar
    super
  end
end

In this code sample it is easy to find the method definition inside of Foo but it is very difficult in large projects to find what code exactly super is calling. This simple example is easy, but it can be hard when there are many ancestors. Currently if I wanted to find this we can inspect ancestors

Foo.ancestors[1..-1].map do |ancestor|
  next unless ancestor.method_defined?(:bar)
  ancestor.instance_method(:bar)
end.compact.first.source_location

To make this process simpler I am proposing a method on the Method class that would return the result of super

It could be called like this:

Foo.new.method(:bar).super_method

I believe adding Method#super_method, or exposing this same information somewhere else, could greatly help developers to debug large systems easily.


Related issues 2 (0 open2 closed)

Related to Ruby master - Feature #7836: Need a way to get Method and UnboundMethod objects to methods overridden by prepended modulesClosedmatz (Yukihiro Matsumoto)Actions
Has duplicate Ruby master - Feature #10216: Add methods to Method and UnboundMethod classess to retrieve method instance for superClosedmatz (Yukihiro Matsumoto)Actions

Updated by tenderlovemaking (Aaron Patterson) almost 10 years ago

I also have this problem especially when debugging code where modules are mixed in at runtime, so I have to do something like this:

class << obj; self; end.ancestors.find_all { |klass|
  klass.method_defined? :foo
}.map { |klass| klass.method(:foo).source_location }.last

(or something similar). A method to get the "super method" would be very convenient.

Updated by nobu (Nobuyoshi Nakada) almost 10 years ago

  • Description updated (diff)

A patch.
No tests yet.

diff --git a/proc.c b/proc.c
index 8153cc9..d1db478 100644
--- a/proc.c
+++ b/proc.c
@@ -1481,11 +1481,17 @@ method_owner(VALUE obj)
     return defined_class;
 }
 
-void
-rb_method_name_error(VALUE klass, VALUE str)
+struct method_name_error {
+    VALUE class_name;
+    const char *type;
+};
+
+static struct method_name_error
+prepare_method_name_error(VALUE klass)
 {
     const char *s0 = " class";
     VALUE c = klass;
+    struct method_name_error e;
 
     if (FL_TEST(c, FL_SINGLETON)) {
 	VALUE obj = rb_ivar_get(klass, attached);
@@ -1500,8 +1506,22 @@ rb_method_name_error(VALUE klass, VALUE str)
     else if (RB_TYPE_P(c, T_MODULE)) {
 	s0 = " module";
     }
-    rb_name_error_str(str, "undefined method `%"PRIsVALUE"' for%s `%"PRIsVALUE"'",
-		      QUOTE(str), s0, rb_class_name(c));
+    e.class_name = rb_class_name(c);
+    e.type = s0;
+    return e;
+}
+
+#define method_name_error(klass, str, t) do { \
+	struct method_name_error e = prepare_method_name_error(klass); \
+	rb_name_error_str(str, t" method `%"PRIsVALUE"' for%s `%"PRIsVALUE"'", \
+			  QUOTE(str), e.type, e.class_name); \
+    } while (0)
+
+
+void
+rb_method_name_error(VALUE klass, VALUE str)
+{
+    method_name_error(klass, str, "undefined");
 }
 
 /*
@@ -2430,6 +2450,23 @@ method_proc(VALUE method)
     return procval;
 }
 
+static VALUE
+method_super_method(VALUE method)
+{
+    struct METHOD *data;
+    VALUE defined_class, super_class;
+
+    TypedData_Get_Struct(method, struct METHOD, &method_data_type, data);
+    defined_class = data->defined_class;
+    if (BUILTIN_TYPE(defined_class) == T_MODULE) defined_class = data->rclass;
+    super_class = RCLASS_SUPER(defined_class);
+    if (!super_class) {
+	method_name_error(defined_class, rb_id2str(data->id), "no superclass");
+    }
+    return mnew(super_class, data->recv, data->id,
+		rb_obj_class(method), FALSE);
+}
+
 /*
  * call-seq:
  *   local_jump_error.exit_value  -> obj
@@ -2735,6 +2772,7 @@ Init_Proc(void)
     rb_define_method(rb_cMethod, "unbind", method_unbind, 0);
     rb_define_method(rb_cMethod, "source_location", rb_method_location, 0);
     rb_define_method(rb_cMethod, "parameters", rb_method_parameters, 0);
+    rb_define_method(rb_cMethod, "super_method", method_super_method, 0);
     rb_define_method(rb_mKernel, "method", rb_obj_method, 1);
     rb_define_method(rb_mKernel, "public_method", rb_obj_public_method, 1);
     rb_define_method(rb_mKernel, "singleton_method", rb_obj_singleton_method, 1);
@@ -2756,6 +2794,7 @@ Init_Proc(void)
     rb_define_method(rb_cUnboundMethod, "bind", umethod_bind, 1);
     rb_define_method(rb_cUnboundMethod, "source_location", rb_method_location, 0);
     rb_define_method(rb_cUnboundMethod, "parameters", rb_method_parameters, 0);
+    rb_define_method(rb_cUnboundMethod, "super_method", method_super_method, 0);
 
     /* Module#*_method */
     rb_define_method(rb_cModule, "instance_method", rb_mod_instance_method, 1);

Updated by Anonymous almost 10 years ago

I would also find this feature very useful when debugging large code bases with complicated class/module hierarchies.

Updated by ko1 (Koichi Sasada) almost 10 years ago

What should happen on #call method with method objects returned from super_method?

Updated by nobu (Nobuyoshi Nakada) almost 10 years ago

It's an ordinary Method (or UnboundMethod) instance, same as SuperClass.instance_method(:foo).bind(obj).

Updated by ko1 (Koichi Sasada) almost 10 years ago

(2014/05/01 1:35), wrote:

It's an ordinary Method (or UnboundMethod) instance, same as SuperClass.instance_method(:foo).bind(obj).

Oh, OK. I misunderstood the spec of Method class.

--
// SASADA Koichi at atdot dot net

Updated by zenspider (Ryan Davis) almost 10 years ago

Maybe it is just me misunderstanding something... but this doesn't seem that complicated. I don't understand why you'd walk through the ancestors when you just just ask one level up:

class BigFoo
  def bar
  end
end

class Foo < BigFoo
  def bar
    super
  end
end

class Object
  def super_method name
    self.class.superclass.instance_method name
  end
end

p Foo.new.method(:bar).source_location.last                           # => 7
p Foo.new.class.superclass.instance_method(:bar).source_location.last # => 2
p Foo.new.super_method(:bar).source_location.last                     # => 2

Updated by marcandre (Marc-Andre Lafortune) almost 10 years ago

Hi,

Ryan Davis wrote:

Maybe it is just me misunderstanding something... but this doesn't seem that complicated. I don't understand why you'd walk through the ancestors when you just just ask one level up:

For one level, with simple class inheritance, it probably isn't, but throw in a prepended module or two, and the desire to go up more than one level (i.e. obj.method(:foo).super_method.super_method) and it's not completely trivial.

If ever we support a module being included more than once in the ancestry chain, then it's actually impossible (unless Method provides a more extensive api to tell where it is in the ancestry chain).

I'm +1 for this feature, although I would favor the shorter name super. I don't think it would ever conflict with the keyword, and if it did there is still the same solution as with class, i.e. self.super.

Updated by marcandre (Marc-Andre Lafortune) almost 10 years ago

Nobuyoshi Nakada wrote:

It's an ordinary Method (or UnboundMethod) instance, same as SuperClass.instance_method(:foo).bind(obj).

Agreed for Method, but I'm not sure I understand how we could define UnboundMethod#super_method, since a Module can be part of different ancestry chains.

# same example as original post continued
module M
  def bar
  end
end
Foo.include bar

Foo.ancestors # => [Foo, M, BigFoo, ...]
Foo.new.method(:bar).super_method.super_method.owner # => BigFoo
Foo.instance_method(:bar).super_method.super_method.owner # => Can't possible give meaningful result

(I didn't try the patch)

Updated by marcandre (Marc-Andre Lafortune) almost 10 years ago

I must be tired.

Nobu: Sorry, I was confused, there's no problem with UnboundMethod#super_method because we retain which class this method was accessed from.

Ryan: That's actually a good example, there is no api to get the "original owner" of an unbound method, so in general it's not possible to implement super_method in Ruby. E.g. from Hash.instance_method(:map), I don't know of a way to get back Hash in pure Ruby, and thus no way to go through Hash's ancestry chain.

Updated by zenspider (Ryan Davis) almost 10 years ago

Maybe I'm still not getting something. If you can call it (or super to it) you can grab it:

class BigFoo
  def bar
  end
end

class Middle1 < BigFoo; end
class Middle2 < Middle1; end
class Middle3 < Middle2; end
class Middle4 < Middle3; end
class Middle5 < Middle4; end

class Foo < Middle5
  def bar
    super
  end
end

class Object
  def super_method name
    self.class.superclass.instance_method name
  end
end

p Foo.new.method(:bar).source_location.last                           # => 13
p Foo.new.class.superclass.instance_method(:bar).source_location.last # => 2
p Foo.new.super_method(:bar).source_location.last                     # => 2

Updated by schneems (Richard Schneeman) almost 10 years ago

Ryan, using superclass gets you really close, but doesn't handle extending object instances:

class BigFoo
  def bar
  end
end

class Foo < BigFoo
  def bar
    super
  end
end

class Object
  def super_method name
    self.class.superclass.instance_method name
  end
end


module MixinFoo
  def bar
    puts "MixinFoo"
  end
end

foo = Foo.new
foo.extend(MixinFoo)
foo.bar # => "MixinFoo"

puts foo.super_method(:bar)
#<UnboundMethod: BigFoo#bar> # wrong return

This is why my original code snippet uses ancestors rather than just superclass. We should see MixinFoo rather than BigFoo.

Updated by josh.cheek (Josh Cheek) almost 10 years ago

I've only ever needed to do this in codebases like Rails, where there's an insane amount of inheritance. However, Rails also relies heavily on method_missing, which super_method would completely miss.

Anyway, I'd like this method to exist also.

Here's how pry does it: https://github.com/pry/pry/blob/06341dfcdd53dc77e36761a974e6c930940dfb86/lib/pry/method.rb#L382

Here's my crack at implementing it:

class Method
  def super_method
    receiver.singleton_class
            .ancestors
            .drop_while { |ancestor| ancestor != owner }
            .drop(1)
            .first
            .instance_method(name)
            .bind(receiver)
  rescue NameError
    raise NameError, "No super method `#{name}' for `#{inspect}'"
  end
end


class BigFoo
  def bar
  end
end

# toss in some superclass spam to show it doesn't matter
class Foo < Class.new(Class.new(Class.new(BigFoo)))
  def bar
  end
end

module MixinFoo 
  def bar
  end
end

Foo.new               # => #<Foo:0x007fc123148728>
   .extend(MixinFoo)  # => #<Foo:0x007fc123148728>
   .method(:bar)      # => #<Method: Foo(MixinFoo)#bar>
   .super_method      # => #<Method: Foo(Foo)#bar>
   .super_method      # => #<Method: Foo(BigFoo)#bar>

Updated by nobu (Nobuyoshi Nakada) almost 10 years ago

  • Related to Feature #7836: Need a way to get Method and UnboundMethod objects to methods overridden by prepended modules added

Updated by ko1 (Koichi Sasada) almost 10 years ago

  • Assignee set to matz (Yukihiro Matsumoto)
  • Target version set to 2.2.0

Updated by matz (Yukihiro Matsumoto) almost 10 years ago

  • Status changed from Open to Feedback

Having something like Method/UnboundMethod#super_method for debugging purpose is OK, but

  • Is super_method is the right name? It sounds little bit weird for me.
  • I don't like Object#super_method.

Matz.

Updated by schneems (Richard Schneeman) almost 10 years ago

I am also not in love with the naming. I would have preferred Method#super but we don't want to overwrite super or confuse functionality here. Maybe something along the lines of get_super. Does anyone have a better naming suggestion?

Updated by marcandre (Marc-Andre Lafortune) almost 10 years ago

I also favor super (as I wrote before). It wouldn't overwrite the super keyword in most cases, and if it does (say from an instance method of Method) then there is still self.super.

Updated by matz (Yukihiro Matsumoto) over 9 years ago

OK, accepted.

The name should be 'super_method', since 'super' make me feel invocation of a mehtod in super class.
The 'super_method' should return nil when there's no method in superclasses.

Matz.

Updated by matz (Yukihiro Matsumoto) over 9 years ago

  • Status changed from Feedback to Open
  • Assignee changed from matz (Yukihiro Matsumoto) to okkez (okkez _)

Updated by nobu (Nobuyoshi Nakada) over 9 years ago

  • Status changed from Open to Closed
  • % Done changed from 0 to 100

Applied in changeset r46964.


proc.c: method_super_method

  • proc.c (method_super_method): new method Method#super_method,
    which returns a method object of the method to be called by
    super in the receiver method object.
    [ruby-core:62202] [Feature #9781]

Updated by aledovsky (Aleksandrs Ļedovskis) over 9 years ago

Yukihiro Matsumoto wrote:

OK, accepted.

The name should be 'super_method', since 'super' make me feel invocation of a mehtod in super class.
The 'super_method' should return nil when there's no method in superclasses.

Matz.

Wouldn't it be more consistent to name this method supermethod, i.e. without "_" in the middle?
Thus it would be logical extension to super<something> family, which now consists of Class#superclass

Updated by nobu (Nobuyoshi Nakada) over 9 years ago

  • Has duplicate Feature #10216: Add methods to Method and UnboundMethod classess to retrieve method instance for super added

Updated by Eregon (Benoit Daloze) about 9 years ago

Marc-Andre Lafortune wrote:

I must be tired.

Nobu: Sorry, I was confused, there's no problem with UnboundMethod#super_method because we retain which class this method was accessed from.

Ryan: That's actually a good example, there is no api to get the "original owner" of an unbound method, so in general it's not possible to implement super_method in Ruby. E.g. from Hash.instance_method(:map), I don't know of a way to get back Hash in pure Ruby, and thus no way to go through Hash's ancestry chain.

Actually there is a caveat with an UnboundMethod created with Module#instance_method, from a module (and not a class), as it has no idea what is the actual super method (as that can change in different ancestry chains).
Instead it will just return a method if there is a module "M" included in the former module with a corresponding method, and that method might be somewhere in the ancestry of an object including M or not (if "M" was included after).

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0