Debugging a large codebase is hard. Ruby makes debugging easier by exposing method metadata and caller stack inside Ruby's own process. Recently in Ruby 2.2.0 this meta inspection got another useful feature by exposing super method metadata. In this post we will look at how this information can be used to debug and why it needed to be added.
One of the first talks I ever wrote was "Dissecting Ruby With Ruby" all about inspecting and debugging Ruby processes using nothing but Ruby code. If you've never heard of the Method method it's worth a watch.
In short, Ruby knows how to execute your code, as well as where your code was defined. For example, with this small class:
class Dog
def bark
puts "woof"
end
end
We can see exactly where Dog#bark
is defined:
puts Dog.new.bark
# => "woof"
puts Dog.new.method(:bark).source_location.inspect
# => ["/tmp/dog.rb", 2]
Even if someone did some crazy metaprogramming or you accidentally over-wrote the method, Ruby will always tell you the location of the method it will call.
Super problems
If you've seen the "Dissecting Ruby" talk, you'll know that there is a big problem with the super method. It's almost impossible to tell where the final method location being called is written.
class SchneemsDog < Dog
def bark
super
end
end
I ended up using some metaprogramming to figure this out:
cinco = SchneemsDog.new
cinco.class.superclass.instance_method(:bark)
# => ["/tmp/dog.rb", 6]
This works, but it wouldn't if we did certain types of metaprogramming. For example, we would get the wrong answer if we did this:
module DoubleBark
def bark
super
super
end
end
cinco = SchneemsDog.new
cinco.extend(Doublebark)
In this case, cinco.bark
will call the method defined in the Doublebark
module:
cinco.bark
# => bark
# => bark
puts cinco.method(:bark)
#<Method: SchneemsDog(DoubleBark)#bark>
The actual "super" being referred to is defined in the SchneemsDog
class. However, the code tells us that the method is in the Dog
class, which is incorrect.
puts cinco.class.superclass.instance_method(:bark)
# => #<UnboundMethod: Dog#bark>
This is because our Doublebark
module isn't an ancestor of the cinco.class
. How can we solve this issue?
Super solutions
In feature request #9781, I proposed adding a method to allow Ruby to give you this information directly. Shortly after, one of my co-workers, Nobuyoshi Nakada, A.K.A. "The Patch Monster", attached a working patch, and it was accepted into the Ruby trunk (soon to become 2.2.0) around July.
If you are debugging in Ruby 2.2.0 you can now use Method#super_method. Using the same code we mentioned previously:
cinco = SchneemsDog.new
cinco.method(:bark).super_method
# => #<Method: Dog#bark>
You can see this returns the method on the Dog
class rather than the SchneemsDog
class. If we call source_location
in the output, we will get the correct value:
module DoubleBark
def bark
super
super
end
end
cinco = SchneemsDog.new
cinco.extend(Doublebark)
puts cinco.method(:bark)
# => #<Method: SchneemsDog(DoubleBark)#bark>
puts cinco.method(:bark).super_method
# => #<Method: SchneemsDog#bark>
Not only is this simpler, it's now correct. The return of super_method
will be the same method that Ruby will call when super
is invoked, regardless of whatever craziness is done with metaprogramming. Even though this is a simple example, I hope you'll find this useful in the wild.
Follow @schneems for Ruby articles and pictures of his dogs. Note that Cinco was not harmed in the making of this blog post