In Progress
Unit 1, Lesson 1
In Progress

Ancestral Behavior

Ruby’s inheritance chain is simple: the last module included wins. Sometimes this can cause problems: what if a module you include happens to have a method that collides with a method from the superclass? It’s not a common problem, but when it does happen it’s a real head-scratcher. In this episode, you’ll learn how to solve it.

Video transcript & code

Let's say we've got a WashingMachine class.

Given a WashingMachine object, we have access to some typical washing machine settings, like the current water level, the current wash cycle , and the temperature in the tub.

wm = WashingMachine.new
wm.water_level                  # => 0
wm.cycle                        # => :fill
wm.temperature                  # => 72

Now let's say we develop a new washing machine, that builds on the old one.

class StainMaster5000 < WashingMachine
  attr_accessor :clothes

  def initialize
    super
    @clothes = ["red shirt", "blue sock", "black sock"]
  end
end

Let's make an instance. This amazing new appliance can actually tell us what clothes are currently inside it!

sm = StainMaster5000.new
sm.clothes                      # => ["red shirt", "blue sock", "black sock"]

But why stop there? The StainMaster 5000 should have all the bells and whistles!

Let's include the Enumerable module, and then define the each method to iterate through the clothes.

class StainMaster5000
  # ...
  include Enumerable

  def each(&block)
    clothes.each(&block)
  end
end

Now we can do cool stuff like convert the machine directly to an array of clothes, sort the clothes , or even get a list of just the clothes' colors,, all directly from the washing machine object!

sm.to_a
# => ["red shirt", "blue sock", "black sock"]
sm.sort
# => ["black sock", "blue sock", "red shirt"]
sm.map{|g| g.split.first}
# => ["red", "blue", "black"]

OK, I admit it. This is one of the more contrived examples I've come up with on this show.

But I did it to highlight a problem that you may well run into some day, under less contrived circumstances.

Here's the problem. Let's take a second look back at the original WashingMachine attributes.

sm.water_level                  # => 0
sm.cycle                        # => #<Enumerator: #<StainMaster5000:0x005610...
sm.temperature                  # => 72

Hmmm… one of these things is not like the others. The water_level and temperature readers still look right, but the cycle indicator is now returning something very different than it used to.

The problem is, cycle is one of the methods that Enumerable defines. We can talk about what it does in another episode. The right now, it's just in the way.

What we'd like to do is to include all of the Enumerable methods, except any with names that conflict with base class methods. But that's not how Ruby module inheritance works. We can't be selective about which methods are brought in. It's all or nothing.

How do we say that this class should use the superclass definition of cycle, instead of the Enumerable definition? It's actually quite easy, but the solution is not at all obvious.

First off, we need to be able to acquire a handle on the original WashingMachine definition of cycle.

As we've seen in a few other episodes, we can do this by asking the WashingMachine class for the instance_method named :cycle.

WashingMachine.instance_method(:cycle)
# => #<UnboundMethod: WashingMachine#cycle>

In return, we get an UnboundMethod object.

Knowing this, let's return to the StainMaster5000 class definition.

After we include Enumerable, we send the define_method message to the class.

We tell it we're defining the method named :cycle.

Now typically, when we use define_method, we pass in a block which will make up the body of the method. But not this time.

In this case, we pass a second argument, which is the UnboundMethod we learned how to retrieve a moment ago.

class StainMaster5000
  # ...
  define_method(:cycle, WashingMachine.instance_method(:cycle))
end

When we re-test the reader methods, all is well again. cycle once again returns :fill.

sm.water_level                  # => 0
sm.cycle                        # => :fill
sm.temperature                  # => 72

This exploits an obscure capability of the define_method call:

Instead of a block, it can take an UnboundMethod object as a second argument. In effect, it acts almost like an aliasing mechanism: it gives us the ability to take method objects and plug them in under any name we want.

For instance, if we had wanted to leave the original Enumerable definition intact, but we could have renamed the method :wash_cycle instead.

class StainMaster5000
  # ...
  define_method(:wash_cycle, WashingMachine.instance_method(:cycle))
end

Admittedly, this is some deep metaprogramming, and hopefully you'll never need to use it. But it gives some insight into how methods in Ruby can be detached and then re-bound in other contexts.

Happy hacking!

Responses