Metaprogramming method closures in Ruby
by Josh Staiger
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.