A Tale of Three Modules

; updated

In which a weird problem with testing and simply_helpful leads us down the rabbit hole to appreciate the way Ruby modules are included into classes, and how the ordering of such events can be important.

(This is a long one folks; sorry about that)

Ruby is so forgiving. As _why so eloquently stated, “Ruby loves me like Japanese Jesus!”.

I’ve been working in Ruby for, I guess, over five years now, and so I’ve come to take this love for granted. Ruby lets me do whatever I want. Let’s write some code, yeah?

class Monkey
  def name
    "Bonobo"
  end
end

mike = Monkey.new
mike.name

Yeah, real nice. Real sweet monkey there. But hang on, I just realised that I forgot to add a method! Aaaargh!!? I cannot has cheezeburger? :’(

No, wait. This is Ruby, remember? Ruby will forgive me, and let me add the method to the class any time I like!

class Monkey
  def eat(food)
    case food
    when Banana
      "yum"
    else
      throw :poop
    end
  end
end

mike.eat :some_rubbish

Phew! Thank you Ruby! You are so kind! So leniant! You never scold me with a hurtful recompile. Except…

.. it’s not always that simple. Let’s drive over to the car-keys-in-a-punch-bowl mixin party to see how Ruby can sometimes behave in ways less forgiving.

module A
end
module B
  include A
end
module C
  include B
end
C.ancestors # => [C, B, A]

The ancestors method shows every module - and I mean every module - that has been “mixed in” to the receiving class or module. So, in this case, we see that C has included B, and also has included A (because B included A). This is obvious when we define some methods in these modules:

module A
  def a; end
end
module B
  include A
  def b; end
end
module C
  include B
  def c; end
end
C.instance_methods # => ["a", "c", "b"]

All our methods are there. And, while Ruby smiles kindly down upon us, we can even add methods to those modules afterwards and have them available. Check it:

module A
  def new_a; end
end
C.instance_methods # => ["a", "new_a", "c", "b"]

But now watch this. We’re going to build our three modules as before, except that we’re going to include a new module into A after A has been included into B

module A
end
module B
  include A
end
module X
end
module A
  include X
end
B.ancestors
# => [B, A]

So you see, even though A now includes X (so we’d expect to see [B, A, X] as the ancestor list), it doesn’t work. We can check the ancestors of module A to be sure that we really did include X in there:

A.ancestors
# => [A, X]

Yup, we did. And it gets even hairier. Methods defined on our “missing” module are not available to classes that include B.

module X
  def hello
    "dave"
  end
end
class D
  include B # which includes A, which we'd hope (but doesn't) include X
end
d = D.new
d.hello
# => NoMethodError: undefined method 'hello' for #<D:0x4ec14>

So here’s the deal - when Ruby included A into B, Ruby internally scans up through all the ancestors of A at that point, tying all of the modules found into the inheritance hierarchy of B to make the methods available to any instances.

However, when we later try and include X into A this has no effect on B, or anything that includes B either, because their inheritance hierarchy’s are already determined. In other words, this inclusion is not dynamic. Oh, Ruby! Why hast thou forsaken us?

A bit of background

The reason I got stuck into this was due to an obscure testing problem that I came across with Rails. When testing views that used the simply_helpful plugin (now a “legacy plugin” since the features have been rolled into edge), I found that helper methods from that plugin were missing, despite working under script/server in any environment:

test_should_show_object(MonkeysControllerTest):
ActionView::TemplateError: undefined method 'div_for' for #<#<Class:0x8360ec8>:0x82b8cb4>
    On line #1 of app/views/monkeys/show.rhtml

    1: <% div_for @monkey do %>
    2:   <b>Name:</b>
    3:   <%=h @monkey.name %>
    4: <% end %>

If we take a look at simply_helpful’s init.rb, we can see that these modules are definitely being declared as helpers, so why aren’t the methods available in the view?

Digging around using the breakpoint mechanism, I discovered the dynamically created class which views are evaluated in. This class is available as self at this point, so let’s examine the modules which are included in it:

irb:001:0> self.class.ancestors
=> [#<Class:0x8346208>, #<Module:0x841e43c>, MonkeysHelper, 
     #<Module:0x126a5e0>, ApplicationHelper, #<Module:0x30ffe24>, 
     ActionView::Base, #<Module:0x1257cc4>, Engines::RailsExtensions::Templates::ActionView, 
     Authentication::Helper, ActionView::Helpers::UrlHelper, ActionView::Helpers::TextHelper, 
     ActionView::Helpers::TagHelper, ActionView::Helpers::ScriptaculousHelper,  
     ActionView::Helpers::PaginationHelper, ActionView::Helpers::NumberHelper,  
     ActionView::Helpers::JavaScriptHelper, ActionView::Helpers::PrototypeHelper,  
     ActionView::Helpers::JavaScriptMacrosHelper, ActionView::Helpers::FormTagHelper,  
     ActionView::Helpers::FormOptionsHelper, ActionView::Helpers::FormHelper,  
     ActionView::Helpers::DebugHelper, ActionView::Helpers::DateHelper,  
     ActionView::Helpers::CaptureHelper, ActionView::Helpers::CacheHelper,  
     ActionView::Helpers::BenchmarkHelper, ActionView::Helpers::AssetTagHelper,  
     ActionView::Helpers::ActiveRecordHelper, ActionView::Partials,  
     ActionView::Base::CompiledTemplates, ERB::Util, Object, Base64::Deprecated,  
     Base64, Kernel]

The key here is the 6th member of this rowdy band, the charming-yet-anonymously-named #<Module:0x30ffe24>. This module is actually created in ActionController::Base, as the master helper module:

irb:002:0>ActionController::Base.master_helper_module
=> #<Module:0x30ffe24>
irb:003:0> self.class.ancestors[5]
=> #<Module:0x30ffe24>

When you call helper within a controller (as is happening in simply_helpful’s init.rb), you’re actually mixing in methods and modules to this master helper module.

Furthermore, we can look at the ancestors of this module right here in the view breakpoint:

irb:004:0> self.class.ancestors[5].ancestors
=> [#<Module:0x30ffe24>, SimplyHelpful::RecordTagHelper, SimplyHelpful::RecordIdentificationHelper]

Huzzah! There are the simply_helpful modules that we’re seemingly missing. But why aren’t they listed in the ancestors of the self class available to the view? I think you see where we’re going here.

Somehow, and only when testing, and this might not even apply to Edge Rails, the master helper module is being included into the view’s anonymous class before the plugin gets loaded. Weird. Thankfully, there’s a quick-if-dirty fix - we just need to make SURE that these modules get included into the main view class by adding this to the bottom of test/test_helper.rb:

class ActionView::Base
  include SimplyHelpful::RecordIdentificationHelper,
          SimplyHelpful::RecordTagHelper
end

Phew.

(Incidentally, this might be a similar problem to this)

(Also: Thanks to David Black for helping me figure this out!)

interblah.net - A Tale of Three Modules

Blog posts I will never finish

As part of my clearing of the decks for the new year, I’m declaring bankruptcy on the never-to-be-finished drafts that I’ve had cluttering up my repository for all of 2012.

Cry out in loss, reader, as you will never learn about The Kintama Problem, where I was trying to simultaneously explore/explain to myself why I have written my own test framework (the eponymous Kintama), and also talk a bit about the structure of tests, ideas for how to (re)run tests and common decision-points every Ruby DSL designer shares.1

Gnash your teeth in despair as you realise that you cannot struggle through my clumsy conflation of thoughts about Bret Victor’s excellent Inventing on Principle, and how it might possibly relate to why I reject the alpha-geek programmer stance that mastering the vi editor is good2.

Climb through blizzard storms up to the highest pinnacle only to throw yourself off, such that you might escape the terrible knowledge that my post-conference thoughts about Ru3y Manor3 are forever lost to you!

Woe! Woe be upon us all!

  1. Actually, I lied – you can read about Kintama and also Setup vs. Action for Ruby tests, Rerunning tests in Ruby and Capturing behaviour in Ruby DSLs

  2. I lied again – you can read about Inventing on Principle and vi if you like. 

  3. AKA my attempt to ham-fistedly force my philosophical agenda once more down the throats of the conference-going nerd. Alas, this is a blog post that I have actually abandoned, rather than decomposed like all the others.