Thursday, February 19, 2009

(Ruby) Closures - jack of all trades, master of none.

Ruby is based around closures. Closures are both used to represent blocks of code, as well as anonymous functions:

As an anonymous function:

[1, 2, 3, 4].select { |i| i % 2 == 0 }  
# => returns [2, 4]

As a block of code:

10.times do
  puts "Hello World"
end

The problem is that they are not quite the same, and don't solve quite the same problem. In fact, I would go so far as to say that closures solve both problems... poorly.

As an anonymous function, closures grab a whole lot more context than you usually would like, and especially in ruby where you each closure by necessity always retain the complete context.

Its double nature as block and anonymous function means that exiting a closure occasionally works a bit different that expected, "return" alternately exits the current method or just returns the closure with that value. In some cases one can use "break" with an argument (a bit depending on what version of ruby you're using):

def test1
  [1, 2].collect(&Proc.new{ |i| return i })
end
p test1 # => 1

def test2
  [1, 2].collect(&lambda{ |i| return i })
end
p test2 # => 1 ([1, 2] in 1.9)

def test3
  [1, 2].collect { |i| return i } 
end
p test3 # => 1

def test4
  [1, 2].collect(&Proc.new{ |i| break i })
end
p test4 # Illegal

def test5
  [1, 2].collect(&lambda{ |i| break i })
end
p test5 # [1,2] (Illegal in 1.8)

def test6
  [1, 2].collect { |i| break i } 
end
p test6 # => 1

I am tempted to suggest that instead of lumping these two different usages into a single concept (the closure), we would be better served by separating these into anonymous functions and blocks.

A block would inherit context but an anonymous function would not (it would instead resolve variable values during creation).

Of course, the following could would be slightly different from the closure version:

a = 1
f = function() { return a; }
a = 2
p f.call() # => 1 (2 if this would have been a closure)

For our blocks this would allow us to unroll method calls, and in general optimize methods taking blocks as arguments by treating them as if the methods were macros.

Blocks and anonymous functions might not cover everything you can do with a closure, but how often do you really want a function that retains the whole context it was created in?