You are currently browsing the monthly archive for September 2009.

In studying the inheritance of methods I came across what I consider a surprising behavior of modules included into classes.

First, when you include a module into a class, the module methods are available in the class and they also propagate down to subclasses. This is reflected in the fact that the module is added to the ancestors of both the including class and all subclasses (be the defined before or after the include).

 
class LateIntoClassTest < Test::Unit::TestCase
  
  class A
  end
  
  class B < A
  end
  
  module LateInClass
  end
  
  class A
    include LateInClass
  end
  
  class C < A
  end
  
  def test_including_a_module_into_a_superclass_adds_to_ancestors
    # LateInClass is added to A
    assert_equal [A, LateInClass, Object, Kernel], A.ancestors
    
    # LateInClass is added to B
    assert_equal [B, A, LateInClass, Object, Kernel], B.ancestors
    
    # LateInClass is added to C
    assert_equal [C, A, LateInClass, Object, Kernel], C.ancestors
  end
end

What is surprising (at least to me), is that the same is not true when you include a module into an included module. I thought modules were kind of like superclasses; if you add to a module then you add to everything that uses the module. Not so.

Here you can see LateInModule is not added to classes that already include A. By contrast, classes defined after the ‘late’ include will add LateInModule to their ancestors.

 
class LateIntoModuleTest < Test::Unit::TestCase

  module A
  end
  
  class B
    include A
  end
  
  module LateInModule
  end
  
  module A
    include LateInModule
  end
  
  class C
    include A
  end
  
  def test_including_into_an_included_module_DOES_NOT_add_to_ancestors
    # LateInModule is added to A
    assert_equal [A, LateInModule], A.ancestors
    
    # LateInModule is missing from B
    assert_equal [B, A, Object, Kernel], B.ancestors
    
    # LateInModule is added to C
    assert_equal [C, A, LateInModule, Object, Kernel], C.ancestors
  end
end

You might take from this the lesson that modules are not superclasses, they are collections of methods poured into a class by include. But that isn’t the full story either. After all, you can modify a module and still have those changes propagate into an including class.

 
class LateModuleModificationTest < Test::Unit::TestCase

  module A
  end
  
  class B
    include A
  end
  
  module A
    def late_method; true; end
  end
  
  def test_included_modules_MAY_be_modified
    assert_equal true, B.new.late_method
  end
end

I find it tricky to express this behavior descriptively, even though the cause is clear; modules only add to ancestors when first included in a class.

Update: for those who are interested, Redmine has a feature request regarding how modules get added to ancestors.