Sunday, August 20, 2006

Closure variable capture in Ruby and C#, compared

A couple of posts on Larry O'Brien's blog piqued my interest, because they're about one of my favourite things - anonymous delegates. Specifically: In the first post, Larry says:
The proposal has slightly cleaner syntax than what is in C# but I believe would reprise an issue that C# has, in that "captured outer variables" are shallow copies.
However, in the second, he contrasts the behaviour of two programs, one in C# and the other in Ruby. The implication appears to be that Ruby treats variable capture in closures differently from C# - and that was my first thought too. However, a brief experiment with Ruby showed that the different semantics has nothing to do with closures at all, and in fact it's all down to C#'s for loop.

So, here's the first program, edited slightly for length (and to remove Console.ReadKey, which doesn't play nicely with Cygwin rxvt):

using System; 
using System.Collections.Generic; 
 
delegate void VoidDelegate(); 
 
class Program
{ 
    public static void Main() 
    { 
        List<VoidDelegate> closures = new List<VoidDelegate>(); 
        //Create a bunch of closures
        for (int i = 0; i < 10; i++) 
        { 
            VoidDelegate myClosure = delegate
            { 
                //Capture outer variable
                Console.WriteLine(i); 
            }; 
            closures.Add(myClosure); 
        }
        
        foreach (VoidDelegate closure in closures) 
        { 
            closure(); 
        }
        
        Console.ReadLine();
    } 
}
If you're familiar with C# 2.0, you'll know that when run, this program will print out '10' for ten lines. This is because there's only one instance of the variable 'i', and that instance is captured by each delegate. Because the for loop changes the value of i, at the end of the loop every delegate will have a capture of the same variable, whose value is 10. That's why each closure prints '10'.

Larry contrasts the above program with this, in Ruby:

closures = Array.new() 

#Create a bunch of closures
10.times { | i | 
  myClosure = lambda { 

    #Capture outer variable
    puts(i)
  }
  closures.push(myClosure)
} 

closures.each { | myClosure | 
  myClosure.call()
}
This, of course, prints each number from 0 to 9.

Now, one might draw the conclusion from this that Ruby's closures are different from C#'s closures. That wouldn't be the right conclusion though - Ruby's variable capture is actually no different from C#. The difference is all down to the scope of the loop variable. I'll demonstrate it by writing cross-idiomatic translations of the two programs - i.e. writing the Ruby program in C#, and the C# program in Ruby, trying hard to preserve semantics.

First, I'll write the Ruby program in C#. The difference is down to the chosen iteration primitive: in Ruby, it's a method of numbers which accepts a code block as an argument (at least, that's the primive Larry's chosen), while in C#, it's the 'for' statement. So, to get the Ruby behaviour in C#, I'll have to write a Times method:

using System;
using System.Collections.Generic;

public static class App
{
    delegate void Method();
    
    public static void Times(int iterations, Action<int> action)
    {
        for (int i = 0; i < iterations; ++i)
            action(i);
    }
    
    static void Main()
    {
        List<Method> methods = new List<Method>();
        
        Times(10, delegate(int i)
        {
            methods.Add(delegate
            {
                Console.WriteLine(i);
            });
        });
        
        foreach (Method m in methods)
            m();
    }
}
I can even use this as a gratuitous chance to use C# 3.0 LINQ Preview, with its extension methods and more concise lambda expressions:
using System;
using System.Collections.Generic;

static class App
{
    delegate void Method();
    
    static void Times(this int iterations, Action<int> action)
    {
        for (int i = 0; i < iterations; ++i)
            action(i);
    }
    
    static void Main()
    {
        List<Method> methods = new List<Method>();
        
        10.Times(i => 
        {
            methods.Add(() => Console.WriteLine(i));
        });
        
        methods.ForEach(m => m());
    }
}
Don't worry, I don't think it's much more readable either :) Anyway, both of these two C# programs, following an emulation of a Ruby loop primitive, produce the numbers 0 to 9. That's because they capture a new instance of the variable each time, in an argument.

Now for the Ruby version of the original C#. Here, the Ruby for loop over a range is sufficient to demonstrate behaviour similar to C# (forgive my horrible Ruby, I'm only a beginner :):

closures = Array.new() 

for i in 0..9
    closures.push lambda { puts(i) }
end

closures.each { | myClosure | 
  myClosure.call()
} 
This, similarly to the C# version, prints out '9' ten times, because it's captured the single instance of 'i'. I'll get even closer to the C# implementation. The C# for-loop, as in other C-based languages, is syntax sugar for something like this:

for (<initialStatement>; <condition>; <loopExpression>)
    <body>

// Translates to...

{
    <initialStatement>;
    while (<condition>)
    {
        {
            <body>
        }
        <loopExpression>;
    }
}

// ... with extra braces to reflect the C#/Java/C++ style scoping.

So, onwards to a Ruby transliteration of this (but not translating the scoping, which in Ruby appears to be limited to code blocks (e.g. lambdas) and methods, not compound statements):
closures = Array.new() 

i = 0
while i < 10
    closures.push lambda { puts(i) }
    i = i + 1
end

closures.each { | myClosure | 
    myClosure.call()
} 
This prints '10' ten times.

So, there it is. Ruby's closures don't do any deeper copying of variables than C#, and with some of the looping primitives, they're even susceptible to the same downsides. An advantage Ruby has is the way its closures mesh well as arguments to methods, resulting in fairly clean language extensions. That means that one need never directly use Ruby's 'for', 'while' or 'loop' primitives to implement loops - but that's a matter of style.

It's interesting that Java's proposed closure syntax has this composability feature in common with Ruby.

No comments: