Currying y aplicación parcial de funciones

Dos conceptos que son importantes en programación funcional, y que a primera vista no lo parecen, son currying aplicación parcial de funciones. Ambos están ligados entre sí, pero no son lo mismo.

Por cierto, en Wikipedia usan currificación para referirse al currying. Aquí usaré el término original en inglés.

Currying

El término se refiere a tomar una función que toma n parámetros y transformarla en una serie de funciones que toman un número menor de parámetros. Aquí entran en juego los conceptos de funciones como ciudadanos de primera clase y funciones de orden superior:

Cuando definimos una función que toma argumentos, los declaramos casi siempre de la siguiente forma:

// Java
private boolean areEqual(int x, int y) {
     return x == y;
}
// Scala
def areEqual(x: Int, y: Int) = x == y

Y los pasamos a la función así:

areEqual(2,3)

Podemos decir que la función areEqual tiene aridad 2; es decir: necesita 2 parámetros para poder ser calculada. Además, regresa un valor de tipo booleano.

Cuando aplicamos currying a areEqual, lo que obtenemos es una función que toma un argumento int y regresa otra función que toma un argumento int y que a su vez regresa un valor de tipo booleano.

Pongámoslo así . La => indica “función”. A la izquierda van los parámetros y lo que está a la derecha es el valor de retorno:

  • areEqual = (Int, Int) => Boolean (Aridad 2)
  • currying areEqual = Int => (Int => Boolean) (Aridad 1)

¿Cómo se hace el currying? Depende del lenguaje, pero básicamente es posible si éste permite expresiones lambda.

// Scala
def areEqual(x: Int) = (y: Int) => x == y

// También es posible así:
def areEqual(x:Int)(y:Int) = x == y

Si revisamos el tipo de la función, encontramos que son lo mismo. Ignoren aquí el subguión que aparece. Lo explico más adelante:

scala> def areEqual(x:Int)(y:Int) = x == y
areEqual: (x: Int)(y: Int)Boolean

scala> :t areEqual _
Int => (Int => Boolean)

scala> def ae2(x: Int) = (y:Int) => x == y
ae2: (x: Int)Int => Boolean

scala> :t ae2 _
Int => (Int => Boolean)

C# también permite currying:

public static Func<int,bool> areEqual(int x) {
		return y => x == y;
	}

Console.Write(areEqual(2)(3)); // False

Hacer currying en Java es técnicamente posible, pero hay que hacerlo por medio de interfaces, y la cantidad de código que se tiene que escribir es exagerada. Alguien acá se tomó la molestia de poner un ejemplo:

http://stackoverflow.com/questions/6134278/does-java-support-currying

Aplicación parcial de funciones

Si una función tiene aridad n y la llamamos con m parámetros, donde m < n, normalmente el compilador nos dirá que nos faltan argumentos.

En el ejemplo anterior, si llamamos a areEqual con un parámetro nada más, el compilador nos dice que necesitamos 2 argumentos. Todo normal.

Aplicar parcialmente una función significa precisamente eso que el compilador nos diría que es un “error”: aplicamos una función a un número menor de parámetros que los que espera, y lo que obtenemos es una función que recibe los parámetros que hacen falta y regresa un valor del tipo que devuelve la función original. El “truco” consiste en asignar esa función que se regresa a un identificador (como una variable), o aplicarla directamente.

Reusemos areEqual. Esta vez, la llamaremos con un parámetro nada más y asignaremos el resultado a un identificador. Aquí viene la explicación sobre el subguión que prometí más arriba:  en Scala, necesitamos poner un subguión en el lugar de los parámetros que faltan para indicar que estamos aplicando parcialmente la función:

// Scala

/* Sin usar subguión, el compilador nos indica
   que faltan argumentos, y que debemos agregarlo
   si queremos tratar la acción como una función
   parcialmente aplicada
*/

scala> val f = areEqual(2)
<console>:8: error: missing arguments for method areEqual;
follow this method with `_' if you want to treat it as a partially applied function
       val f = areEqual(2)

/* Ahora con subguión.
   Aplicamos parcialmente areEqual, y el resultado
   lo asignamos a f.
   f es una función que recibe un entero y regresa un boolean.
*/

scala> val f = areEqual(2) _
f: Int => Boolean = <function1>

// Ahora podemos usar f

scala> f(3)
res0: Boolean = false

scala> f(2)
res1: Boolean = true

Cabe mencionar es más fácil aplicar parcialmente una función en Scala si la función  tiene múltiples listas de parámetros. En nuestro ejemplo, areEqual está definida así:

def areEqual(x: Int)(y: Int) = x == y

x y y están en diferentes lugares de parámetros. Si por el contrario la función estuviera definida así:

def areEqual(x: Int, y: Int) = x == y

Tenemos que hacer lo siguiente para aplicarla parcialmente:

scala> val f = areEqual(2,_:Int)
f: Int => Boolean = <function1>

scala> f(3)
res0: Boolean = false

scala> f(2)
res1: Boolean = true

Es decir: hay que especificar el tipo de datos del parámetro que no estamos incluyendo.

Muy bonito… ¿y?

Es fácil confundir currying con aplicación parcial de funciones. Estrictamente hablando, ambos toman una función de aridad n y regresan una función de aridad menor. La diferencia es que en aplicación parcial de funciones se fijan con valores específicos algunos parámetros.

Lo anterior nos permite abstraer conceptos de forma mucho más sencilla. Esto se hace importante cuando trabajamos con funciones de orden superior y comenzamos a pasar funciones por aquí y por allá.

Como nota, en Haskell todas las funciones reciben exactamente un parámetro. Lo que sucede cuando se crea una con varios parámetros es que Haskell hace currying y crea una serie de funciones, cada una recibiendo un parámetro:

-- Esto es Haskell
-- Ejemplo tomado de http://aprendehaskell.es/content/OrdenSuperior.html

-- Función que multiplica 3 números

multThree :: (Num a) => a -> a -> a -> a
multThree x y z = x * y * z

Para fines prácticos diríamos que multThree toma 3 parámetros y regresa un valor numérico, pero lo que realmente sucede es que  el primer parámetro se aplica a multThree, y esto regresa una función que toma un parámetro (el segundo), y regresa una función que aplica el tercer parámetro y regresa el resultado.

En cuanto a aplicación parcial, como ya mencioné, el uso más lógico es fijar uno o varios de los parámetros de una función en vez de estarla llamando con el mismo parámetro repetido cada vez.

Por ejemplo, hace poco tuve que crear un parser para cierto formato de documentos. Al momento de extraer información, tenía que escribir en 3 diferentes logs dependiendo el tipo de información que extrajera o si encontraba algún error. Tengo una función simple (copiada de internet) que escribe lo que envíes en el archivo que indiques:

private def printToFile(f: java.io.File)(op: java.io.PrintWriter => Unit) {
    val p = new java.io.PrintWriter(f)
    try {op(p)} finally {p.close()}
  }

En vez de estar alternando entre 3 archivos, lo que hice fue crear 3 funciones que aplicaban parcialmente printToFile con el primer parámetro (el archivo) previamente determinado:

val logError = printToFile(new File(errorfile)) _
val logInfo  = printtoFile(new File(infofile)) _
val logDetails = printtoFile(new File(detailsfile)) _

Al momento de usarlas, lo único que tengo que pasar es el parámetro faltante: una función que toma un PrintWriter y regresa nada relevante (Unit):

/*
  Extrae la información y guarda cada parte
  en el archivo usando p.println
*/
logInfo(p => {
   extractInfo(something) map {p.println}
})

Todo lo anterior se vuelve sumamente importante en programación funcional. Por mencionar un ejemplo, el State Monad (un Monad que permite guardar y modificar estados) se compone de un tipo de datos parcialmente aplicado. Si no se conoce el concepto, es muy fácil perderse y confundirse.

Recuerden que este paradigma no es necesariamente mejor que otros. Simplemente es una forma diferente de hacer las cosas, con sus ventajas y desventajas.

Java 8

Al parecer, Java 8 soportará muchos conceptos de programación funcional. Acá algunas de las mejoras a este lenguaje:

http://www.javaworld.com/javaworld/jw-07-2013/130725-love-and-hate-for-java-8.html

Por tanto, no está de más aprenderlos para poder usarlos, aún cuando no se usen lenguajes 100% funcionales.