tags:

views:

212

answers:

3

More verbosely, I have a module Narf, which provides essential features to a range of classes. Specifically, I want to affect all classes that inherit Enumerable. So I include Narf in Enumerable.

Array is a class that includes Enumerable by default. Yet, it is not affected by the late inclusion of Narf in the module.

Interestingly, classes defined after the inclusion get Narf from Enumerable.

Example:

# This module provides essential features
module Narf
  def narf?
    puts "(from #{self.class}) ZORT!"
  end
end

# I want all Enumerables to be able to Narf
module Enumerable
  include Narf
end

# Fjord is an Enumerable defined *after* including Narf in Enumerable
class Fjord
  include Enumerable
end

p Enumerable.ancestors    # Notice that Narf *is* there
p Fjord.ancestors         # Notice that Narf *is* here too
p Array.ancestors         # But, grr, not here
# => [Enumerable, Narf]
# => [Fjord, Enumerable, Narf, Object, Kernel]
# => [Array, Enumerable, Object, Kernel]

Fjord.new.narf?   # And this will print fine
Array.new.narf?   # And this one will raise
# => (from Fjord) ZORT!
# => NoMethodError: undefined method `narf?' for []:Array
+2  A: 

class Array has already been mixed-in with the Enumerable module which doesn't include your Narf Module yet. Thats the reason it throws a( basically its methods )n error.

if you include Enumerable in Array again, ie.

class Array
  include Enumerable
end

A mix-in makes a reference from the class to the included module, which in that particular objectspace has all methods to be included. If you modify any of the existing methods of a module, all the classes that include the module will reflect the changes.

But if you add a new modules to the already existing module, you have to re-include the module so that the reference can be updated.

Rishav Rastogi
The fact that ruby is interpreted has little to do with the observed behavior. It's just a matter of how method lookup and the ancestor chain are implemented. Which is seems to be an acceptable compromise for performance.
kch
Well yes, I just wrote my train of thought there, Its actually got to do with the way mix-ins/"include" is implemented in Ruby. Updating my answer with that.
Rishav Rastogi
And looks like you introduced a new misinformation in the last sentence: if you add new methods to a module, the method lookup will work fine on the mixing classes. What you can't do is add new modules to a module and expect its methods to show up on the mixing classes.
kch
cf. sepp2k comment on jonathan's answer.
kch
+3  A: 

There are two fixes to your problem that come to mind. None of them are really pretty:

a) Go through all classes that include Enumerable and make them also include Narf. Something like this:

ObjectSpace.each(Module) do |m|
  m.send(:include, Narf) if m < Enumerable
end

This is quite hackish though.

b) Add the functionality to Enumerable directly instead of its own module. This might actually be ok and it will work. This is the approach I would recommend, though it's also not perfect.

sepp2k
Yeah, I went with option b. Posted my answer.
kch
A: 

In writing my question, inevitably, I came across an answer. Here's what I came up with. Let me know if I missed an obvious, much simpler solution.

The problem seems to be that a module inclusion flattens the ancestors of the included module, and includes that. Thus, method lookup is not fully dynamic, the ancestor chain of included modules is never inspected.

In practice, Array knows Enumerable is an ancestor, but it doesn't care about what's currently included in Enumerable.

The good thing is that you can include modules again, and it'll recompute the module ancestor chain, and include the entire thing. So, after defining and including Narf, you can reopen Array and include Enumerable again, and it'll get Narf too.

class Array
  include Enumerable
end
p Array.ancestors
# => [Array, Enumerable, Narf, Object, Kernel]

Now let's generalize that:

# Narf here again just to make this example self-contained
module Narf
  def narf?
    puts "(from #{self.class}) ZORT!"
  end
end

# THIS IS THE IMPORTANT BIT
# Imbue provices the magic we need
class Module
  def imbue m
    include m
    # now that self includes m, find classes that previously
    # included self and include it again, so as to cause them
    # to also include m
    ObjectSpace.each_object(Class) do |k|
      k.send :include, self if k.include? self
    end
  end
end

# imbue will force Narf down on every existing Enumerable
module Enumerable
  imbue Narf
end

# Behold!
p Array.ancestors
Array.new.narf?
# => [Array, Enumerable, Narf, Object, Kernel]
# => (from Array) ZORT!

Now on GitHub and Gemcutter for extra fun.

kch
I think @sepp2k suggestion of moneypatching Enumerable to directly define narf? is a bit less convoluted.
ennuikiller
A bit less generic, too.
kch
Does the ancestor list look the same for modules that include Enumerable before and after your imbue call is made?
Bob Aman
@bob I'm not sure I understand what you're asking.
kch
@bob I think I got it now. The list is the same plus Narf.
kch
Another way of thinking about this: including module B in module A results module B being inserted in Module A's ancestor chain. Including module B in class C results in module B being inserted in class C's ancestor chain. It is Module B that is inserted, and not Module B's ancestor chain (which includes Module A), since Module B already includes all of Module A's methods. Thus class C knows that Module B is in its ancestor chain, and gets all of Module A's methods, but knows nothing about Module A.
Asher
The implication (of my immediately previous comment) is that if you require the ancestor method chain, you should include each module in order in the class or module that requires them. They will be added to the class's ancestor chain in the order they are included. Call class.ancestors for specifics in any given context.
Asher