Thursday, July 24, 2008

Ruby: Underuse of Modules

When I began seriously using Ruby I noticed two things that I didn't like about the language.It's a few years later and I've noticed a few interesting things.
  • I can't remember the last time I actually wanted a method to get the metaclass.
  • If you use a module to add behavior your behavior becomes part of the ancestor tree, which is significantly more helpful than putting your behavior directly on the class.
No metaclass method necessary
In April 2005, Why gave us Seeing Metaclasses Clearly. I'm not sure the article actually helped me see metaclasses clearly, but I know I pasted that first block of code into several of my first Ruby projects. I used metaclasses in every way possible, several of which were probably inappropriate, and saw exactly what was possible, desirable, and painful.

I thought I had a good understanding of the proper uses for metaclasses, and then Ali Aghareza brought me a fresh point of view: defining methods on a metaclass is just mean.

We were on the phone talking about who knows what, and he brought up a blog entry I'd written where I dynamically defined delegation methods on the metaclass based on a constructor argument. He pointed out that doing so limited your ability to change the method behavior in the future. I created some simple examples and found out he was right, which lead to my blog entry on why you should Extend modules instead of defining methods on a metaclass.

Ever since that conversation and the subsequent blog entry, I've been using modules instead of accessing the metaclass directly. "Just in case someone wants to redefine behavior" isn't really a good enough reason for me if the level of effort increases, but in this case I found the code to be easier to follow when I used modules. In programming, there are few win-win situations, but Ali definitely showed me one on this occasion.

If you interact with a metaclass directly, do a quick spike where you introduce a module instead. I think you'll be happy with the resulting code.

Include modules instead of reopening classes
In January of 2007 I wrote a blog entry titled Class Reopening Hints. I didn't write it because I thought it was very valuable, I wrote it so developers afraid of open classes could get some sleep at night. Those guys think we are going to bring the end of the world with our crazy open classes, and I wanted to let them know we'd at least thought about the situation.

I'm really not kidding, I thought the entry was a puff piece, but it made some people feel better about Ruby so I put it together. I never followed the Use modules instead of adding behavior directly advice though, and I don't think many other Rubyists did either. It was extra effort, and I didn't see the benefit. In over two and an half years working with Ruby I've never once had a problem finding where behavior was defined. With that kind of experience, I couldn't justify the extra effort of defining a module -- until the day I wanted to change the behavior of Object#expects (defined by Mocha). I was able to work around the fact that Mocha defines the expects method directly on Object, but the solution was anything but pretty.

It turns out, using modules instead of adding behavior directly to a reopened class has one large benefit: I can easily define new behavior on a class by including a new module. If you only need new behavior, then defining a new method on the class would be fine. But, if you want to preserve the original behavior, having it as an ancestor is much better.

Take the following example. This example assumes that a method hello has been defined on object. Your task is to change the hello method to include the original behavior and add a name.

# original hello definition
class Object
def hello
"hello"
end
end

# your version with additional behavior
class Object
alias old_hello hello
def hello(name)
"#{old_hello} #{name}"
end
end

hello("Ali") # => "hello Ali"

That code isn't terrible. In fact, there are a few different ways to redefine methods and access the original behavior, but none of them look as nice as the following example.

# original hello definition
class Object
module Hello
def hello
"hello"
end
end
include Hello
end

# your version with additional behavior
class Object
module HelloName
def hello(name)
"#{super()} #{name}"
end
end
include HelloName
end

hello("Ali") # => "hello Ali"

When you have an ancestor, the behavior is only a super call away.

note:Yes, I've also reopened the class to include the module, but when I talk about reopening the class I'm talking about defining the behavior directly on the reopened class. I could have also included the module by using Object.send :include, HelloName. Do whichever you like, it's not pertinent to this discussion.

Prefer Modules to metaclasses and reopened classes
The previous example illustrates why you should prefer modules, it gives simple access to your method behavior to anyone who wishes to alter but reuse the original behavior. This fact applies to both classes that include modules and instances that extend modules.

So why didn't Matz give us first class access to the metaclass? Who cares. He probably knew extending modules was a better solution, but even if he didn't -- it is. He didn't give you a method to access the metaclass, and whether he knew it or not, you don't need it.

14 comments:

  1. That post summarizes my thoughts too! Great read.

    ReplyDelete
  2. Anonymous7:10 AM

    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'te attempted this, without success.

    ReplyDelete
  3. Anonymous9:05 AM

    No problem. Check back on July 29th for a post with examples.

    Cheers, Jay

    ReplyDelete
  4. Anonymous10:34 AM

    Is it the decorator pattern ?

    It made me remember this nice piece of code : http://pastie.org/12066

    Very helpfull articles, i'm waiting for the 29th.

    ReplyDelete
  5. Anonymous1:27 PM

    Jay,

    I just wanted to take the time to say thanks for writing informative, top-notch posts.

    Thanks. :-)

    Kevin

    ReplyDelete
  6. Funny that the metaclass method was just recently added to Rails. Now it will be harder not to use. :)

    http://github.com/rails/rails/commit/f4f6e57e8c2a446a4a600576f0caf0fb8921ba13

    ReplyDelete
  7. Anonymous10:23 PM

    @Jay:

    I've been following your blog longer than I can remember, and I think this was one of the most informative posts I've ever read here. Thanks a lot for this.

    ReplyDelete
  8. To add to the back-patting, this was a very informative post. I really appreciated the links to your other posts that gave some insight into your progression as a Ruby programmer. (I also appreciated the link to Why's article.)

    I don't know how I wasn't subscribed to your feed in the first place, but I certainly am now. Thanks again!

    ReplyDelete
  9. Anonymous6:33 AM

    Thank you all for the kind words.

    I've also published the follow up with Dwemthy's Array.


    I hope it's also helpful.

    Cheers, Jay

    ReplyDelete
  10. Anonymous7:22 AM

    Hi Jay --

    Thanks for the interesting post. I agree that modules are underutilized, especially as regards #extend. In particular, extending objects with modules is, I believe, the best way to deal with the issue of core modifications (i.e., the fact that they're almost always a bad idea). A lot of cases of perceived need for a new method turn out to be just for one or two objects anyway, and changing an entire class (especially a core class) just so that one object can have a method always seems very loose to me. And #extend, of course, solves the problem of global overrides and name clashes and so forth (at least by reducing them to one object).

    I'd like a #singleton_class method in Ruby, since I can't really say I never want access to the singleton class. My understanding is that Matz hasn't put it in for two reasons: uncertainty about what that class should be called (metaclass, singleton class, etc.); and the fact that, in theory at least, the whole per-object behavior system might some day be implemented in a way that doesn't even involve classes. I think reason #2 isn't really pertinent any more; I'm pretty sure the class interface to per-object behavior is here to stay. The name issue continues, I guess.

    ReplyDelete
  11. Anonymous8:36 AM

    Hi David,

    Thanks for the comment and the info.

    Cheers, Jay

    ReplyDelete
  12. I read this with some confusion, until I realized by metaclass you meant singleton class. While technically the same thing in Ruby, I generally think of a metaclass as the "class level" of a class. And those, can be quite useful!

    With that distinction made, I do agree with you. I have rarely ever found a good use for the object singleton class. But I use base classes ;)

    Even so, there should be a method for it. (class << self; self; end) is just silly.

    ReplyDelete
  13. Hey, this is a way old post... but no less useful!

    http://viewsourcecode.org/why/hacking/seeingMetaclassesClearly.html

    this is a link to why's metaclasses

    ReplyDelete
  14. Anonymous11:02 PM

    I'm not sure if there has been a change in the way ruby handles these types of includes, but I was only able to get this to work by using extend on instances of the class as opposed to include on the reopened class itself.

    ReplyDelete

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