Funciones como ciudadanos de primera clase y expresiones lambda

En la entrada anterior, varias personas me hicieron el apunte de que debería haber puesto la última parte (donde explico para qué puede ser útil ese conocimiento) en el principio. Otros comentaron que aunque es posible entender el escrito, sí es necesario conocer ciertos conceptos, como mapeo, para “agarrar el hilo”.

Agradezco mucho sus comentarios 🙂

Aunque no pretendo hacer (de momento) una guía exhaustiva sobre programación funcional, sí me gustaría plasmar algunos conceptos básicos que puedan servir para despertar el interés de investigar más.

Cabe mencionar que programar en forma funcional no necesariamente es mejor que la programación orientada a objetos. Cada paradigma tiene pros y contras, pero conocer lo que cada uno ofrece nos permite tomar mejores decisiones a la hora de crear código o de seleccionar un lenguaje para ciertas tareas.

Vayamos al grano:

Funciones como ciudadanos de primera clase

Término que suena totalmente exagerado para describir una característica de ciertos lenguajes de programación. Cuando algo puede ser ciudadano de primera clase, se refiere a que es un valor que puede ser asignado a variables, pasado como parámetro o devuelto como resultado en una función.

En lenguajes como Java, estos valores son los tipos de datos: int, double, String, Object, etc. Y como son de primera clase, no es raro verlos aplicados de esa manera en todas partes en el código:

// Esto es Java

int x = 1; // x es una variable de tipo entero

/*
   Aquí se pasa un instancia de User a la función, y ésta 
   devuelve un valor booleano.
*/
private boolean hasLastName(user: User) {
    return !"".equals(user.lastName);
}

Los lenguajes que permiten que las funciones sean de primera clase no tienen problema en que las funciones sean usadas justo como cualquier otro tipo de datos.

// Esto es Scala

/* square es una constante de tipo Int => Int, 
   es decir, una función que recibe un entero
   y regresa un entero */

val square = (x: Int) => x * x

// Por tanto, es posible usarla así:

square(4) // regresa 16

/* Esta función toma 2 parámetros: una lista de enteros y
   una función que toma una lista de enteros y regresa 
   un entero.

   Regresa un booleano dependiendo del resultado de
   aplicar la función parámetro a la lista 

*/

def calculateFromList(list: List[Int])(f: List[Int] => Int): Boolean = 
   f(list) > 15

/* Esta función no recibe parámetros.
   Regresa una función que toma un parámetro entero y regresa 
   un entero
*/

def cube: Int => Int = x => x*x*x

C# además de permitir lo anterior, tiene delegates, que básicamente son tipos a los que se les puede asignar cualquier método que coincida con su declaración (parámetros, tipos de datos y valor de retorno).

// Esto es C#

/*
  square es una función que toma un entero y
  regresa un entero
*/

Func<int,int> square = (x) => { return x * x };

/*
  Compose toma como parámetros 2 funciones que toman un parámetro cada una
  y regresan un valor.
  Regresa función que toma un parámetro del tipo que recibe el primer parámetro
  y regresa un valor del tipo del valor de retorno del segundo parámetro
*/

Func<A,C> Compose<A,B,C>(Func<A,B> f, Func<B,C> g) 
{
  return x => g(f(x));
}

// Compose en acción

Func<string, int> f1 = (str) => {
   return str == "ja" ? 1 : 0;
};

Func<int,boolean> f2 = (x) => {
   return x == 1;
};

Func fComposed = Compose(f1,f2);

fComposed("jo"); // false

/* Usando delegates. 
   Ejemplo tomado de http://msdn.microsoft.com/en-us/library/ms173172(v=vs.90).aspx
*/

public delegate void Del(string message);

public static void DelegateMethod(string message)
{
    System.Console.WriteLine(message);
}

// Instantiate the delegate.
Del handler = DelegateMethod;

// Call the delegate.
handler("Hello World");

Nada complicado una vez que se entiende qué está pasando.

Funciones de orden superior

Ya mencionadas en la entrada de Monads. Simplemente son funciones que aceptan como parámetros funciones, o bien que regresan funciones como resultados.

En el ejemplo anterior en C#, Compose es una función de orden superior.

Expresiones lambda

Otro término que se oye muy técnico. Una expresión lambda es una función que no está ligada a ningún identificador. En otros términos se puede definir como función anónima. Esto es: definir una función en el lugar en donde se esperaría una llamada a una función previamente declarada.

Por ejemplo, tomeemos un ejemplo previamente usado en Scala:

def calculateFromList(list: List[Int])(f: List[Int] => Int): Boolean = 
   f(list) > 15

El segundo parámetro es una función que toma una lista de enteros y regresa un entero. Normalmente, definiríamos primero la función y después la usaríamos como parámetro:

// Esto es Scala

/* Ésta es la función que pasaremos como parámetro.
   Suma los elementos dentro de la lista.
   Ignoren los subguiones misteriosos por el momento.   
*/
def functionToPass(l: List[Int]): Int = l.reduceLeft{_ + _}

val theList = 1 to 8 toList // List(1,2,3,4,5,6,7,8)

/* Llamemos a la función calculateFromList
   Regresa true porque functionToPass(theList) = 36   
*/
calculateFromList(theList)(functionToPass)

Ahora bien, en vez de usar algo ya declarado podemos usar una expresión lambda para definir la función en el lugar donde es necesaria, sin necesidad de declarar nada anticipadamente.

La siguiente forma de llamar a calculateFromList regresa exactamente lo mismo que la forma anterior:

// Seguimos en Scala

/* Regresa true.
   La función es declarada ahí y pasa como parámetro.
   Al aplicarla a theList, el resultado es 36
*/
calculateFromList(theList){aList => aList.reduceLeft{_ + _}}

Y si usamos aún más los misteriosos subguiones, también podemos hacer lo siguiente y obtener el mismo resultado:

// Scala

calculateFromList(theList){_.reduceLeft{_ + _}}

De hecho, la función reduceLeft de una lista toma como parámetro una función que toma 2 parámetros, por lo que también es una función de orden superior, y {_ + _} es la expresión lambda que pasa en lugar de la función.

Este tipo de conceptos puede simplificar muchas tareas que en lenguajes como Java necesitarían mucho más código. Por ejemplo, si quisiéramos encontrar si en una lista de números hay uno mayor a 4, en Java tendríamos que hacer algo como esto:

// Java

private boolean existsGreaterThanFour(List<Int> list) = {
  for (int x : list) 
     if (x > 4)
        return true;

  return false;
}

// theList contiene (1,2,3,4,5,6,7,8)

System.out.println(existsGreaterThanFour(theList)) // true

En Scala (y Haskell, por mencionar un par), el tipo List tiene una función llamada exists que recibe como parámetro un predicado, que no es nada más que una función que regresa un booleano, y regresa true si existe al menos un elemento que cumpla con ese predicado. Entonces, lo anterior en Scala quedaría así:

// Scala (por si hay duda)

// theList igual que arriba en Java

println(theList.exists(_ > 4)) // true

Aunque los ejemplos no son tan grandes, las diferencias se comienzan a notar en código más complejo.

Esto es sólo una muestra de lo que lenguajes más modernos nos ofrecen. Yo uso Scala, pero no necesariamente tiene que ser el mejor. C# es un lenguaje muy maduro y permite muchos conceptos de programación funcional (ya mencionamos que LINQ es monádico). Si tienen interés, échenle un vistazo a todo esto desde el punto de vista de su lenguaje favorito, vean si lo permite o no, y de ahí a practicar y conocer más.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.