Ruby, iOS, and Other Development

A place to share useful code snippets, ideas, and techniques

All code in posted articles shall be considered public domain unless otherwise noted.
Comments remain the property of their authors.

2006-06-09

Mixing in Class Methods

It is pretty common to have some conceptual block of functionality that is appropriate for a module, except it involves a combination of instance methods and class methods. The issue of singleton methods on a module not being added as singleton methods on a class when mixing in the module has come up repeatedly on the mailing list. While it is a compelling argument that wanting to mix in class methods as well as instance methods is the common case, having that as the only behavior prevents some desirable flexibility (particularly involving DSLs).

Instead, consider the following idiom (Updated! thanks, Sas):

module M
 module ClassMethods; end
 def self.included(klass)
   klass.extend(ClassMethods)
 end

 def foo
   puts "Calling class bar() method from foo()"
   self.class.bar
 end

 module ClassMethods
   def bar
     puts "Called #{self}::bar()"
   end
 end
end

We can now define methods intended to be mixed into the including class in the M::ClassMethods module. This is a naïve solution, however, and does not handle the situation in which a module is included in another module. Unfortunately, the simplicity of a ClassMethods module can't deal with including one module in another since the same ClassMethods module would be shared across both modules. In addition, we might like to provide some code to be called when our module is included, but the Module#included is what we use to make the class method mixin work. To make this easier, let's push the functionality into the Module class (which is the superclass of all modules just as Class is the superclass of all classes):

class Module
 private

 module MixinClassMethods
   def included_by_module(klass)
     #check to see if klass is already set up
     if not klass.instance_variables.include? '@class_method_module'
       klass.send(:mixin_class_methods)
     end
     klass_method_module =
       klass.instance_variable_get('@class_method_module')
     klass_method_module.send(:include, @class_method_module)
   end

   def included(klass)
     @extra_include_block.call(klass) if @extra_include_block
     case klass
       when Class
         klass.extend(@class_method_module)
       when Module
         #more work to include in a module
         included_by_module(klass)
     end
   end

   def define_class_methods(&block)
     @class_method_module.module_eval &block
   end

 end

 def mixin_class_methods(&block)
   #ensure the existence of the ClassMethods module
   if not (Module === (@class_method_module ||= Module.new))
     fail "@class_method_module is not a module!"
   end
   @extra_include_block = block
   extend MixinClassMethods
 end

end
It is now possible to do the following:
require 'mixin_class_methods'

module M
 mixin_class_methods { |klass|
   puts "Module M has been included by #{klass}"
 }

 def foo
   puts "Calling class bar() method from foo()"
   self.class.bar
 end

 define_class_methods {
   def bar
     puts "Called #{self}::bar()"
   end
 }
end

module N
 include M
end

class Baz
 include N
end

Baz.bar
Baz.new.foo
This will produce the following output:
Module M has been included by N
Called Baz::bar()
Calling class bar() method from foo()
Called Baz::bar()
Enjoy!

Labels: ,

3 Comments:

  • At 7/04/2006 01:30:00 PM, Anonymous Anonymous said…

    Nice blog, Gregory

    But I got stuck right at the begining

    I have a file test.rb with the following
    --------------

    module M
    module ClassMethods; end
    def included(klass)
    klass.extend(ClassMethods)
    end

    def foo
    puts "Calling class bar() method from foo()"
    self.class.bar
    end

    module ClassMethods
    def bar
    puts "Called #{self}::bar()"
    end
    end
    end

    class C
    include M
    end

    c = C.new
    c.foo

    -------------

    when I execute I get the following

    >ruby C:\temp\ruby\test2.rb
    Calling class bar() method from foo()

    C:/temp/ruby/test2.rb:9:in `foo': undefined method `bar' for C:Class (NoMethodError)
    from C:/temp/ruby/test2.rb:24
    >exit

    I guess I must be doing something wrong...

     
  • At 7/04/2006 02:17:00 PM, Blogger Unknown said…

    I've found the problem

    where it says
    ---
    def included(klass)
    klass.extend(ClassMethods)
    end
    ---
    should say
    ---
    def M.included(klass)

    or even better

    def self.included(klass)
    ---

    Saludos

    Sas

     
  • At 12/12/2007 02:29:00 PM, Anonymous Anonymous said…

    module Base
      def self.included( dest_module )
     super

        def dest_module.module_name
          self.name
        end

      end
    end

    module User
      include Base
    end

    >> User.module_name
    => "User"
    >> User.singleton_methods
    => ["module_name"]

    One thing I am struggling with is how do document the likes of module_name

     

Post a Comment

<< Home