Functors – Más detalles

Ya he mencionado un poco acerca de Functors cuando hablé de Monads. Aquí ampliaré un poco al respecto.

Matemáticamente hablando, un functor es un tipo de “mapeo” entre categorías… sí, esa definición no ayuda mucho. Supongamos que tenemos dos conjuntos C y D, y objetos X que pertenecen a C.  Un functor F asocia a esas X con un objeto F(X) que pertenece a D (tomado tal cual de la definción en Wikipedia). Si hablamos de morfismos, asocia X -> Y que pertenecen a C con F(X) -> F(Y) que pertenecen a D.

functor

Muchas matemáticas de por medio…

En lo que concierne a programación funcional, un functor es una typeclass cuyos miembros deben saber mapear un morfismo que toma un valor y lo transforma en otro, sin tener que “sacarlo” para aplicarlo. Esos “morfismos” son simplemente funciones que toman un argumento del tipo de datos que envuelve el Functor y regresan otro tipo de datos (que puede ser que sea el mismo).

Demasiada confusión…

Supongamos que tenemos una función f: String -> Int que lo que hace es regresar la longitud de la cadena que le entra. Es obvio que el parámetro que espera es una cadena, y si le pasamos otro de un tipo diferente, no lo aceptará. ¿Todo bien hasta aquí? Ahora, imaginemos que tenemos una “caja” que puede o no contener valores de un tipo determinado. A esa caja le llamaremos F. Una variable de este tipo que contenga cadenas será entonces F[String]. Si queremos aplicar nuestra función f a la cadena que está dentro de la caja, directamente no podemos hacerlo porque f espera un String, no un F[String] como parámetro. Lo que hacen los functors es que saben cómo aplicar esa función a los elementos que contienen. Entonces, no se trata de cambiar los parámetros de f a F[String] -> Int, sino delegarle la caja la forma de implementar esa función a sus elementos sin necesidad de cambiarla. Entonces, la caja debe tener una función g que reciba un parámetro del tipo de datos que contiene, y regrese cualquier otro tipo de datos, pero en vez de regresar el valor nada más, lo hará envuelto en una caja del mismo tipo. Es decir, g tiene que estar definida más o menos como:

g: (f: A -> B)(F[A]): F[B]

Y si en vez de poner a F[A] como parámetro de g hacemos que g regrese una función, la definición quedaría como:

g: (f: A -> B): F[A] -> F[B]

que con los tipos de datos de nuestro ejemplo quedaría:

g: (f: String -> Int): F[String] -> F[Int]

Esta función g en Scala se llama “map”, y en Haskell se llama “fmap”. Entonces, cuando hablamos de functores, hablamos de una typeclass que requiere que sus miembros implemente una función “map” que aplique  una función a lo que contienen, y regrese los resultados envueltos en otro Functor (en este caso del mismo tipo).

Cuando una función es aplicada a un contexto diferente sin tener que cambiarle explícitamente los tipos de datos que recibe, se dice que la función fue levantada a ese contexto. En inglés, esto se conoce como “function lifting”. En el caso de los Functors, lo que hacemos es levantar la función a su contexto, es decir, hacemos que una función f que toma tipos de datos “simples”, se transforme implícitamente en una que toma tipos de datos “complejos” (en este caso, Functors). O sea que f: String -> Int “se levanta” a f: F[String] -> F[Int].

Todo lo anterior parece muy complicado, pero en realidad el concepto es muy sencillo.

Veamos un tipo de datos que pertenece a Functor: una lista. La función “map” de una lista debe aplicar una función a cada uno de sus elementos y regresar los resultados en otra lista. Siguiendo con el ejemplo de la función que toma una cadena y regresa su longitud, si queremos hacer algo similar en Java (ignoren Java 8 por el momento) tendríamos que implementar algo como esto:

List<Int> mapLengths (List<String> list) {
     
     List<Int> lengths = new List<>();
     for (String s : list) 
        lengths.add(s.length)
     

     return lengths;
}

En Java, las listas no son Functors, pero en Scala sí, por lo que sabemos entonces que debe existir una función map, Viendo el Scaladoc de List tenemos que:

final def map[B](f: (A) ⇒ B): List[B]

Entonces solamente necesitamos pasarle a map la función que queremos que sea aplicada a los elementos de la lista, y como podemos usar expresiones lambda, el código en Java arriba queda de la siguiente forma en Scala:

// Definimos la lista
scala> val l = List("Hola","a","todos")
l: List[String] = List(Hola, a, todos)

// Aplicamos la función con map
scala> l map (s => s.length)
res0: List[Int] = List(4, 1, 5)

En particular, ésta es la línea que reemplaza al código en Java:

l map (s => s.length)

Y si queremos ser más concisos, podemos escribir lo anterior como:

l map (_.length)

El subguión simplemente es un placeholder que tomará el valor de cada elemento de la lista, por lo que la función puede leerse como “mapea la función length de cada elemento de la lista”.

Para que uno de nuestros tipos de datos pertenezca a la typeclass Functor en Scala necesita, además de implementar la función map, cumplir con las 2 leyes de functors:

  1. Si mapeamos la función de identidad a un Functor, el resultado tiene que ser un Functor idéntico al original. La función identidad simplemente regresa intacto el argumento recibido; en Scala se llama “identity”.
  2. Si componemos dos funciones y mapeamos la función resultante sobre un Functor, el resultado debe ser igual que si primero mapeamos una función sobre el Functor y luego la otra.
    El ejemplo que viene acá describe muy bien lo anterior.

 

scala> (List(1, 2, 3) map {{(_: Int) * 3} map {(_: Int) + 1}}) assert_=== (List(1, 2, 3) map {(_: Int) * 3} map {(_: Int) + 1})

Hay que tener en cuenta que el compilador no avisa si un Functor no cumple las 2 leyes.  Eso es un chequeo que hay que hacer manualmente (Scalaz tiene clases que ayudan a probarlas) antes de asegurar que nuestro tipo de datos es un Functor.

Como se puede observar, los Functors simplemente nos aseguran que son capaces de aplicar una función a los elementos que contengan, y el resultado será “encapsulado” en otro Functor.

El tema que sigue en esta serie de escritos es respecto a los Applicative Functors, los cuales son una extensión natural de los Functors, pero que contienen tremendo poder si se saben aplicar correctamente.