Friday, July 25, 2008

Ruby: Dwemthy's Array using Modules

Following my blog entry Underuse of Modules, an anonymous commenter asks
Can you show another example, then, of how one might implement the "magic" of Dwemthy's Array (http://poignantguide.net/dwemthy/) just using modules? I can never remember how to do this sort of thing, and if modules can make it conceptually simpler it would be most useful.
I'm not sure exactly what magic they were referring to, but I'll assume they mean what allows creature habits to be defined in class definitions. Based on that assumption, I pulled out this code from the example.

class Creature
def self.metaclass; class << self; self; end; end

def self.traits( *arr )
return @traits if arr.empty?

attr_accessor(*arr)

arr.each do |a|
metaclass.instance_eval do
define_method( a ) do |val|
@traits ||= {}
@traits[a] = val
end
end
end

class_eval do
define_method( :initialize ) do
self.class.traits.each do |k,v|
instance_variable_set("@#{k}", v)
end
end
end
end
end

class Rabbit < Creature
traits :bombs
bombs 3
end

Rabbit.new.bombs # => 3

Note, I removed the comments and added the Rabbit code. The Rabbit is there to ensure the magic continues to function as expected.

The version using a module isn't significantly better, but I do slightly prefer it.

class Creature
def self.traits( *arr )
return @traits if arr.empty?

attr_accessor(*arr)

mod = Module.new do
arr.each do |a|
define_method( a ) do |val|
@traits ||= {}
@traits[a] = val
end
end
end

extend mod

define_method( :initialize ) do
self.class.traits.each do |k,v|
instance_variable_set("@#{k}", v)
end
end
end
end

class Rabbit < Creature
traits :bombs
bombs 3
end

Rabbit.new.bombs # => 3

The above version is a bit clearer to me because it defines methods on a module and then extends the module. I know that if I extend a module from the context of a class definition the methods of that module will become class methods.

Conversely, the first example forces me to think about where a method goes if I do an instance_eval on a metaclass. By definition all class methods are instance methods of the metaclass, but there are times when you can be surprised. For example, using def changes based on whether you use instance_eval or class_eval, but define_method behaves the same (change metaclass.instance_eval in the original example to metaclass.class_eval and the behavior doesn't change). This type of thing is an easy concept that becomes a hard to find bug.

If you spend enough time with metaclasses it's all clear and easy enough to follow. However, modules are generally straightforward and get you the same behavior without the mental overhead. I'm sure someone will argue that metaclasses are easier to understand, which is fine. Use what works best for you.

However, there are other reasons why it might make sense to use modules instead, such as wanting to have an ancestor (and thus the ability to redefine and use super).

Again, it probably comes down to personal preference.

7 comments:

  1. Anonymous1:07 PM

    Thank you. That was quicker than Monday! I think the module version has helped me understand the metaclass version.

    What I can't seem to do is to factor out the module into an external module called Traits, which I can use like:

    class Character
    include Traits
    traits :forgetfulness, :arrogance
    end

    Maybe it is because I'm getting confused about what {self, self.class} are inside and outside the method definitions of a module, especially when dynamically created.

    Or, is this because a method in a module has no way to test if its module has been included in a class or not [AFAIK], so the only way to get this to work is to define it in the scope of a class? Then you can get at the variables (class or instance) to modify them specifically for that class, whereas a module defined outside a class can only get at that modules variables?

    Do you want to see the ghastly horridness I have created out of your example in my attempt, given that it doesn't work?

    ReplyDelete
  2. In response to the anonymous comment prior, I've created a pastie which shows an includable Traits module. Hope that helps.

    ReplyDelete
  3. Hi mr anonymous,

    I'm guessing that the confusion is due to the use of arr in the Module.new definition, which abuses the fact that arr is in scope.

    Just a guess, but a great blog post anyway. Bathing in the coding style of merb, you'd love the simplistic style than the "magical" one.

    ReplyDelete
  4. Thanks for the quick turn-around in answering that commenter's question. It was helpful to see an example of refactoring something I had seen before, using your recommendations.

    ReplyDelete
  5. Anonymous12:36 PM

    Thank you for these responses. Including a module in a module was a nice trick, nkryptic, I'll have to play with that code some more though to really absorb it. I'm glad my question was useful to others, I was wondering if I was just being thick. Thank you for your patience.

    ReplyDelete
  6. Interesting; now the traits are isolated in a module, you can share them between classes. For this implementation, pull #initialize from #traits and put it into the top level of Creature (where it belongs?). Move the attr_accessor(*arr) into the loop over traits in #initialize (hmmm, bit of a hack using self.class.send). Now export the module (e.g. make mod an instance variable @mod and add a class level accessor).

    Now you can declare class Hare < Creature; extend Rabbit.mod; bombs 6; end

    Or more interesting: class Chimera < Creature; extend Lion.mod, Goat.mod, Serpent.mod; end

    best wishes,
    Jerry

    ReplyDelete
  7. A pastie may make that clearer: http://rafb.net/p/XlSByD47.html

    Jerry

    ReplyDelete

Note: Only a member of this blog may post a comment.