Today I found out that ruby closures are not function objects. When a proc or lambda is created the variables in local scope are bound and not copied. This means that if an outside method updates a variable the closure will see it, and if the closure updates a variable the method will see it. For example:


closures = []
for n in 0..7
  closures << lambda { n }
end
closures.map { |c| c.call } => [7, 7, 7, 7, 7, 7, 7, 7]

WTF you say? I did. Well, the variable n was updated outside the { n } closure. All eight closures will print 7 in this example since that was the last value of n. What if I need to keep a unique value of n for each closure? There’s a trick, make a functor. This is a slang functor, not the one from category theory. I still have no idea what those are. Here’s the solution:


def make_functor(n)
  lambda { n }
end

closures = []
for n in 0..7
  closures << make_functor(n)
end
closures.map { |c| c.call } => [0, 1, 2, 3, 4, 5, 6, 7]

Because make_functor’s argument (n) goes out of scope when make_functor returns it (n) can no longer be updated. This effect isolates the variable in the closure from the rest of the program and the variable n is not garbage collected because it is still referenced by the closure.

Now lets apply this to a real world problem. With the ruby-gnome2 library I am writing a class that displays a collection of ActiveRecord objects as an editable grid. Because ruby-gnome2 is a close match to the C bindings there is quite a bit of record keeping to do. Here is a loop that will add an “edited” signal handler to each column in a Gtk::TreeView. This code is buggy because n is shared between all closures created when signal_connect internally calls to_proc on the provided block. An edit to any cell will result in that change getting applied to only the cell in the last column since the loop exits with n on the last column. Talk about confusing the user!


for n in 0...NUM_COLUMNS
  renderer.signal_connect "edited" do |renderer, path, data|
    iter = @tree_store.get_iter path
    iter[n] = data
  end
end

Functors to the rescue!!!


def connect_edit_functor(renderer, n)
  renderer.signal_connect "edited" do |renderer, path, data|
      iter = @tree_store.get_iter path
      iter[n] = data
  end
end

for n in 0...NUM_COLUMNS
  connect_edit_functor(renderer, n)
end

Here we pass the needed variables to a method that will bind the signal handler and then gracefully let those variables go out of scope before the loop updates n. The result is that each instance of the signal handling closure now holds it’s own copy of n at the correct value. The correct column will therefore be referenced when a user edits a cell.


Your Response