The Power of Implementing Ruby in Ruby

If you're not familiar with Mixology, it's a gem that enables modules to be both mixed into and unmixed from objects, creating a great way to implement the state pattern in Ruby.

The First Test

The first test for Mixology is fairly straightforward. Let's say we have an object with a foo method that returns "foo from object", and a module with a foo method that returns "foo from mixin". When we mix in the module, foo should return "foo from mixin". We should then be able to unmix the module from the object and have the foo method return "foo from object" again.

def test_unmix
  object = Class.new { def foo; "foo from object"; end }.new
  mixin = Module.new { def foo; "foo from mixin"; end }

  object.mixin mixin
  assert_equal "foo from mixin", object.foo

  object.unmix mixin
  assert_equal "foo from object", object.foo
end

Getting the First Test to Pass

Using MRI, we're at the point where we need to drop down into C code to implement this. My C skills aren't too great, which is why Patrick Farley implemented Mixology. But my Ruby skills are much better. Because Rubinius implements as much of Ruby in Ruby as possible, we can get this test to pass with 100% Ruby code.

module Mixology
  def mixin(mod)
    reset_method_cache
    IncludedModule.new(mod).attach_to metaclass
    reset_method_cache
    self
  end
  
  def unmix(mod_to_unmix)
    last_super = metaclass
    this_super = metaclass.direct_superclass
    while this_super
      if (this_super == mod_to_unmix ||
          this_super.respond_to?(:module) && this_super.module == mod_to_unmix)
        reset_method_cache
        last_super.superclass = this_super.direct_superclass
        reset_method_cache
        return self
      else
        last_super = this_super
        this_super = this_super.direct_superclass
      end
    end
    self
  end
  
  protected
  
  def reset_method_cache
    self.methods.each do |name|
      name = self.metaclass.send(:normalize_name,name)
      Rubinius::VM.reset_method_cache(name)
    end
  end
end

Object.send :include, Mixology

And now if we run the test, we'll have green.

$ rubinius test/mixology_test.rb
require 'test/unit/testcase' has been deprecated
Loaded suite test/mixology_test
Started
.
Finished in 0.024741 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

It's not every day when you want implement something like unmixing a module. But if you do want to, Rubinius empowers you to do much more at the Ruby level. It's also a great way to learn more about Ruby's internals. For example, the IncludedModule class used in this implementation is how Ruby puts modules in the inheritance hierarchy for an object.

Source on Github

There are still 5 pending tests before Mixology's whole test suite passes in Rubinius. If you want to give it a shot, fork my github repo.