Firmas invitadas
 

Las expresiones lambda en C# 3.0

 

Publicado el 24/Oct/2006
Actualizado el 24/Oct/2006

Autor: Octavio Hernández
octavio@pokrsoft.com
PoKRsoft, S.L.
 
Lambda expressions in C# 3.0 (English version) English version


 

0. Prefacio

Es motivo de gran satisfacción para mí poder participar con este modesto artículo en el homenaje al décimo aniversario de la página Web de “El Guille”, un lugar en Internet que se ha ganado por derecho propio un sitio en el corazón de miles de desarrolladores de habla hispana que utilizan tecnologías Microsoft. ¡Felicidades, y a cumplir muchos años más!

 

1. Introducción

No desvelamos ningún secreto al decir que C#, que en su versión inicial se presentaba como una herramienta lingüística destinada en principio a subsumir los paradigmas de programación procedimental (imperativa), orientada a objetos (en línea con los estándares impuestos en su momento por C++ y Java) y basada en componentes (con dos referentes claros, Delphi y Visual Basic), ha ido evolucionando a partir de ese momento por un camino que intenta tener en cuenta e incorporar al lenguaje las mejores ideas surgidas al calor de otros puntos de vista de la programación como los lenguajes dinámicos (Perl, Python, Ruby), los lenguajes de acceso a almacenes de datos (SQL, XML) o la programación funcional (LISP, ML).

Las expresiones lambda, una de las novedades más importantes de la próxima versión 3.0 de C# [3] sobre las que se apoyará todo el andamiaje de LINQ [4], son precisamente un recurso proveniente del mundo de la programación funcional. En LISP y sus dialectos, como Scheme, la construcción lambda es un mecanismo para definir funciones. Por ejemplo, la expresión:

        (define cuadrado (lambda (x) (* x x)))

 

define una variable cuadrado cuyo valor es una función que calcula el cuadrado de su argumento. Una de las características más importantes de estos lenguajes, a la que otorgan una enorme importancia los arquitectos de .NET Framework [2], es que las funciones lambda pueden ser tratadas no solo como piezas de código que pueden ser invocadas, sino también como valores “corrientes y molientes” que pueden ser manipulados por otros bloques de código. Utilizando un término que escuché por primera vez hace más de 20 años a mi entonces tutor, el Dr. Luciano García, las funciones lambda facilitan la metaprogramación – la escritura de programas capaces de manipular otros programas.

Este artículo describe la implementación de las expresiones lambda en C# 3.0, centrándose principalmente en su explotación como bloques de código; el tema de la representación como datos y posterior tratamiento programático de las expresiones lambda solamente se introduce en aras de la completitud, quedando en buena medida pendiente para una próxima entrega.

2. Las expresiones lambda de C# como entidades de código

La manera natural de implementar una definición como la de la función lambda cuadrado anterior en C# sería a través de un tipo y una instancia de delegados [5]:

        delegate T Mapeado<T>(T x);

        Mapeado<int> cuadrado = delegate(int x) { return x * x; };

 

La primera línea del código anterior define un tipo delegado genérico llamado Mapeado. A partir de este modelo podremos crear instancias de delegados para funciones que a partir de un valor de un tipo T producen otro valor del mismo tipo T (o sea, que mapean –recuerde el término, que comenzará a utilizar con frecuencia en un futuro cercano- un valor del tipo T a otro valor del mismo tipo).

En la segunda línea, por otra parte, se define una instancia del tipo delegado anterior que “apunta” a una función que devuelve el cuadrado de un número entero. En esta sintaxis se hace uso de otra característica incorporada a C# en la versión 2.0, los métodos anónimos, que permiten la definición en línea del bloque de código que especifica la funcionalidad a la que se desea asociar a la instancia del delegado. En la versión original de C# habría sido necesario definir explícitamente la función (y el delegado no habría podido ser genérico):

        static int Cuadrado(int x)

        {

            return x * x;

        }

        Mapeado<int> cuadrado2 = Cuadrado;

 

Aunque los métodos anónimos ofrecen una notación más compacta y directa, su sintaxis es aún bastante verbosa y de naturaleza imperativa. En particular, al definir un método anónimo es necesario:

·         Utilizar explícitamente la palabra reservada delegate.

·         Indicar explícitamente los tipos de los parámetros, sin ninguna posibilidad de que el compilador los deduzca a partir del contexto de utilización.

·         En el caso bastante frecuente en que el cuerpo de la función es una simple sentencia return seguida de una expresión que evalúa el resultado (como en el caso de nuestro cuadrado), se hace aún más evidente el exceso sintáctico.

Las expresiones lambda pueden considerarse como una extensión de los métodos anónimos, que ofrecen una sintaxis más concisa y funcional para expresarlos. La sintaxis de una expresión lambda consta de una lista de variables-parámetros, seguida del símbolo de implicación (aplicación de función) =>, que es seguido a su vez de la expresión o bloque de sentencias que implementa la funcionalidad deseada. Por ejemplo, nuestra definición de cuadrado quedaría de la siguiente forma utilizando una expresión lambda:

        Mapeado<int> cuadrado3 = x => x * x;

 

¿Más corto imposible, verdad? Se expresa de una manera muy sucinta y natural que cuadrado3 hace referencia a una función que, a partir de un x, produce x multiplicado por sí mismo. En este caso, el compilador deduce (infiere) automáticamente del contexto que el tipo del parámetro y del resultado deben ser int. Pudimos haberlo indicado explícitamente:

        Mapeado<int> cuadrado4 = (int x) => x * x;

 

Si se especifica el tipo del parámetro, o la expresión tiene más de un parámetro (se indiquen explícitamente o no sus tipos) los paréntesis son obligatorios. Por otra parte, en la parte derecha de la implicación se puede colocar una expresión, como hemos hecho hasta el momento, o un bloque de sentencias de cualquier complejidad (siempre que por supuesto produzca como resultado un valor del tipo adecuado):

        Mapeado<int> cuadrado5 = (int x) => { return x * x; };

 

Aunque para una mejor comprensión hemos definido nuestro propio tipo delegado genérico Mapeado<T>, en la práctica la mayor parte de las veces utilizaremos las distintas sobrecargas del tipo genérico predefinido Func:

        // en System.Query.dll

        // espacio de nombres System.Query

        public delegate T Func<T>();

        public delegate T Func<A0, T>(A0 a0);

        public delegate T Func<A0, A1, T>(A0 a0, A1 a1);

        public delegate T Func<A0, A1, A2, T>(A0 a0, A1 a1, A2 a2);

        public delegate T Func<A0, A1, A2, A3, T>(A0 a0, A1 a1, A2 a2, A3 a3);

 

El tipo Func representa a los delegados a funciones (con 0, 1, 2 ó 3 argumentos, respectivamente) que devuelven un valor de tipo T. Utilizando este tipo, podríamos definir una nueva versión de cuadrado así:

        Func<int, int> cuadrado6 = x => x * x;

 

A continuación se presentan algunos ejemplos más de definiciones y uso de expersiones lambda:

        // más ejemplos

        static Func<double, double, double> hipotenusa =

            (x, y) => Math.Sqrt(x * x + y * y);

 

        static Func<int, int, bool> esDivisiblePor =

            (int a, int b) => a % b == 0;

 

        static Func<int, bool> esPrimo =

            x => {

                for (int i = 2; i <= x / 2; i++)

                    if (esDivisiblePor(x, i))

                        return false;

                return true;

            };

 

        static void Main(string[] args)

        {

            int n = 19;

            if (esPrimo(n)) Console.WriteLine(n + " es primo");

            Console.ReadLine();

        }

 

3. Las expresiones lambda de C# como entidades de datos

Con lo visto hasta el momento, cualquiera podría pensar que las expresiones lambda son un mero recurso de tipo “azúcar sintáctico” para “endulzar” el consumo de delegados y métodos anónimos. Pero las expresiones lambda de C# 3.0 ofrecen una posibilidad adicional, no disponible para los métodos anónimos, y que juega un papel importante en la implementación de la tecnología LINQ (Language Integrated Query – Consultas Integradas en el Lenguaje), que también será incorporada a la próxima versión de C# y VB y que constituye, según la modesta opinión de este autor, uno de los avances más significativos de los últimos tiempos en el área de los lenguajes y sistemas de programación.

Esta posibilidad adicional es la de compilarse como árboles de expresiones, objetos de datos que representan en memoria al algoritmo de evaluación de una expresión lambda. Estos árboles en memoria pueden ser luego fácilmente manipulados mediante software (¡metaprogramación!), almacenados, transmitidos, etc. Aunque aplazaremos un análisis más detallado de los árboles de expresiones para una próxima entrega, veamos el pequeño ejemplo a continuación:

        static Expression<Func<double, double, double>> hipotenusaExpr =

            (x, y) => Math.Sqrt(x * x + y * y);

 

        static void Main(string[] args)

        {

            Console.WriteLine(hipotenusaExpr);

            // imprime '(x, y) => Sqrt(Add(Multiply(x, x), Multiply(y, y)))'

 

            hipotenusa = hipotenusaExpr.Compile();  // aquí se genera ILAsm !

            Console.WriteLine("Hipotenusa(3, 4) = " + hipotenusa(3, 4));

            // imprime 'Hipotenusa(3, 4) = 5'

 

            Console.ReadLine();

        }

 

En el fragmento de código anterior, la expresión lambda para calcular la hipotenusa de un triángulo rectángulo se asigna no a un delegado, sino a una variable del tipo genérico predefinido Expression:

        // en System.Query.dll

        // espacio de nombres System.Expressions

        public class Expression<T> : LambdaExpression { }

 

En este caso, no se genera el código ILAsm correspondiente a la expresión, sino un árbol de objetos que representa a la secuencia de acciones necesarias para su evaluación. De la estructura del árbol generado podemos hacernos una idea observando la salida que produce la llamada a su método ToString().

Observe que la clase Expression ofrece un método Compile(), mediante el cual se hace posible generar dinámicamente el código ejecutable correspondiente a la expresión en el momento deseado. O sea, que este método sirve como “puente” entre los dos mundos, traduciendo los datos en código en el momento en que se hace necesario evaluar la expresión.

 

4. Conclusiones

Las expresiones lambda son una de las principales novedades que incluirá la próxima versión 3.0 de C#. En este artículo hemos visto cómo las expresiones lambda tienen dos facetas de representación y utilización: como código (en forma de métodos anónimos) y como datos (en forma de árboles de expresiones), estrechamente relacionadas entre sí. En nuestra próxima entrega hablaremos con mayor profundidad sobre los árboles de expresiones.

El código fuente del ejemplo utilizado en el artículo está disponible para su descarga. Para poder compilarlo y ejecutarlo satisfactoriamente, se deberá instalar inicialmente la Presentación Preliminar de LINQ de Mayo de 2006, disponible en [1].

 

5. Agradecimientos

El autor quiere aprovechar esta oportunidad para agradecer públicamente a Guillermo “El Guille” Som, no tanto por la oportunidad que me ha brindado de participar en esta ocasión especial como por la sincera amistad que me ha ofrecido desde el primer momento y por las muchas experiencias y conocimientos que me ha transmitido de forma totalmente desinteresada. ¡Gracias, Maestro!

 

6. Referencias

  1. Recursos relacionados con C# 3.0 y LINQ: http://msdn.microsoft.com/csharp/future/default.aspx.
  2. Box, Don “Scheme is love”, publicado en MSDN Magazine, octubre de 2005.
  3. Hernández, Octavio “Lo que nos traerá Orcas: novedades en C# 3.0”, publicado en dotNetManía Nº 24, marzo de 2006.
  4. Hernández, Octavio “Lo que nos traerá Orcas: la tecnología LINQ”, publicado en dotNetManía Nº 25, abril de 2006.
  5. Som, Guillermo “Delegados y eventos (1ª y 2ª partes)”, publicados en dotNetManía Nº 30 y 31, octubre y noviembre de 2006.

 


Código de ejemplo (ZIP):

 

Fichero con el código de ejemplo: octavio_ExpresionesLambda.zip - 2.93 KB

(MD5 checksum: DE43576E155B16397E81261C836E5504)

 


Ir al índice principal de el Guille