Bind FTW!

Monads – Yo también tenía que escribir al respecto

Seguí la tradición: cuentan las leyendas que cuando una persona comienza a ver la luz al usar Monads, invariablemente escribe un tutorial. Pero en mi caso, no es un tutorial, sino más bien una breve explicación de lo que son, junto con algunos ejemplos. Esto es con el fin de que yo mismo compruebe si entiendo el concepto general, y al mismo tiempo de que salgan gurús en programación funcional y me corrijan y me digan en qué estoy mal.

Ésta no es una guía exhaustiva, y omitiré muchos conceptos, pero pondré referencias por aquello de que haya interesados en el tema.

Como nota, sé que en español los términos son Funtor mónadas, pero nada más no me entran en la cabeza, por lo que los usaré en inglés. Además, aunque no está dirigido a un lenguaje en particular, la mayoría de los ejemplos mostrados están en Scala.

Funciones de orden superior

Algunos ya han visto esto, sobre todo si últimamente han trabajado con herramientas como jQuery.

Una función de orden superior es simplemente una función que:

  1. Tiene como parámetros una o más funciones, o bien
  2. Regresa una función como resultado

Por ejemplo, en Python, map toma como parámetros una función y una lista de elementos, y aplica esa función a cada uno de los elementos de la lista:

a = (1,2,3,4,5)

def plusOne(n):
    return 1 + n

map(plusOne,a)
[2,3,4,5,6]

Obviamente, también se pueden usar lambdas.

En Scala, que es el lenguaje que más uso actualmente, lo anterior, usando una expresión lambda, sería:

val a = List(1,2,3,4,5)

a map {_ + 1}
res0: List[Int] = List(2, 3, 4, 5, 6)

 

Functors

Pongámoslo así: Un functor es una construcción de tipo que define una función de orden superior que transforma una función a => b (siendo a y b tipos de datos) en una función F a => F b., lo que se le llama mapear. Esto significa que si tienes un F que contiene datos de tipo a, y una función que toma como parámetro un dato de tipo a y regresa un dato de tipo b, lo que un Functor F define es la forma en la que se mapea su contenido (datos de tipo a), y el valor de regreso es el mismo tipo de Functor, pero con datos de tipo B.

Suena complicado, pero si entendieron los ejemplos explicados en la parte de funciones de orden superior, lo que están viendo es actuar a un Functor.

Menciono que “es una construcción de tipo”, porque no es en sí un tipo de datos, sino más bien una especie de contrato que describe características comunes de diferentes tipos de datos. En el mundo de la programación funcional esto se llama typeclasses, pero dejaremos eso para después.

En Scala, Haskell, y otros lenguajes funcionales, una lista es un Functor. La lista define una función llamada map para aplicar una función a cada uno de los elementos que contiene, y regresa una lista del tipo de datos que regresa la función aplicada.

En el ejemplo de arriba de Scala, lo que tenemos es una lista de enteros (List[Int]), y mapeamos una función que toma cada uno de los elementos, le suma uno, y lo añade a una lista nueva, que, en este caso, también será de enteros. Si, por ejemplo, convertimos cada uno de los elementos de la lista a String, el resultado será una lista de Strings:

a map {_.toString}
res1: List[String] = List(1, 2, 3, 4, 5)

Cambiando las F por listas en la definición de functors explicada, tenemos una Lista de elementos de tipo a (Int), definimos una función que toma un entero y lo convierte a String (regresando con ello un String) y la mapeamos a dicha Lista, obteniendo otra Lista, pero de elementos de tipo b (String).

Si vemos la declaración de List.map en el API de Scala encontramos lo siguiente:

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

Lo que concuerda con la definición anterior (el tipo genérico A está incluido en la declaración del tipo de dato List, por eso aquí no aparece). En el ejemplo anterior, la parte de f: (A) => B corresponde a:

{_.toString}

Que a su vez, y gracias a la sintaxis de Scala, es el equivalente a:

{i => i.toString}

Siendo i cada elemento de la lista. La función cumple con la declaración: El tipo A es Int, y la función toma un Int y regresa un tipo B (String). map se encarga de aplicarla a todos los elementos de la lista y de añadir los resultados a una nueva lista.

Hay muchos otros functors (árboles, por ejemplo), pero la analogía es la misma.

Los functors tienen que obedecer 2 reglas: identidad y asociatividad, pero eso no lo cubriremos aquí. En los tutoriales se explica mucho mejor, aunque son conceptos que se sobreentienden con sus nombres.

Sí, sí, pero ¿y los Monads?

Para allá voy. Cuando comencé a estudiar todo este rollo, estaba medio arisco porque en casi todos los lugares mencionaban que hay que estudiar teoría de categorías para entender lo que son los Monads, y la programación funcional en general. Aunque estrictamente eso es cierto, la verdad es que entender el concepto de Monads y aplicarlo en los programas realmente no requiere conocer tan a fondo toda esa teoría. Ciertamente es mucho mejor si se aprende, pero una vez comprendiendo la idea de lo que son los Monads es relativamente fácil ponerlos en código.

Un Monad es una construcción de tipo que envuelve un tipo de dato y define una forma de combinarse con otras operaciones. Podemos decir que un Monad es un Functor que además de mapear, define la forma de combinar operaciones y de envolver en él tipos de datos simples.

Sí, lo sé. Suena complicado, y mi definición quizá no sea la mejor. Aunque no 100% exacta, la analogía que me ayudó a entender mejor todo esto es la de la caja: Imaginen que tienen un dato del tipo que quieran. Ese dato lo meten a una caja, que tiene ciertas características, y hay operaciones definidas para sacar el elemento de la caja, trabajar con él y meterlo de nuevo en una caja del mismo tipo que la anterior. La “caja” es el Monad en este caso.

Las operaciones que un Monad debe definir son:

  • Poner en contexto un dato. Es decir, “meterlo a la caja”. Esto tiene diferentes nombres dependiendo del lenguaje. En Haskell es returnEn Scala puro no existe una función que lo haga, pero si añadimos Scalaz , esta librería nos proporciona la función point.
  • Definir la forma en la que la caja se combinará con funciones que reciban como parámetros tipos de datos que la caja contenga. Esto incluye definir también lo que pasa cuando la caja no contiene nada. Esta operación se llama por lo general bind; en Haskell es >>= y en Scala es flatMap. Scalaz incluye también >>=

Quizá el Monad más fácil de entender es el Maybe Monad, llamado en Scala Option. Cualquiera que sea el nombre que le pongan, lo que ese Monad envuelve es un dato que puede o no existir, facilitando enormemente la combinación de operaciones y evitando largas líneas de revisión a ver si una variable es null o no.

Creemos una función simple: división entre 2 números. Parece sencillo, pero hay que recordar que si dividimos entre cero el mundo se acaba. Generalmente lo que se hace es regresar un valor default para indicar un error (como -1), aunque no es del todo correcto. Usemos Java:

public double division(double a, double b) {
    return b == 0 ? -1 : (a / b);
}

Y eso usando un one-liner. Como saben, regresar un valor default no siempre está bien porque puede ser también el resultado de la operación. Hay quienes por ejemplo regresarán Double.MIN_VALUE y se evitarán problemas.

¡Ah! Pero Java (y otros lenguajes) tienen forma de aventar y atrapar excepciones. Probemos aventando una de tipo “parámetro ilegal”:

public double division(double a, double b) throws IllegalArgumentException {
    if (b == 0)
      throw new IllegalArgumentException("b is zero");
    else
      return a/b;  
}

Y eso implica atraparla en donde se manda llamar esta función. Algo como:

public void main(String[] a) {
    try {
       System.out.println(division(1,0));       
    } catch (IllegalArgumentException e) {
      System.err.println(e.getMessage());
    }
}

Ahora bien, si necesitamos el valor de regreso de la función division para pasarlo a otra función, y esa función a su vez puede aventar otra excepción, tenemos:

public void main(String[] a) {
    try {
        double result = division(1,0);
        System.out.println("Result is: " + doSomething(result));
    } catch (IllegalArgumentException e) {
      System.err.println(e.getMessage());
    } catch (Exception e) {
      System.err.println(e.getMessage());
    }
}

Como se pueden dar cuenta, el resultado de las funciones divisiondoSomething puede estar presente o no. Si no atrapamos las excepciones y usamos un valor default, hay que revisar después si el resultado es igual a ese valor default para poder definir si un error ocurrió.

El Maybe Monad se usa precisamente para “envolver” datos que pueden o no existir, dándole un tipo definido al resultado que se tiene que regresar, y proporcionando una forma simple de evaluar si un resultado fue satisfactorio o no.

Usemos Scala y el tipo Option.

def division(a: Double, b: Double): Option[Double] = 
  if (b == 0) None else Some(a/b)

Aquí de nuevo revisamos si el valor b es cero, y de ser así, regresamos el valor Noneque indica que la “caja” llamada Option no contiene ningún valor; si es diferente de cero, metemos el resultado de la división en la “caja” con Some.

Ahora bien, si no usáramos las operaciones monádicas, tendríamos que revisar si el resultado de division contiene algo o no. Es posible hacerlo con pattern matching, pero si lo que nos importa es si la segunda función puede ser ejecutada o no, podemos aprovechar el flatMap para evitar hacer ese chequeo.

Definamos doSomething:

def doSomething(d: Double): Option[String] = d match {
   case n if n > 1: Some("The parameter is greater than 1")
   case n if n == 1: Some("The parameter is exactly 1")
   case _ => Some("The parameter is lesser than 1")
}

¿Por qué doSomething debe regresar un Option[String] cuando visiblemente no hay casos donde no contenga nada? Porque sigue las reglas de bind para combinar Monads.

La operación bind se define de la siguiente forma:

m a -> (a -> m b) -> m b

Si recuerdan arriba en los Functors, la “caja” era F. Aquí, esa caja es “m”. Bind toma como parámetros:

  •  m a : Una “caja” envolviendo un valor de tipo a
  • a -> m b : Una función que toma un parámetro de tipo a y regresa un valor de tipo b envuelto en una caja de las mismas características que el punto anterior.
  • El resultado de bind es precisamente el resultado de lo anterior (m b).

Si cambiamos las m por Option, y poniendo todo en términos de Scala, tenemos que:

Option[A] => (A => Option[B]) => Option[B]

Que en nuestro caso particular es:

Option[Double] => (Double => Option[String]) => Option[String]

El primer valor es lo que nos regresa la función division, y el segundo valor es la función doSomething.

Entonces, en vez de hacer algo como esto en Java:

public static double division (double a, double b) {
   return b == 0 ? -1 : (a/b)
}

public static String doSomething (double d) {
   if (d > 1)
     return "parameter is greater than 1";
   else if (d == 1)
      return "parameter is exactly 1";
   else
      return "parameter is lesser than 1";
}
public static void main(String args[]) {
  // argument1 y argument2 tomados de args y convertidos exitosamente a tipo double
   val result = division(argument1,argument2);

   if (result != -1) 
      System.out.println(doSomething(result));
   else 
      System.out.println("Division by zero");
}

Hacemos algo como esto:

def division(a: Double, b: Double): Option[Double] = 
  if (b == 0) None else Some(a/b)

def doSomething(d: Double): Option[String] = d match {
   case n if n > 1 => Some("The parameter is greater than 1")
   case n if n == 1 => Some("The parameter is exactly 1")
   case _ => Some("The parameter is lesser than 1")
}

def main(as: Array[String]) { 
  // argument1 and argument2 parseados exitosamente
   println(division(argument1,argument2).flatMap{doSomething}.getOrElse("Division by zero!"))
}

Quizá este ejemplo no sea el mejor para demostrar lo útiles que son los Monads, pero creo que es fácil de comprender. Veamos otro ejemplo un poco más significativo:

Supongamos que tenemos un pequeño programa que controla los bonos que se le dan a los empleados en una serie de compañías. También supongamos que tenemos 2 clases para representar a la compañía en la que el usuario labora  y a los mánagers (que curiosamente es un término aceptado en la RAE) de las compañías : Company y Manager.

Ahora bien, para revisar si un empleado obtiene un bono, hay que encontrar primero la compañía a la que pertenece y después a un mánager, y luego “preguntarle” al mánager la cantidad del bono del usuario.

Pensando en Java tendríamos algo como:

public static double getBonusForUser(User user) {
    double bonus = 0;
    Company company = getCompanyForUser(user);

    if (company != null) {
        Manager manager = company.findManager();

        if (manager != null) {
           bonus = manager.getBonusForUser(user);
        }
    }

   return bonus;
}

Es necesario revisar que la compañía y el mánager no sean null para evitar una súper NullPointerException (que todos amamos, claro). Ahora imaginen el caso en donde hay que revisar más valores a ver si son null o no, la escalerita de if se hace más larga.

Si en vez de que getCompany y findManager regresaran una Company o Manager respectivamente (con el riesgo de que no haya y sean null) devolvieran un valor que puede o no contener un objeto de clase necesaria, el código sería mucho más legible. Veamos en Scala usando Option:

def getBonusForUser(user: User): Double = 
   getCompanyForUser(user).flatMap{_.findManager}.flatMap{_.getBonusForUser(user)}.getOrElse(0.0)

getCompanyForUser regresa un Option[Company]; Company.findManager regresa un Option[Manager] y Manager.getBonusForUser regresa un Option[Double]. En el momento en el que una de estas Option no contenga valor, el resultado de la operación es None, es decir, una Option que no contiene nada. El último getOrElse simplemente trata de tomar el valor que está dentro de la Option, y si no lo encuentra, devuelve el parámetro que le pasemos, en este caso “0”.

Ahora bien. El código anterior en Scala se ve muy “pro” en una línea, pero también hay que pensar en legibilidad. He aquí la belleza en todo esto: existe una forma mucho más sencilla y clara de escribir lo anerior. En Haskell es la do notation, mientras que en Scala es una for comprehension. Como estamos viendo todo con Scala para no asustar a quienes nunca han visto código en Haskell, usemos lo segundo:

def getBonusForUser(user: User): Double = {

  val optionbonus = for {
    c <- getCompanyForuser(user)
    m <- c.findManager
    b <- m.getBonusForUser(user)
  } yield b

  optionbonus.getOrElse(0.0)

}

Esto es sólo una forma más fácil y comprensible de escribir lo anterior. Lo que sucede es que dentro de la for comprehension las Option se están pasando como ninjas, es decir, en background. Todavía se siguen ejecutando los flatMaps, pero para nuestros ojos, esto es mucho más comprensible.

Cabe mencionar que las for comprehensions y la do notation trabajan con cualquier Monad.

Los Monads también deben cumplir 3 leyes para poder ser considerados como tal: identidad a la izquierda, identidiad a la derecha y asociatividad, pero ésas tampoco las mencionaré aquí. Son ciertamente necesarias para comprender cómo se comportan los Monads, especialmente cuando creamos uno nuevo, pero para efectos de este post las omitiremos.

Asimismo, omití por completo la explicación de los Applicative Functors, que aunque no es necesaria para entender el funcionamiento básico de los Monads, sí lo es para entender la teoría y algunos otros aspectos importantes. Lo menciono aquií porque los Applicative Functors también tienen reglas que seguir.

 

Una lista es un Monad

Al menos en un lenguaje funcional lo es. En Java, aunque existe la interface List, no es monádica.

Si recordamos la definición de Monad, nos damos cuenta de que una lista envuelve una serie de valores en una “caja”, cumple las leyes de los Monads (no explicadas aquí) y define la forma de poner un valor en contexto (meterlo a la caja) y de combinarse con funciones que toman parámetros del tipo de datos que la lista guarda. En Scala podríamos definirlo de la siguiente forma:

def pointToList[A](a: A) = List(a)

Es decir: dado un valor de cualquier tipo, para ponerlo en contexto simplemente lo envolvemos en una lista.

Bind es, como ya mencionamos, el flatMap, el cual podríamos definir así:

def flatMap[B](f: (A) => List[B]): List[B]

Ciertamente, en Scala flatMap está definido un poco diferente, pero para efectos de entender la analogía lo dejemos así. f es una función que toma elementos de tipo A (el tipo de elementos que guarda la lista) y regresa una lista de tipo B.

Viéndolo desde un punto de vista más idiomático, flatMap aplica 2 operaciones: un map que aplica la función a cada elemento de la lista, lo que regresa un valor de tipo List[List[B]], y un flatten, que lo que hace es “aplanar” la lista de listas y la deja en una sóla:

scala> List(1,2,3,4).map(n=>List(n+1)).flatten
res15: List[Int] = List(2, 3, 4, 5)

scala> List(1,2,3,4).flatMap(n=>List(n+1))
res16: List[Int] = List(2, 3, 4, 5)

En Haskell, las operaciones del Monad Lista se definen como:

return x = [x]
xs >>= f = concat (map f xs)

Que si se fijan es básicamente lo mismo. El bind aplica la función f a la lista xs, y al final junta todas las listas en un sólo resultado.

 

¿De qué me sirve todo esto?

Los Monads no son exclusivos de lenguajes funcionales, porque aunque están definidos, son sólo un concepto. Es perfectamente posible crear Monads en otros lenguajes de programación. Por mencionar algunos, hay ejemplos en Python, Javascript, etc.

Además, para quienes han usado LINQ en Visual Studio, estoy seguro que se les hará familiar la do-notation o las for-comprehension. LINQ es monádico, y al pensar de esta manera, es mucho más fácil entender qué está pasando cuando hacemos una asignación con from y seleccionamos algo con SelectMany. Mucha gente piensa en SQL cuando ve algo así:

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 }; 

var lowNums = 
    from n in numbers 
    where n < 5 
    select n;

La realidad es que LINQ es monádico, y para poder usar este tipo de sintáxis en tipos definidos por el usuario, basta con definir SelectMany en ellos, que es exactamente lo mismo que definir bind en un Monad (hacen lo mismo).

Una vez que se entiende el concepto, independientemente si se usa o no, nos da una visión diferente al código que escribimos; tenemos un arma más para el diseño de programas.

La razón más obvia para entender Functors, Applicatives, Monads y demás es para entender lenguajes de programación funcional, especialmente lenguajes puros como Haskell, en donde se tienen que respetar todos los conceptos (no side-effects, no variables, funciones puras). Es más: en Haskell, escribir algo en la pantalla implica el uso de un Monad: IO. Por lo tanto, si no entendemos qué está pasando, nos costará más trabajo escribir programas ahí.

[Fuentes]

 ¡Aprende Haskell por el bien de todos!

Scalaz en GitHub

Learning Scalaz

Functional Programming – What is a Monad?

Monads en HaskellWiki

101 LINQ Samples en MSDN

Introduction to Monads in Scala

Scala Monads: Declutter your code with monadic design

Haskell: Understanding Monads – List

You could have invented monads!