Para inaugurar esta newsletter y enlazando con conceptos que he ido desarrollando en mi blog, hoy os traigo un artículo más práctico que teórico sobre un concepto que suele ser dificil ya no de entender, si no de aplicar en el día a día. Me estoy refieriendo, como no podía ser de otra manera, a las Mónadas. Y concretamente a una en especial: La mónada Maybe.
Es importante notar que cuando hablamos de “mónadas” nos referimos a la palabra que está compuesta de 3 sílabas y con vocal tónica en la “o”, lo que la convierte en una esdrújula. Por lo que no debemos confindirla con la palabra “monada” cuya fuerza tónica se encuentra en la penúltima sílaba, que la vuelve llana y como todas las llanas acabadas en ‘s’, ‘n’ o vocal, no lleva tilde.
A nosotros la que nos intersa es por supuesto, la esdrújula Mónada.
El problema
Antes de ponernos a mostrar ejemplos sobre como Maybe es la solución a muchos problemas, hay que conocer cuales son esos problemas. Al final, las mónadas entran en juego cuando hay cierto tipo de complejidad que necesitamos aislar y controlar.
En el caso de Maybe encapsulamos la diyuntiva de tener valores nulos, de modo que en lugar de tener que preguntar continuamente si un valor está realmente ahí o no para poder transformarlo, se nos facilita un API para aplicar transformaciones exista el valor o no.
Un Maybe es una caja en la que metemos un valor y a partir de ese momento perdemos la propiedad sobre el. Así, cada vez que queremos transformar el valor tenemos que abrir la caja, sacar el valor (de haberlo), aplicarle una función y cuidadosamente volver a meter el resultado en la caja.
Ese proceso por el cual se abre la caja está encapsulado dentro de Maybe y nosotros solo tenemos que preocuparnos de utilizar un API que nos permite centrarnos en nuestra lógica de dominio sin tener que preocuparnos si dentro de la caja hay algo o no. Es decir, las mónadas en general y el Maybe en específico nos ofrecen un tipo nuevo el cual utilizamos en nuestras computaciones sin tener que preocuparnos si dentro de ese tipo hay algo en realidad.
Por tanto, el tipo de código que se pretende simplificar, tiene esta pinta:
En un lenguaje con Javascript donde la nulidad o ausencia de valor está representada por dos tipos primitios, null y undefined. El tener una forma de evitar tener que comprobar continuamente si a lo que queremos acceder está ahí realmente, se vuelve casi una necesidad básica, ya que el opuesto es obviar que todo está ahí, no hacer ni una sola comprobación y mañana cuando queramos ejecutar nuestro código en digamos un servidor de node porque nos han chivado que el SSR es el último grito, ser testigos del aluvión de “undefined is not a function” porque la variable global window
sobre la que hemos edificado nuestro código, no existe en nodejs.
La solución
Al igual que los métodos .map, .filter y .reduce nos abstraen de la iteración de una lista con diferentes propositos y nos permite pensar en terminos de “cómo transformar” en lugar de “que hacer para transformar”, un Maybe nos permite olvidarnos de comprobar constantemente si los valores existen o no. De tal forma que el código anterior se convierte en esto:
Si window
, window.user
, window.user.name
o window.logger
son undefined/null no ocurrirá nada. En cambio si la información está disponible y suponiendo que “logger” imprima por pantalla el valor dado, lo que veremos será el contenido de window.user.name
. No hay condicionales ni bloques anidados y no tenemos que explicitamente usar los tipos null o undefined. Aun así nuestro código es robusto y no veremos ningún “undefined is not a function” si algo no está definido.
El punto clave de Maybe, es que da igual las operaciones que realicemos con el valor que hay en su interior ya que si en cualquier momento se genera un nullable (null/undefined), la operación devolverá un Nothing que sigue siendo del tipo Maybe, por lo que no se romperá el API.
El API
Ahora vamos a ir profundizando en los distintos mecanismos que nos ofrece el API del paquete de npm maybe-monada cuya documentación puedes consultar aquí.
Todo el código lo tienes disponible en Github.
Para instalar la librería, simplemente ejecuta esto en un proyecto npm:
npm i maybe-monada
maybe-monada se caracteriza por su bajo peso (menos de 5kb), su API y que al estar implementada con tipos especificos tanto para la ausencia de valor como la presencia del mismo, no ejecuta condicionales en ninguno de los métodos que expone al desarrollador, por lo que es más óptima que otras librerías ya existentes.
Un Maybe está formado por dos tipos: Just y Nothing. Just representa la presencia de un valor y Nothing lo opuesto (en algunas implementaciones Just también puede ser llamado Some). Estos tipos están definidos como sigue:
Just :: a → Maybe a
Nothing :: Maybe
Estas dos funciones son los “constructores” por las cuales podemos conseguir un tipo Maybe
.
Durante todo el artículo vamos a estar usando la sintáxis
FuncitonName :: types
para redactar los tipos. Puedes aprender más de las anotaciones aquí.
Además, maybe-monada nos ofrece 2 factorías interesantes para meter un valor dentro de un Maybe y realizan por nosotros las comprobaciones necesarias para construir un Just
(en caso de que el valor no sea ni null ni undefined) o un Nothing
(en el caso en el que estemos antes un nulo):
Maybe.of :: a → Maybe a
Maybe.from :: a → Maybe a
Realmente Maybe.of
y Maybe.from
hacen lo mismo, la diferencia en el nombre es simplemente para adaptarse semánticamente a cualquier tipo de código. Hay situaciones en las que hacer un from es más idiomático que un of y viceversa, pero ambas son intercambiables.
Por tanto:
Adicionalmente puedes usar simplemente el constructor Maybe
para obtener el mismo resultado que usar of o from.
Importante:
Just
es la factoría que usamos cuando sabemos que nuestro valor no es nullable, ya que espera recibir un valor distinto de null/undefined. Sin embargo, si hacemosJust(null)
acabaremos con un null dentro de un Maybe y puede ser contraproducente.Además, ya que como los tipos no primitivos en Javascript pasan la referencia a estos en las funciones, si hacemos por ejemplo
Maybe.Just(myObject)
, cualquier referencia amyObject
podrá cambiar el objeto que hay dentro del Maybe, rompiendo de alguna forma con la encapsulación, ya que el API no realiza copias de tipos no primitivos por temas de performance.
Recuerda que Just
y Nothing
son constructores que devuelven siempre el mismo tipo: Maybe
.
Transformando los Maybes
Vale, ya hemos visto como poder meter valores dentro del Maybe (recuerda que un Maybe es como una caja), ahora necesitamos una forma de poder aplicar transformaciones sobre el tipo que contiene. Como Maybe es un Functor, podemos usar map
:
map :: (a → b) → Maybe b
Suponiendo que tengamos un Maybe de un tipo a, map recibe una función que toma un valor del tipo a (el que hay dentro del Maybe) y devuelve un tipo b. Finalmente map devuelve un nuevo Maybe con el tipo resultante de la función de transformación, osea b.
Recuerda que a y b pueden ser el mismo tipo. Así cualquiera de estas funciones de ejemplo sería válida para map:
(Number → Number)
,(String → Number)
, etc.
A continuación muestro un ejemplo:
Qué ocurre si hacemos un map sobre un Maybe que no tiene nada — esto es, que sea Nothing
— ? Pues que el map devuelve Maybe.Nothing
y no aplica la transformación (ya que no hay valor que transformar).
Adicionalmente maybe-monada viene con una segunda versión de map que acepta una función con arity 2, osea con dos argumentos. Esta es map2:
map2 :: Maybe b → (a → b → c) → Maybe c
Es decir, tengo dos cajas con valores y quiero aplicarle una función que toma esos dos valores, los transforma y devuelve un único Maybe:
Si quieres saber más a cerca de los Functors, te recomiendo que el eches un vistazo a este artículo donde profundizo sobre ellos.
Cajas dentro de cajas
Hemos visto como aplicar transformaciones de un tipo a a
un tipo b
que se encuentre dentro del Maybe, y hemos dicho que a y b pueden ser cualquier tipo, incluso otro Maybe. Veamos por ejemplo el siguiente código:
Recordemos que la anotación del map era:
map :: (a → b) → Maybe b
Y la de la función isAdult sería:
isAdult :: Number → Maybe Number
Cumple la anotación del map ya que Number → Maybe Number
entra dentro de la función a → b
(recuerda que a y b son cualquier tipo), por lo que acabariamos con esta anotación:
map :: (Number → Maybe Number) → Maybe (Maybe Number)
Solo he sustituido a por
Number
y b porMaybe Number
, que son los tipos de isAdult
Es decir, que el resultado de Maybe.of(17).map(isAdult)
será Maybe(Maybe)
. Osea, una caja dentro de otra caja.
Aplanando niveles
Y es que el transformar valores y generar nuevos maybes en el proceso es algo bastante común y por supuesto maybe-monada nos ofrece una forma de lidiar con este problema. Demos la bienvenida a andThen
o chain
:
andThen :: (a → Maybe b) → Maybe b
chain :: (a → Maybe b) → Maybe b
Ambas definiciones son intercambiables. En algunos códigos será más semántico utilizar uno frente al otro.
Su funcionamiento es exactamente igual que map, con la peculiaridad que espera que la función transformadora devuelva un Maybe
y el resultado de todo será un único Maybe, eliminando el anidamiento que obteniamos con map.
El usar map o chain va a depender de lo que devuelva la función transformadora. Mira siempre con atención las anotaciones que puedes encontrar en la documentación y localiza el método que mejor casa con tus necesidades.
Así, recuperando nuestro ejemplo:
Este método es el que hace que el Maybe sea una mónada.
Produciendo disyuntivas
Una vez que tenemos nuestro valor dentro de un Maybe y somos capaces de transformarlo usando map o chain, a veces es necesario limitar de alguna forma el dominio del valor. Esto es, aplicar algún predicado para producir solo Just de los valores que cumplan dicho predicado.
Para ello maybe-monada tiene la utilidad filter que precisamente nos ofrece eso. La anotación es la siguiente:
filter :: (a → Boolean) → Maybe a
De tal forma que si el valor de dentro satisface la función predicado devolverá un
Just
con el valor, en caso contrario obtendremosNothing
.
Veamos un ejemplo:
Liberando valores
Llega el momento, hemos hecho un buen puñado de transformaciones sobre nuestros valores contenidos en cajas y ahora los queremos de vuelta. La primera pregunta que es importante hacernos es: Es realmente necesario? Es decir, para que quiero el valor? Es posible que solo necesitemos el valor para pasarselo a otro proceso, en cuyo caso es mejor seguir usando el maybe y simplemente usar map como vimos arriba.
Por el contrario puede que queramos el valor para realizar algún tipo de side-effect sobre el cual no nos importe demasiado lo que este devuelva — por ejemplo, mostrar el valor por pantalla o por consola —. En este caso podemos volver a recurrir a map:
Maybe.of(2).map(x => x * 2).map(value => console.log(value))
Si no te queda más remedio que sacar el valor del Maybe, maybe-monada te ofrece varias posibilidades:
La primera es hacer uso del método unwrap. Sin embarbo, maybe-monada aún intenta protegerte del posible nulo que haya en su interior, y si este es el caso el método uwrap puede lanzar una excepción del tipo
UnwrapException.
Así que preparate para envolver la llamada en un try/catch.La otra opción es volverte el responsable de proporcionar un valor por defecto en el caso de que estemos ante un Maybe.Nothing. Esto puedes conseguirlo con la función unwrapOr que recibe un parametro de cualquier tipo:
Maybe.of(null).unwrapOr(“Nada”) // “Nada”
Esta función no establece ningún control si como parámetro pasas un nullable, por lo que úsalo bajo tu responsabilidad.Hay una variante muy práctica de unwrapOr que es unwrapOrElse que en lugar de recibir un valor, recibe una función. De esta forma, el valor por defecto se computa solo en el caso en que el maybe sea
Nothing
. Por eso decimos que este método es lazily evalutated.
Funciones dentro de Maybe
Hemos visto como podemos colocar cualquier valor dentro de un Maybe. Como las funciones también son consideradas valores — como puedes aprender aquí — podemos también meterlas dentro de nuestra caja Maybe:
const logger = Maybe.of(window.logger)
Imaginémos que
window.logger
puede ser una función que toma un valor y lo imprime por consola. O podría serundefined
, por eso lo metemos dentro del Maybe para abstraenos de los posibles nullables.
Ahora podríamos usar map para poder aplicar esa función en el caso de que exista realmente:
logger.map(l => l(“Hello world“));
Recuerda que
logger
es un Maybe y que map solo ejecuta la función sobre el valor en el caso en el que este no se Nothing. Por el contrario genera un Nothing.
Subiendo funciones a la categoría de Maybe
Pongamos que tenemos una funcion que puede funcionar con cualquier tipo de valor:
const double = n => n * 2;
De tal forma que toma un Number
y devuelve otro Number
, siendo este el doble del numero introducido.
Pero que ocurre si quiero usar esa función pero en lugar de un tipo Number
tengo un Maybe Number
, cómo podría usar esa función con este tipo?. La primera forma es sencilla, con map:
const n = Maybe.of(2);
const res = n.map(double);
res.uwrap(); // 4
La segunda es enseñar a mi función double a trabajar con el tipo Maybe — esto es, subirlo de categoría —.
Para que una función pueda operar en la categoría de Maybe, tiene que de alguna forma pertenecer a esa categoría. Para ello basta con colocar la función dentro de un Maybe:
const doubleMaybe = Maybe.of(double)
Desde este momento es como si nuestra función double dijiese: Ahhh, estando aquí dentro ya entiendo como trabajar con estos tipos.
Por tanto usando el método ap podríamos aplicar la función dentro de un Maybe a otro Maybe:
ap :: Maybe a → Maybe b
Suponiendo que tengamos una función dentro de un Maybe (a → b)
, ap toma un tipo Maybe y devuelve otro tipo Maybe. Así podriamos aplicar nuestro doubleMaybe
a otro Maybe, resolviendo el problema que teniamos antes de no poder pasarle un Maybe Number
a double
:
Así hemos enseñado a nuestra función a trabajar con Maybes.
Con ap es como se pueden construir utilidades para “elevar” cualquier función de no importa que arity, a la categoría de Maybe:
Nos hemos creado dos utilidades para subir a Maybe dos funciones de uno y dos parámetros respectivamente y ahora podemos enseñar a cualquier función a trabajar con maybes.
lift
ylift2
aunque pueden ser facilmente construidas, ya vienen empaquetadas con maybe-monada. De tal forma que tienesMaybe.lift
yMaybe.lift2
a tu disposición.Con subir funciones hasta arity 2 tendrás más que suficiente, ya que en general tener funciones de más de 3 parámentros suele ser considerada una mala práctica.
Agrupando maybes
Hay veces que tenemos un par de Maybes que de alguna forma están relacionados y nos gustaría poder unirlos en un solo maybe para simplificar los cálculos. Para eso podemos recurrir a zip:
zip :: Maybe a → Maybe b → Maybe([a,b])
El primera maybe (Maybe a) hace referencia al Maybe ejecutor. Es decir si tengo
maybe1.zip(maybe2)
, elMaybe a
corresponde a maybe1.
Básicamente toma dos maybes y mete sus contenidos (en caso de que haya) en una tupla (un array de dos elementos). Finalmente devuelve un único Maybe con esta tupla en su interior. Si cualquiera de los maybes pasados es un Nothing, zip devolverá Nothing:
No obstante, muchas veces el zippeo tiene indole temporal, es decir que solo queremos unirlos bajo una misma tupla para aplicarles una función. En este caso tenemos la variante zipWith
:
zipWith :: Maybe a → Maybe b → ([a,b] → c) → Maybe c
Toma dos maybes y una función que recibe una tupla y devuelve un valor. Finalmente devuelve un nuevo Maybe con el resultado de aplicar la función:
Concluyendo
El API no termina aquí, hemos introducido algunas otras utilidades para haceros la vida más sencilla cuando trabajeís con maybe-monada y puedes verlas todas en la documentación oficial.
Además como maybe-monada está implementada siguiendo las especificaciones de Functor y Monad, puede ser usada con librerías como ramda.
Espero que este artículo práctico te haya resultado revelador y que te haya animado a usar los Maybes dentro de tu código.