Making Methods Immutable in Ruby (or, Death to Monkey Patching)

Update (09-21-2008): I have a followup to this article here.

You’ve probably been told that in Ruby classes are always open and any code can come along and redefine your methods in any way it pleases. Indeed, this is true.

Kinda

What’s not true, despite every reference I’ve read saying the contrary, is that you can’t prevent this from happening.

Ruby provides a method_added() callback that is invoked every time a method is added or redefined within a class. It’s part of the Module class, and every Class is a Module. There are also two related callbacks called method_removed() and method_undefined().

This means you could detect when other code has redefined a method, and do something about it! How about redefining that method (again) to point back to your original code? Indeed, this works.

I’ve encapsulated the details of this in a new module I call Immutable. It provides one class method called immutable_method(). Provide it a list of methods you don’t want touched and it’ll make sure they can’t be redefined. Hence, immutable.

It’s on GitHub: http://github.com/up_the_irons/immutable/tree/master

Here’s an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
require 'rubygems'
require 'immutable'

module Foo
  include Immutable

  def foo
    :foo
  end

  immutable_method :foo
end

# Now re-open Foo and redefine foo()
module Foo
  def foo
    :baz
  end
end

include Foo

foo # => :foo

foo() returns :foo, not :baz, even though we redefined foo() to return :baz.

If this makes you think of Java’s final method modifier, you’d be almost right in saying immutable_method is similar. But there’s one important difference: with immutable_method you can still redefine methods in subclasses. This makes sense, as one can argue that if you want to reimplement or extend a method, a child class is the only place where you should be doing that anyway. I tend to agree, call me a purist.

Why would this be useful to you? I don’t know. But I can tell you what motivated me.

My motivation for writing something that provides what some may call “evil” functionality (heh, and monkey patching isn’t evil?) of closed methods/classes came from my trials of trying to improve Rails loading time by reimplementing some ActiveSupport methods in C (Ruby extension).

I absolutely needed my C versions of certain methods to be present, and not clobbered by ActiveSupport.

So why didn’t I just load my extension after ActiveSupport? Because by then it is too late. Rails is already loaded, along with slow running methods that get called over 10,000 times during initialization.

Run the profiler and see for yourself:

1
./script/performance/profiler "require 'config/environment'"

This is a prelude to my next post, when I present the plugin I’ve built that takes away over 30% of overhead from Rails loading time. Stay tuned (or subscribe :)

But for now, if you want to try Immutable, clone it from GitHub, or if you have RubyGems 1.2.0 or higher, do this:

1
2
3
4
# Requires RubyGems 1.2.0+ (or so I'm told...)

gem sources -a http://gems.github.com
sudo gem install up_the_irons-immutable

Then you can run the example above. You’re all set.

See the README for further details.

Enjoy (or flame me for ruining your monkey patching).

Comments