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

Comments

Looks good! Thanks for picking up my slack. :)

Posted by: Kurtiss Hare December 29, 2006 09:18 PM | Permanent link

Hello Josh

Posted by: Kitty January 1, 2007 08:05 AM | Permanent link

i'm sad :( i don't understand your blog posts anymore

Posted by: eileen January 1, 2007 02:49 PM | Permanent link

Hello Josh

Posted by: World January 1, 2007 02:51 PM | Permanent link

hahahah!!

i am glad someone else voiced my opinion for the recent posts already. ;)

erm, why is this "program" (language? code?) named Ruby?? Or is it?!

anyway, happy new year!

Posted by: Phoebe January 1, 2007 04:34 PM | Permanent link

You guys can look at the pictures ;)

Posted by: Josh Staiger January 2, 2007 08:41 PM | Permanent link

"You guys can look at the pictures ;)"

maybe you should put up more entertaining pictures for us to look at then, ha!

(that other picture with the starfish really freaks me out -- yet fascinates me at the same time...)

Posted by: Phoebe January 3, 2007 06:22 PM | Permanent link