December 29, 2006

Metaprogramming method closures in Ruby

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.

In Technology and Software

Posted by Josh Staiger at 06:34 PM