Ruby, modules, super and alias_method

In general, I really like the way Ruby handles class inheritance & module mix-ins. But, recently, I found an odd behavior - maybe a bug? - in Ruby 1.8.6 (the Ruby shipping with Leopard) when using alias_method and super inside an included module. (Other versions of Ruby may or may not have the behavior - I haven't tested them exhaustively.) Here's hoping this saves someone else some head-scratching down the line.

So, here's the scenario: You have one or more classes that inherit from a base class, and these include a module which conditionally overrides a method in the base class. All of this goes reasonably well, until you throw alias_method into the mix - then things get weird.

Oh, I guess there might be spoilers here, if you haven't seen the first season of Heroes yet.

Let's say this is our base class:

class Human
  def initialize(name)
    @name = name
  end

  def special
    puts "#{@name} is normal, no special abilities."
  end
  alias_method :abilities, :special
end
   mohinder = Human.new('Mohinder')
mohinder.special   # Mohinder is normal, no special abilities.
mohinder.abilities # Mohinder is normal, no special abilities.

Okay, cool. That works as we expect. Now, let's make the descendent classes:

class Hero < Human
  attr_accessor :powers

  def special
    unless @powers
      super
    else
      puts "#{@name} is a super - he/she can #{@powers}"
    end
  end
  alias_method :abilities, :special
end

class BadGuy < Human
  attr_accessor :powers

  def special
    unless @powers
      super
    else
      puts "#{@name} is a super - he/she can #{@powers}"
    end
  end
  alias_method :abilities, :special
end

But, we're not being very DRY here. Let's modularize!

module SuperPower
  attr_accessor :powers
  def special
    unless @powers
      super
    else
      puts "#{@name} is a super - he/she can #{@powers}"
    end
  end
  alias_method :abilities, :special
end

class Hero < Human
  include SuperPower
end

class BadGuy < Human
  include SuperPower
end

Right on! Fire that up!

mohinder = Human.new('Mohinder')
mohinder.special   # Mohinder is normal, no special abilities.
mohinder.abilities # Mohinder is normal, no special abilities.

peter = Hero.new('Peter Petrelli')
peter.powers = "mimic the powers of other supers nearby, fly, and read minds"
peter.special   # Peter Petrelli is a super - he/she can mimic the ...
peter.abilities # Peter Petrelli is a super - he/she can mimic the ...

sylar = BadGuy.new('Sylar')
sylar.powers = "steal powers from other supers, telekinetics, and super-hearing"
sylar.special   # Sylar is a super - he/she can steal powers ...
sylar.abilities # Sylar is a super - he/she can steal powers ...

hrg = Hero.new('Horn-Rimmed Glasses')
hrg.special   # Horn-Rimmed Glasses is normal, no special abilities.
hrg.abilities # NoMethodError: super: no superclass method 'special'

D'oh! Hunh? I think there's two really odd things about this error:

  1. super is apparently trying to call special when invoked as abilities
  2. there actually is a special method defined, abilities is "fake" method

I don't entirely comprehend why, but I suspect that the way Ruby internally masks the name of the invoked method confuses the heck out of both super and the standard error reporting mechanism.

Anyway... If you're actually seeing this oddity, the solution I found is to just avoid alias_method in modules. The old-fashioned way works just fine:

module SuperPower
  attr_accessor :powers
  def special
    unless @powers
      super
    else
      puts "#{@name} is a super - he/she can #{@powers}"
    end
  end

  def abilities
    special
  end
end

mohinder = Human.new('Mohinder')
mohinder.special   # Mohinder is normal, no special abilities.
mohinder.abilities # Mohinder is normal, no special abilities.

peter = Hero.new('Peter Petrelli')
peter.powers = "mimic the powers of other supers nearby, fly, and read minds"
peter.special   # Peter Petrelli is a super - he/she can mimic the powers ...
peter.abilities # Peter Petrelli is a super - he/she can mimic the powers ...

sylar = BadGuy.new('Sylar')
sylar.powers = "steal powers from other supers, telekinetics, and super-hearing"
sylar.special   # Sylar is a super - he/she can steal powers from ...
sylar.abilities # Sylar is a super - he/she can steal powers from ...

hrg = Hero.new('Horn-Rimmed Glasses')
hrg.special   # Horn-Rimmed Glasses is normal, no special abilities.
hrg.abilities # Horn-Rimmed Glasses is normal, no special abilities.

Permalink • Posted in: heroes, programming, ruby, super