Metaprogramming method closures in Ruby


December 29, 2006
night01.jpg

In my previous post, Of Closures, methods, procs, scope, and Ruby, I discussed how blocks and procs are closures in Ruby while methods are not.

To review, this won't work:

class MyClass
    @friend
    hello_string = "Hello"
    def initialize
        @friend = "Kitty"
    end
    def say_hello
        puts hello_string + " " + @friend  # Bzzt!  hello_string is not in scope
    end
end
my_object = MyClass.new
my_object.say_hello # Bzzt!

And neither will this (at the toplevel):

hello_string = "Hello, world!"
def say_hello
    puts hello_string  # Bzzt!  hello_string is not in scope
end
say_hello # Bzzt!

In response Kurt hypothesized we could get method closures in Ruby with a bit of metaprogramming.

And he's right.

As Bryce points out, Ruby's Module class has a private method nammed define_method that defines a new method given a symbol representing its name, and a block representing its body.

Because the second parameter is a block, and blocks can be closures, this gives us the key to defining method closures.

Within the context of a class definition:

class MyClass
    @friend
    hello_string = "Hello"
    def initialize
        @friend = "Kitty"
    end
    define_method(:say_hello) { puts hello_string + " " + @friend }
end
my_object = MyClass.new  
my_object.say_hello # Hello Kitty

Yay!

But suppose we want to define one-off methods from the top-level instead.

Because define_method is private, normally we wouldn't be able to call it outside a class definition, but we can get around this using the send hack.

Let's define a convenience method:

def lexdef(method_symbol, &block) 
    self.class.send :define_method, method_symbol, &block
end

And now we can define method closures from the toplevel:

hello_string = "Hello"
lexdef :say_hello do |friend|
    puts hello_string + " " + friend
end
say_hello "Kitty" # Hello Kitty

Voilà! An unobtrusive syntax for defining method closures!

I like it :)

As of Ruby 1.8, blocks don't yet support Ruby's first-class syntax for receiving block arguments. So this won't work:

hello_string = "Hello"
lexdef :say_hello do |friend, &formalize|   # Bzzt!  Syntax error
    puts hello_string + " " + yield(friend)
end
say_hello("Kitty") { |friend| "Ms. " + friend }

But word on the street is &block arguments to blocks will be supported in Ruby 1.9.