August 20, 2009

Posted by John

Tagged beginner and methods

Older: MongoMapper Indy.rb Presentation

Newer: More MongoMapper Awesomeness

Lookin' on Up...To the East Side

I am currently reading the Well-Grounded Rubyist by David Black. It is a great book and reading it reminds me of things I was confused on when I started in Ruby. One of those things was the path Ruby uses to figure out which method to call when inheritance and mixins are in play.

As I read it last night, I thought I should post about it, so here it goes. Let’s start with a simple class.

class A
  def foo
    puts 'foo in A'
  end
end

A.new.foo 
# foo in A

Inheritance

That was pretty straightforward. Next up lets look at inheritance.

class A
  def foo
    puts 'foo in A'
  end
end

class B < A
end

B.new.foo
# foo in A

Again, straight forward. And if I define foo in B, it calls foo in B as that is first in the lookup path.

class A
  def foo
    puts 'foo in A'
  end
end

class B < A
  def foo
    puts 'foo in B'
  end
end

B.new.foo
# foo in B

What if I wanted to call both foo in B and foo in A? That is where super comes in. It allows you to go up the chain and call methods.

class A
  def foo
    puts 'foo in A'
  end
end

class B < A
  def foo
    super
    puts 'foo in B'
  end
end

B.new.foo
# foo in A
# foo in B

Notice how when we call super foo in A and B is in the output as it called A and then B. You can call super at any point in the method. It doesn’t really matter.

Super

One note on super: if you call super without parentheses, it will call the next method up the chain with the same arguments that were passed in. If, however, you call super with parenthesis, like super(), you have to pass in the arguments you would like to send. This will make more sense with a simple example.

class A
  def foo(message)
    puts 'foo in A'
    puts "#{message} in A"
  end
end

class B < A
  def foo(message)
    super
    puts 'foo in B'
    puts "#{message} in B"
  end
end

B.new.foo('heyyooo! ')
# foo in A
# heyyooo!  in A
# foo in B
# heyyooo!  in B

Not that foo has the same signature in A and B so calling super automatically passed the message argument in B’s foo to A. What if we wanted to take another argument in B?

class A
  def foo(message)
    puts 'foo in A'
    puts "#{message} in A"
  end
end

class B < A
  def foo(message, bar)
    super
    puts 'foo in B'
    puts "#{message} in B"
    puts bar
  end
end

B.new.foo('heyyooo! ', 'baz')
# ArgumentError: wrong number of arguments (2 for 1)

No dice! Remember now what I mentioned about super with parenthesis. Lets put that in action.

class A
  def foo(message)
    puts 'foo in A'
    puts "#{message} in A"
  end
end

class B < A
  def foo(message, bar)
    super(message)
    puts 'foo in B'
    puts "#{message} in B"
    puts "#{bar} in B"
  end
end

B.new.foo('heyyooo! ', 'baz')
# foo in A
# heyyooo!  in A
# foo in B
# heyyooo!  in B
# baz in B

Botta bing bang boom! That is more like what we want.

Mixins

Ok, now that we have a grasp on looking up methods in normal classes and classes that have a superclass, lets throw mixins into the mix.

module Fooish
  def foo
    puts 'foo in Fooish'
  end
end

class A
  include Fooish
end

A.new.foo
# foo in Fooish

So, the foo method was not defined in A but was defined in Fooish and it worked just as we expected. What if we define the method both in A and Fooish? Lets give it a try.

module Fooish
  def foo
    puts 'foo in Fooish'
  end
end

class A
  include Fooish
  
  def foo
    puts 'foo in A'
  end
end

A.new.foo
# foo in A

Groovy. That is pretty straightforward as well. Now, remember super? Yeah, super is our friend. Lets say you want to call the method in A and then in Fooish.

module Fooish
  def foo
    puts 'foo in Fooish'
  end
end

class A
  include Fooish
  
  def foo
    super
    puts 'foo in A'
  end
end

A.new.foo
# foo in Fooish
# foo in A

Ding! Pretty cool right. So mixins are just that, mixins. They get mixed in between your class and its superclass. Speaking of superclass, lets do check out inheritance AND mixins.

module Fooish
  def foo
    puts 'foo in Fooish'
  end
end

class A
  include Fooish
end

class B < A
end

B.new.foo
# foo in Fooish

B gets foo from Fooish which is mixed into A, B’s superclass. Cool. Lets make it a bit crazier.

module Fooish
  def foo
    puts 'foo in Fooish'
  end
end

class A
  include Fooish
end

class B < A
  def foo
    super
    puts 'foo in B'
  end
end

B.new.foo
# foo in Fooish
# foo in B

Again, things are working like we would expect based on what we have learned above. Lets put some methods all over the place so we can see the order of what is happening.

module Fooish
  def foo
    puts 'foo in Fooish'
  end
end

class A
  include Fooish
  
  def foo
    super
    puts 'foo in A'
  end
end

class B < A
  def foo
    super
    puts 'foo in B'
  end
end

B.new.foo
# foo in Fooish
# foo in A
# foo in B

Again, this was a bit more complex, but we get the order we would expect. What if we have 2 mixins?

module Fooish
  def foo
    puts 'foo in Fooish'
  end
end

module Barish
  def foo
    puts 'foo in Barish'
  end
end

class A
  include Fooish
  include Barish
end

A.new.foo
# foo in Barish

Ok, so we got foo in Barish, so that means that the lookup is in reverse order of how modules are included. Maybe the more straightforward way of saying that is the last module included is going to ding first. The kind of interesting thing is that you can even use super in your modules.

module Fooish
  def foo
    puts 'foo in Fooish'
  end
end

module Barish
  def foo
    super
    puts 'foo in Barish'
  end
end

class A
  include Fooish
  include Barish
end

A.new.foo
# foo in Fooish
# foo in Barish

Pretty cool. I would not really recommend doing this as if Barish was the only module included and you called foo, you would get an error. Lets do it just so we can see.

module Fooish
  def foo
    puts 'foo in Fooish'
  end
end

module Barish
  def foo
    super
    puts 'foo in Barish'
  end
end

class A
  include Barish
end

A.new.foo
# NoMethodError: super: no superclass method ‘foo’

Still, pretty neat how it works. What if we mixin in the same module twice?

module Fooish
  def foo
    puts 'foo in Fooish'
  end
end

module Barish
  def foo
    puts 'foo in Barish'
  end
end

class A
  include Fooish
  include Barish
  include Fooish
end

A.new.foo
# foo in Barish

As you can see it had no effect. If the module gets included again it doesn’t change the original lookup order. Lets review the method lookup path.

  1. the class
  2. modules mixed in, in reverse order
  3. superclass (inheritance)
  4. modules mixed in to superclass, in reverse order
  5. rinse and repeat all the way to Object in the Ruby 1.8 series and BasicObject in Ruby 1.9

Conclusion

Ruby’s method lookup path is very straightforward, but confusing at first. Once you learn how it works you can really take advantage of it to write more clean and structured code. Hope this was helpful. Go buy David’s great book for even more goodies like this.

1 Comment

  1. I love how intuitive Ruby is. Everything seems to work as it should, especially with this sort of stuff. Great read.

Sorry, comments are closed for this article to ease the burden of pruning spam.

About

Authored by John Nunemaker (Noo-neh-maker), a programmer who has fallen deeply in love with Ruby. Learn More.

Projects

Flipper
Release your software more often with fewer problems.
Flip your features.