Featured Video Play Icon

Safely Call Superclass Methods from Ruby Modules

In today’s RubyTapas throwback, we tackle a metaprogramming topic. How do you reliably call superclass methods from included modules… when you can’t know for sure if those methods exist, or if they have been overridden out from under you? This video and article will show you some techniques you can use.

Director’s commentary: This was originally published on RubyTapas October 29th, 2012. The metaprogramming techniques shown here are still valid, if rarely needed. The video quality, on the other hand, makes me cringe… I wasn’t nearly as good at pacing back then, and I had a lot more tolerance for “whoopsies” in a three-minute video.

By the way, if you want to learn more about this kind of deep metaprogramming for libraries, you might like Much Ado About Null, a bonus ebook included in the Gold Edition of Confident Ruby.

Here’s the original episode video, and if you scroll down you’ll find the video script and code.

When authoring a reusable module, we may find we want to call a superclass method, but only if it exists. For instance, here’s a module which defines its own #hello method. We want to be able to include it in many different classes, some of which may inherit from other classes that define #hello.

If the class it is included in also inherits a #hello method from somewhere else, the module will simply embellish its output a bit. But since the including class might not define a #hello method, the module also includes its own full implementation. The question is, how do we tell if an ancestor implements #hello?

module YeOlde
  def hello(subject="World")
    if ???
      super
    else
      "Good morrow, #{subject}!"
    end
    puts "Well met indeed!"
  end
end

The answer is to use Ruby’s <a href="https://www.rubytapas.com/out/ruby--defined-eh">defined?</a> operator, with <a href="https://www.rubytapas.com/out/ruby--super">super</a> as its argument.

When we include this module in a class whose parent defines #hello, it uses the parent greeting. When we include it in a class with no #hello method, it uses its own greeting.

module YeOlde
  def hello(subject="World")
    if defined?(super)
      super
    else
      puts "Good morrow, #{subject}!"
    end
    puts "Well met indeed!"
  end
end

class Greeter
  def hello(subject)
    puts "Hello, #{subject}"
  end
end

class GreeterChild < Greeter
  include YeOlde
end

class NonGreeter
  include YeOlde
end

GreeterChild.new.hello("Bob")
NonGreeter.new.hello("Sally")
Hello, Bob
Well met indeed!
Good morrow, Sally!
Well met indeed!

Let’s look at another situation involving the use of <a href="https://www.rubytapas.com/out/ruby--super">super</a> in a module. Let’s say we have a module which defines a #logged_send method. #logged_send acts just like a call to Ruby’s <a href="https://www.rubytapas.com/out/ruby--object-send">#send</a>, except it also logs the method call and arguments.

module Logged
  def logged_send(name, *args, &block)
    puts "Sending #{name}(#{args.map(&:inspect).join(', ')})"
    send(name, *args, &block)
  end
end

When we include this module in most classes it works just fine.

class Greeter
  include Logged

  def hello(subject)
    puts "Hello, #{subject}"
  end
end

Greeter.new.logged_send(:hello, "Major Tom")

But one day we add in another module which overrides <a href="https://www.rubytapas.com/out/ruby--object-send">#send</a> to do something completely different. Suddenly, #logged_send doesn’t work so well.

module PigeonPost
  def send(*messages)
    # ...
    puts "Your message is winging its way to its recipient!"
  end
end
class Greeter
  include PigeonPost
  include Logged

  def hello(subject)
    puts "Hello, #{subject}"
  end
end

Greeter.new.logged_send(:hello, "Major Tom")

The problem here is that when the Logged module called #send, expecting the default Object implementation of #send, it got the PigeonPost implementation instead.

How can we ensure that Logged always gets the original definition of #send? Let’s take it step by step. Inside the #logged_send method, we first need to get a method object referring to the original definition of <a href="https://www.rubytapas.com/out/ruby--object-send">#send</a> from the <a href="https://www.rubytapas.com/out/ruby--object">Object</a> class.

original_send = Object.instance_method(:send)
#<UnboundMethod: Object(Kernel)#send>

This yields an <a href="https://www.rubytapas.com/out/ruby--unboundmethod">UnboundMethod</a> object. This object then needs to be bound to a specific object instance, in this case <a href="https://www.rubytapas.com/out/ruby--object-self">self</a>.

bound_send = original_send.bind(self)

This results in a callable Method object.

The last step is to call the <a href="https://www.rubytapas.com/out/ruby--method">Method</a> object.

bound_send.call(name, *args, &block)

When we put it all together and try again, things work as intended!

module Logged
  def logged_send(name, *args, &block)
    puts "Sending #{name}(#{args.map(&:inspect).join(', ')})"
    original_send = Object.instance_method(:send)
    bound_send = original_send.bind(self)
    bound_send.call(name, *args, &block)
  end
end

This isn’t the most straightforward technique in the world, and I don’t need it very often. But every now and then it’s a real lifesaver.

Now, a few viewers are probably yelling at their screens right now, saying “you should have just used <a href="https://www.rubytapas.com/out/ruby--object-__send__">#__send__</a> instead!”. To which I say: you’re absolutely right, at least for this example. But that’s a topic for another day.

Happy hacking!

1 comment

Leave a Reply

Your email address will not be published. Required fields are marked *

Success message!
Warning message!
Error message!