Real modules for Ruby

Ruby doesn’t have modules!

More specifically, Ruby doesn’t have real mixins (or interfaces or typeclasses if you like). But then what does a module mixin do? It modifies a class’s inheritance chain at runtime

class A; end
class B < A; end
B.ancestors # => [B, A, Object, Kernel]
module M; end
class B; include M end
B.ancestors # => [B, M, A, Object, Kernel]

So to set things straight in your head, just remember that a module is a class can only be instantiated as a superclass of a class that includes the module. include and extend are methods that modify the class inheritance chain at runtime.

problems with the current module hack

So the methods are not included, they are inherited. This may not be the desired behavior. From the pickaxe:

a Ruby include does not simply copy the module’s instance methods into the class. Instead, it makes a reference from the class to the included module. If multiple classes include that module, they’ll all point to the same thing. If you change the definition of a method within a module, even while your program is running, all classes that include that module will exhibit the new behavior.[Of course, we’re speaking only of methods here. Instance variables are always per-object, for example.]

I would call this a bug more than a feature. The other problem with modules is that importing their methods is all or nothing. Normally we only want some of the methods from a module, but we pollute the namespace and risk clobbering other methods by importing them all.

a solution

So a real module system should isolate an include module from outside changes and allow including at a per method basis. Here is a stripped down implementation without error checking:

def import(mod, *meths)
  mod_dup = mod.dup

  unless meths.empty?
    mod_dup.module_eval do
        (mod.instance_methods.map {|m| m.to_sym} - meths).each do |meth|
          remove_method meth
        end
      end
    end
  end

  include mod_dup
end

first the module is duplicated. This solves the problems of the imported module being changed in the future.

 class E; import M end
 E.ancestors # => [E, #<Module:0xb7c6d358>, Object, Kernel]

If the original module is changed, the duplicate module will not be effected. The second thing that the code does is remove methods from the module that we have not specified.

  module Foo
    def foo; 'foo' end
    def bar; 'bar' end
  end

  class Importer
    import Foo, :bar
  end
  Importer.new.bar # => 'bar'
  Importer.new.foo # => NoMethodError

remaining problems

One problem that I don’t know how to solve is that some module methods may depend on others. Like some of the other shortcomings in Ruby, this one will have to be solved by testing.

Update!

import imports all module private methods by default unless the option :import_private => false is passed.

I think this is a good compromise. Modules can be written so that public methods only depend on private methods, and users can choose which public methods to import. Private methods can be given verbose names to avoid namespace confilcts.

git it

The real deal includes error checking and lots of tests

gem install module-import

Browse the source or Git the source:

git clone git://github.com/gregwebs/module-import.git

documentation

© Greg Weber. All original code snippets are placed in the public domain.
Written on Tue Mar 11 16:26:54 2008.
Tagged as gem, ruby.
Send author an email.