domingo, 15 de mayo de 2011

Lambda Expressions

Desde la primer versión de C# existe la posibilidad de usar delegates, objetos que representan una invocación a un método; en esencia un puntero a función fuertemente tipado. Los delegates son utilizados fundamentalmente como callback en eventos, pero pueden ser pasados a funciones para ser ejecutados por las mismas. De esta forma, permite definir contratos en función a firmas de métodos en lugar de requerir el uso (un poco más fuerte) de interfaces.

La declaración de un delegate define la firma de los métodos que pueden ser aceptados por el mismo. Por ejemplo, un delegate que retorna un entero y recibe un string sería definido así:

delegate int TestDelegate(string param1);

y podría instanciarse de esta manera:

class Test
{
int ParseInt(string param1)
{
return int.Parse(param1);
}
static void Main()
{
Test test = new Test();
TestDelegate del = new TestDelegate(test.ParseInt);
Console.WriteLine(del("12") + 1);
}
}

De ejecutarse, este código mostraría el resultado 13.

En este caso, del mantiene una referencia a test y ParseInt se ejecuta en el contexto de dicha referencia.

Está era la única forma que teníamos disponible con C# 1. Ya en la segunda versión de la especificación, se agregó la alternativa de los anonymous delegates. Usando la nueva sintaxis, aumenta la cantidad de cosas que pueden hacerse con los delegates, ya que no es necesario referir a un método el cual será llamado al invocar el delegate, sino que simplemente se puede definir un bloque de código a ejecutarse.

Para el mismo delegate definido arriba, un ejemplo sería:

class Test2
{
static void Main()
{
TestDelegate del = delegate(string param1) {
return int.Parse(param1);
};
Console.WriteLine(del("12") + 1);
}
}

Una característica interesante de los delegates anónimos, es el hecho de que pueden hacer referencia a cualquier variable del contexto en el que son definidos:

class Test3
{
static void Main()
{
int integer = 0;
TestDelegate del = delegate(string param1) {
return int.Parse(param1) + integer;
};
Console.WriteLine(del("12") + 1);
integer++;
Console.WriteLine(del("12") + 1);
}
}

La salida de este programa es 13 y 14, ya que del hace referencia a la variable integer, la cual es modificada luego de la primer invocación. Lo que se vé aquí, es que el delegate está creando una closure, donde el scope actual define el conjunto de variables que son visibles por el delegate.

Esto le da gran poder a los delegates, ya que les permite ser usados en forma más general y sin escribir demasiado código. Un ejemplo, usando iteración:

int[] integers = { 1, 2, 3};
int sum = 0;
Array.ForEach(integers, delegate(int i) { sum += i; }
Console.WriteLine(sum);

Esto nos lleva a la tercer versión del lenguage C#, donde se definen las lambda expressions, que refinan el concepto de anonymous delegate y utilizan las nuevas capacidades de inferencia de tipos del lenguaje para que la sintaxis sea mucho más limpia. El término lambda expression viene del cálculo lambda, la teoría matemática detrás del desarrollo de los lenguajes funcionales (tema para otro día).

De esta manera, el ejemplo anterior se podría reescribir así:

int[] integers = { 1, 2, 3};
int sum = 0;
Array.ForEach(integers, i => sum += i);
Console.WriteLine(sum);

Básicamente, la nueva sintaxis permite indicar el conjunto de parámetros que recibirá el nuevo delegate, sin necesidad de indicar su tipo (hay algunas excepciones). En el caso de delegates simples que retornan un valor, se puede prescindir de usar el keyword return e indicar simplementa la expresión del resultado.

El tipo de delegate generado por una lambda expressión depende fundamentalmente de cómo se va a aplicar. Veamos un ejemplo:

class Test4
{
delegate void TestDelegate2(int num);
static void Main()
{
Func1(i => i + 1);
Func2(i => i + 1);
}
void Func1(Func f1)
{
Console.WriteLine(f1(1));
}
void Func2(TestDelegate2 f2)
{
Console.WriteLine(f2(1));
}
}

Si bien los tipos de f1 y de f2 son diferentes, viendo que el código definido en la expresión es compatible con el tipo de ambos delegates, el compilador permite que utilicemos el mismo código sin necesidad de hacer nada extraño.

Hay otro feature de las lambda expressions que las hace particularmente interesantes y novedosas con respecto a los delegates. Existe un subconjunto de lambada expressions que pueden ser transformadas automáticamente en árboles de expresión, que pueden ser manipulados por el programador.