Programming C, C++, Java, PHP, Ruby, Turing, VB
Computer Science Canada 
Programming C, C++, Java, PHP, Ruby, Turing, VB  

Username:   Password: 
 RegisterRegister   
 include and extend
Index -> Programming, Ruby -> Ruby Tutorials
View previous topic Printable versionDownload TopicRate TopicSubscribe to this topicPrivate MessagesRefresh page View next topic
Author Message
Cervantes




PostPosted: Fri Jun 23, 2006 9:00 pm   Post subject: include and extend

As an introduction to this tutorial, be sure to read wtd's tutorial on mix-ins.

include
By now we've all done things like:
code:
module Bar
    def bar
        puts "Barrry"
    end
end
class Foo
    include Bar
end
Foo.new.bar

But what really is include? Just like our old friends attr_reader and private (and others like them), include is a private class method. It takes one argument for each module you wish to mix in.

What does this tell us? It tells us that it is dynamic. Let's get an example: mixing in the Enumerable module. For Enumerable to work on an object, that object has to have a working 'each' method.
code:
class Foo
    def each
        yield 27
        yield 42
        yield 64
    end
    include Enumerable if Foo.instance_methods.include? "each"
end
a = Foo.new
a.each {|x| p x}
p a.to_a
p a.select {|x| x % 2 == 0}

output wrote:
27
42
64
[27, 42, 64]
[42, 64]

Or perhaps we can do something like this:
code:
module Bar
    def bar
        puts "Barrry"
    end
end
module Baz
    def baz
        puts "Bazzzy"
    end
end

$module_to_include = Baz
class Foo
    include $module_to_include
end

Yes, global variables -- they suck.

But this code is pretty useless. It's always going to mix in the Baz module, and never the Bar module. We could make a decision on which module to include based on the methods that the class defines, but that's rather boring. What if we could decide based on the object itself? Enter extend.

extend
extend is just like include, except it works on individual objects, not the class itself.
code:
module Bar
    def bar
        puts "Barrry"
    end
end
class Foo
end
a = Foo.new
a.extend Bar
a.bar

b = Foo.new
b.bar

output wrote:
Barrry
test.rb:13: undefined method `bar' for #<Foo:0xb7c88b9c> (NoMethodError)

So Bar got mixed into `a', but not into `b'. Let's look at a more interesting example.

Say I wanted to make a little RPG. I've got a PlayerCharacter class set up. My PlayerCharacter will have a certain level, hit points, mana points, and a class. By class, I mean he's a Fighter, a Rogue, a Wizard... that Dungeons and Dragons geek stuff.

We could set up each of the possible classes (Fighter, Rogue, Wizard) as a module and then let our PlayerCharacter object mix in that module. Not all PlayerCharacter objects will be of the same class, however, so we need to mix the module into the PlayerCharacter object, rather than the PlayerCharacter class.

code:
module Klass
    module Fighter
    end
end
class PlayerCharacter
    attr_reader :hp
    def initialize(name, klass)
        @name = name
        @level = 1
        @hp = @mp = 0
        extend klass
    end
end
sarevok = PlayerCharacter.new("Sarevok", Klass::Fighter)
sarevok.is_a? PlayerCharacter # => true
sarevok.is_a? Klass::Fighter  # => true

Seems like it would work nicely. But let's delve in a bit more.

Using super With Mixins
We've all used super before. When one class inherits from another, super calls the parent's version of the method you're currently within.

It does the same when mixing in modules. Notice in the above code that sarevok.is_a? Klass::Fighter returned true. It's almost as if Klass::Fighter is a parent class. Let's go back to include for a moment, for it is far easier when dealing with super.
code:
module Bar
    def bar
        puts "barrry"
    end
end

class Foo
    include Bar
    def bar
        puts "foooy"
        super
    end
end
Foo.new.bar

output wrote:
foooy
barrry

So we can use super to call the module's version of the method. Now what about extend? Using super to try to call a method from the mixed in module is much more tricky. Let's try it and see what happens.
code:
module Klass
    module Fighter
        def level_up
            puts "increase hp by 10"
        end
    end
end
class PlayerCharacter
    attr_reader :hp
    def initialize(name, klass)
        @name = name
        @level = 1
        @hp = @mp = 0
        extend klass
    end
    def level_up
        puts "Level up!"
        super
    end
end
sarevok = PlayerCharacter.new("Sarevok", Klass::Fighter)
sarevok.level_up

output wrote:
increase hp by 10

This is not the output I wanted. I was looking for
output hoped wrote:
Level up!
increase hp by 10

Why did I not get this output?

The reason has to do with the order that the methods are defined in. When the Ruby interpreter reaches the class definition, it goes through the class line by line and defines the methods. Then it reaches the line where we are creating Sarevok. This calls the initialize method, which calls the extend method. Now Klass::Fighter is mixed into the sarevok object. This brings in the new definition of `level_up'. This definition overrides the old definition.

If you really wanted to use this sort of structure but have it work the way we had originally hoped, you'd have to define the instance methods after mixing in Klass::Fighter. To do this, we might use instance_eval in the initialize mehod, or perhaps use the class << self idiom from within the initialize method. However, it is important to know that doing this is far less efficient than declaring instance methods as we normally do. The reason? Read Why's article on Metaclasses. In short, defining methods within the initialize method is defining those methods as singleton methods. But the methods are all the same from object to object, which is why they are normally stored in the PlayerCharacter class. This way, however, we are creating a metaclass for each PlayerCharacter object, but all those metaclasses are essentially the same. What a waste of space that would be.

Conclusion
Mixins are very powerful. Dynamically mixing a module into an object at run-time, using extend, opens a world of possibilities. Ruby's dynamic nature, once again, shines.
Sponsor
Sponsor
Sponsor
sponsor
Display posts from previous:   
   Index -> Programming, Ruby -> Ruby Tutorials
View previous topic Tell A FriendPrintable versionDownload TopicRate TopicSubscribe to this topicPrivate MessagesRefresh page View next topic

Page 1 of 1  [ 1 Posts ]
Jump to:   


Style:  
Search: