lunes, 9 de junio de 2014

Expresiones Lambda en C# - Parte 1: Introducción a las Expresiones Lambda

Tabla de Contenido

0. Introducción
1. Qué es una Expresión Lambda
1.1 Concepto abstracto
1.2 Concepto en C#
1.2.1 Tratamiento de expresiones lambda por el compilador
1.2.2 Declaración
1.2.3 Uso
1.2.4 Historia
2. Operaciones
2.1 Tipos de parámetros explícitos
2.2 Captura de variables externas
2.2.1 Actualización de captures variables
2.2.2 Ciclo de vida de una variable capturada
3. Conclusiones
4. Glosario
5. Literatura & Enlaces

0. Introducción

Inicio una nueve serie de artículos orientados al descubrimiento de conceptos teóricos y prácticos de otra de las construcciones funcionales con las que cuenta el lenguaje de programación C#: expresiones lambda. En la serie de delegados tuvimos la oportunidad de hablar de los métodos anónimos (no poseen nombre formal), conocimos su uso para escenarios donde requerimos la asignación de una firma formal (tipos concretos de los parámetros) para un delegado; las expresiones lambda, como veremos con más detalle, son una apuesta con un nivel de abstracción aún más alto y poderoso para la escritura de métodos anónimos con omisión de varios de los formalismos estándar requeridos para la creación un método compatible con un delegado.

1. ¿Qué es una Expresión Lambda?

Conozcamos las dos categorías de definiciones de una expresión lambda.

1.1 Concepto abstracto


El concepto de expresión lambda proviene del trabajo formulado por el matemático-lógico norteamericano Alonzo Church [4] en su cálculo lambda (λ-calculus) de 1936 que consiste en "...un sistema formal diseñado para investigar la definición de función, la noción de aplicación de funciones y la recursión...[5, 6].
λ-calculus
Figura 1. λ-calculus.

Por otra parte, existen varios conceptos sinónimos de las expresiones lambda, entre ellos:
  • Función anónima
  • Función literal
  • Abstracción lambda.
sin embargo, siempre nos referiremos a este tipo de función como expresión lambda, por supuesto, en el contexto de la ciencia de la computación.

Continuando, una expresión lambda es una función anónima que no posee un identificador (o nombre) específico. Si nos dirigimos a las operaciones aritméticas básicas, podríamos imaginarnos algo como esto:

Función convencional:
SumaCuadrados(\mathd{x},\mathd{y})= \mathd{x}\times\mathd{x}+\mathd{y}\times\mathd{y}
Función lambda:
(\mathd{x},\mathd{y})\mapsto\mathd{x}\times\mathd{x}+\mathd{y}\times\mathd{y}

A diferencia de la función convencional, la función lambda carece de un identificador, sólo posee el conjunto argumentos o parámetros, y el símbolo -> (que se lee como: el conjunto de parámetros mapeados a la expresión...).

La función lambda también pudo escribirse así:
\mathd{x}\mapsto(\mathd{y}\mapsto=\mathd{x}\times\mathd{x}+\mathd{y}\times\mathd{y})

En este caso,los valores se mapean empezando por el argumento o parámetro de mayor profundidad en la agrupación lógica de paréntesis.


Veamos un ejemplo sustituyendo los parámetros con valores concretos:



Para la primera expresión:


nótese el lugar en donde se posiciona el conjunto de argumentos para los parámetros de la función anónima: al final de la misma. En la siguiente línea se sustituyen (binding) los identificadores de los parámetros por los valores de los argumentos: 5 y 2, respectivamente.


Por otro lado, si lo hacemos para la función lambda equivalente:


es evidente que se requiere un paso adicional para obtener el resultado, debido a las sustituciones según el nivel de profundidad de parentización.


No nos detengamos más en el concepto abstracto (matemático), y pasemos al concepto en C#.

1.2 Concepto en C#

Una expresión lambda en C# es un método anónimo (sin nombre) que se ha de declarar/definir (y asignar inmediatamente) sobre una instancia de un delgado. Las expresiones lambda también se usan crear árboles de expresiones [8] (más adelante entraremos en detalle en este tema).

Las expresiones lambda pueden ser usadas en estos escenarios:
  • Argumentos de otras funciones, 
  • Tipo de retorno de una función, 
  • Expresiones LINQ (la tercera parte de esta serie está dedicada a este tópico), 
  • Asignación a instancias de delegados genéricos, 

1.2.1 Tratamiento de expresiones lambda por el compilador

Internamente el compilador lleva a cabo la siguiente conversión de una expresión lambda dependiendo el contexto en donde se haya definido:
  • Una instancia de un delegado.
  • Un árbol de expresiones (útiles para crear expresiones lambda que pueden ser interpretadas en tiempo de ejecución).

1.2.2 Declaración

La representación general de una expresión lambda sigue este formato:

(parámetros) => expresión-o-bloque-de-sentencias

Descripción puntual:
  • (parámetros): Lista de parámetros. Características:
    • Se debe dejar los paréntesis vacíos () cuando no se especifique ningún parámetro.
    • El tipo de dato es opcional.
    • Los parámetros se pueden pasar por referencia con el uso de las palabras claves out, y ref (Variables y Parámetros en C# - Parte 2).
  • expresión-o-bloque-de-sentencias: una expresión, o bloque de sentencias:
    • Expresión:

      - Compuesta por los parámetros, expresión lógica, invocación a un método, referencia a una variable
    • Bloque de sentencias:

      { sentencia1; sentencia2; sentenciaN }
  • Operadora =>: este es el operador que se encarga de mapear los parámetros a la expresión al conjunto de sentencias de bloque.

1.2.3 Uso

Veamos el primer ejemplo de declaración y uso de una expresión lambda en C#:

Contamos con el siguiente delegado:

delegate int OperacionDelegado(int x);

Ahora podemos asignar una expresión lambda a una instancia del delegado previo:

OperacionDelegado raizCuadrada = x => x * x;

Nótese lo que viene por delante del operador de asignación:
  • Parámetro: x
  • Expresión lambda: x * x
En cuanto al parámetro x, no hemos tenido necesidad especificar su tipo, debido a que el compilador a través de toda su maquinaria se encarga de inferir el tipo de los parámetros y el tipo de retorno dependiendo del contexto, es decir, a la instancia del delegado que estamos asignado la expresión lambda. Así:
  • Parámetro del delegado: int
  • Tipo retorno del delegado: int
Desde [1], es importante agregar el siguiente comentario:
Conversión expresiones lambda a métodos concretos
Figura 2. Conversión expresiones lambda a métodos concretos.
Versión texto:
Internally, the compiler resolves lambda expressions of this type by writing a private method, and moving the expression's code into that method.
Ahora veamos un ejemplo en donde especificamos los tipos de los parámetros (esto puede ser útil [11] en situaciones donde el compilador se le puede dificultar o imposibilitar la inferencia de los tipos):

(int x, string s) => s.Length > x;

1.2.4 Historia

Las expresiones lambda fueron introducidas en la versión 3.0 de Microsoft .NET Framework. Antes de ser parte de los lenguajes compatibles con la CLR, se usaban diferentes técnicas para analogar el funcionamiento de este tipo de expresiones. Por ejemplo:
  • En C# 1.0 uso de delegados (Delegados en C#)
  • En C# 2.0 introducción de métodos anónimos
Para demostrar cómo ha ocurrido esta evolución, veamos el siguiente ejemplo:

Archivo de código fuente EvolucionExpresionesLambda.cs [enlace alternativo]:
Observemos las diferencias entre las dos versiones de C# 1.0 y 2.0, y la versión de C# 3.0, es evidente que podemos obviar la escritura de código extra para un delegado y un método. Además de la reutilización de los delegados integrados en la BCL (Base Class Library) como el de la línea 39.

> Prueba de ejecución.

Resultado:
 === C# 1.0: Uso de delegados === 
 25
 === C# 2.0: Delegado con código de inicialización (métodos anónimos) === 
 49

 === C# 3.0: Delegado con expresión lambda ===
 121

 === C# 3.0: Delegado genérico integrado y expresión lambda ===
 169

2. Operaciones

2.1 Tipos de Parámetros Explícitos

Los tipos de parámetros de una expresión lambda son inferidos por el propio compilador de C#. Sin embargo, es posible especificar los tipos para cada uno de los parámetros. Esto puede ayudar al compilador a la creación de la expresión lambda, y en casos extremos, remover la imposibilidad de la operación de inferencia.

Veamos un ejemplo de uso:

Archivo de código fuente TiposParametrosExplicitos.cs [enlace alternativo]:
> Prueba de ejecución.

2.2 Captura de variables externas

En el cuerpo de expresión de una expresión lambda podemos incluir referencias a variables locales y los parámetros de un método. En el contexto de una expresión lambda, a estas referencias se les conoce como variables externas.

Para poner como caso:

public static void Main()
{
int factor = 3;
// Delgado genérico integrado:
Func<int, int> producto = numero => numero * factor;

Console.WriteLine (producto (5)); // 15
}

A cualquier variable a la que se referencie en el cuerpo de una expresión lambda, se le conoce como captured variable (e.g., factor), en consecuencia, a las expresiones lambda que están integradas por captured variables se les llama closure.

A lo anterior hay que agregar que las captures variables se evalúan una vez que la expresión lambda entra en acción. Para demostrarlo, leamos este fragmento de código:

class Demo
{
public static void Main()
{
int factor = 2;

Func<int,int> multiplier = (n) => n * factor;

factor = 10;

Console.WriteLine (multiplier(3));
}
}

> Pueba de ejecución.

2.2.1 Actualización de captured variables

Las variables capturadas en una expresión lambda se actualizan de forma automática cada vez se invoca indirectamente (a la expresión lambda) a través del delegado. Veamos este efecto en el siguiente fragmento de código:

int variableLocal = 0;

// Captured variable:
Func<int> delegado = () => variableLocal++;

Console.WriteLine (delegado()); // 0
Console.WriteLine (delegado()); // 1
Console.WriteLine (variableLocal); // 2

> Prueba de ejecución.

2.2.2 Ciclo de vida de una variable capturada

La referencia y el valor a una variable capturada está disponible mientras el ámbito del delegado esté al alcance de ejecución. 

Ejemplo de uso:

public static Func<int> GeneraDelegado()
{
int variableLocal = 0;
return () => variableLocal++; // Retorna un closure
}

public static void Main()
{
Func<int> natural = GeneraDelegado();

Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 1
}

> Prueba de ejecución.

Nota desde [1]:
Captura interna de variables
Figura 3. Captura interna de variables.

Versión texto:
Capturing is internally implemented by "hoisting" the captures variables into fields of private class. When the moethods is called, the class is instantiated and lifetime-bound to the delegate instance. 

Conclusiones

Ya contamos con los esenciales (conceptos, práctica, etc.) para el uso de expresiones lambda. En la primera parte vimos los conceptos teóricos (matemático) y en C# de lo que es una expresión lambda (función literal, función anónima, abstracción lambda). También vimos cómo declarar una expresión lambda y uso en código fuente C#. Se realizó un breve recorrido histórico partiendo de C# 1.0 hasta llegar a la versión 3.0 de C#: delegados (C# 1.0), métodos anónimos (C# 2.0), y la aparición de las expresiones lambda en C# 3.0. Al final exploramos algunas operaciones interesantes sobre las expresiones lambda. En la siguiente parte trabajaremos más a fondo con los delegados genéricos integrados (Func y Action) en .NET Framework con expresiones lambda.

Glosario

  • Captured variable
  • Closure
  • Expresión
  • Expresión lambda
  • Lambda
  • Método
  • Método anónimo
  • Variable externa

Literatura & Enlaces

[1]: C# 5.0 in a Nutshell by Joseph Albahari and Ben Albahari. Copyright 2012 Joseph Albahari and Ben Albahari, 978-1-449-32010-2.
[2]: Exploring Lambda Expression in C# - CodeProject - http://www.codeproject.com/Articles/24255/Exploring-Lambda-Expression-in-C?msg=4838348#xx4838348xx
[3]: Anonymous function - Wikipedia, the free encyclopedia - http://en.wikipedia.org/wiki/Lambda_(programming)
[4]: Alonzo Church - Wikipedia, the free encyclopedia - http://en.wikipedia.org/wiki/Alonzo_Church
[5]: Lambda calculus - Wikipedia, the free encyclopedia - http://en.wikipedia.org/wiki/Lambda_calculus
[6]: File:Greek lc lamda thin.svg - Wikipedia, the free encyclopedia - http://en.wikipedia.org/wiki/File:Greek_lc_lamda_thin.svg
[7]: Cálculo lambda - Wikipedia, la enciclopedia libre - http://es.wikipedia.org/wiki/C%C3%A1lculo_lambda
[8] Binary expression tree - Wikipedia, the free encyclopedia - http://en.wikipedia.org/wiki/Binary_expression_tree
[9]: LINQ (Language-Integrated Query) - http://msdn.microsoft.com/en-us/library/bb397926.aspx
[10]: Variables y Parámetros en C# - Parte 2 | Experiencias Construcción Software - http://ortizol.blogspot.com/2013/09/variables-y-parametros-en-c-parte-2.html
[11]: Lambda Expressions (C# Programming Guide) - http://msdn.microsoft.com/en-us/library/bb397687.aspx
[12]: Delegados en C# - Parte 1: Introducción | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2014/05/Delegados-en-csharp-parte-1-introduccion.html


J

2 comentarios:

Envíe sus comentarios, dudas, sugerencias, críticas. Gracias.