Empecé a trabajar con Scala hace unos meses. Uno de los conceptos que más me costó entender es la Either
mónada. Entonces, decidí jugar con él y comprender mejor su poder.
En esta historia, comparto lo que he aprendido, con la esperanza de ayudar a los programadores a abordar este hermoso idioma.
La mónada Either
Either
es una de las mónadas más útiles de Scala. Si te estás preguntando qué es una mónada, bueno… ¡No puedo entrar en detalles aquí, tal vez en una historia futura!
Imagínese Either
como una caja que contiene un cálculo. Trabaja dentro de esta caja, hasta que decide sacar el resultado de ella.
En este caso específico, nuestra Either
caja puede tener dos “formas”. Puede ser (ya sea) a Left
o a Right
, dependiendo del resultado del cálculo dentro de él.
Puedo oírte preguntar: "Está bien, ¿y para qué es útil?"
La respuesta habitual es: manejo de errores.
Podemos poner un cálculo en el Either
, y convertirlo en un Left
en caso de errores, o que Right
contenga un resultado en caso de éxito. El uso de Left
para errores y Right
para el éxito es una convención. ¡Entendamos esto con algo de código!
En este fragmento solo estamos definiendo una Either
variable.
Podemos definirlo como que Right
contiene un valor válido o como que Left
contiene un error. También tenemos un cálculo que devuelve an Either
, lo que significa que puede ser a Left
o a Right
. Simple, ¿no es así?
Proyección derecha e izquierda
Una vez que tengamos el cálculo en el cuadro, es posible que deseemos obtener el valor. Estoy seguro de que espera llamar .get
a Either
y extraer su resultado.
Eso no es tan simple.
Piénselo: puso su cálculo en Either
, pero no sabe si resultó en a Left
o a Right
. Entonces, ¿qué debería .get
devolver una llamada? ¿El error o el valor?
Por eso, para obtener el resultado, debe hacer una suposición sobre el resultado del cálculo.
Aquí es donde entra en juego la proyección .
A partir de un Either
, puede obtener un RightProjection
o un LeftProjection
. Lo primero significa que asume que el cálculo resultó en a Right
, el segundo en a Left
.
Lo sé, lo sé ... esto puede ser un poco confuso. Es mejor entenderlo con algo de código. Después de todo, el código siempre dice la verdad .
Eso es. Tenga en cuenta que cuando intenta obtener el resultado de a RightProjection
, pero es a Left
, obtiene una excepción. Lo mismo ocurre con a LeftProjection
y tienes un Right
.
Lo bueno es que puedes mapear en proyecciones. Esto significa que puede decir: "suponga que es un derecho: haga esto con él", Left
sin modificar (y al revés).
De la opción a cualquiera
Option
es otra forma habitual de tratar los valores no válidos.
An Option
puede tener un valor o estar vacío (su valor es Nothing
). Apuesto a que notó una similitud con Either
… ¡Es incluso mejor, porque en realidad podemos transformar un Option
en un Either
! ¡Hora del código!
Es posible transformar un en Option
a Left
o a Right
. El lado resultante de Either
contendrá el valor de Option
si está definido. Frio. Espera un minuto ... ¿Qué pasa si Option
está vacío? Obtenemos el otro lado, pero necesitamos especificar lo que esperamos encontrar en él.
De adentro hacia afuera
Either
es magia, todos estamos de acuerdo en eso. Así que decidimos usarlo para nuestros cálculos inciertos. Un escenario típico cuando se hace programación funcional es el mapeo de una función en un List
de elementos, o en un Map
. Hagámoslo con nuestra nueva Either
computación con nueva potencia ...
Huston, tenemos un "problema" (bueno, no es un problema GRANDE, pero es un poco incómodo). Sería mejor tener la colección dentro Either
que muchos Either
dentro de la colección. Podemos trabajar en eso.
Lista
Empecemos por List
. Primero razonamos al respecto, luego podemos jugar con el código.
Tenemos que extraer el valor de Either
, ponerlo en List
y poner la lista dentro de un Either
. Bien, me gusta.
El punto es que podemos tener a Left
o a Right
, por lo que debemos manejar ambos casos. Hasta que encontremos un Right
, podemos poner su valor dentro de un nuevo List
. Procedemos de esta manera acumulando cada valor en el nuevo List
.
Eventualmente llegaremos al final del List
de Either
, lo que significa que tenemos un nuevo que List
contiene todos los valores. Podemos empacarlo Right
y listo. Este fue el caso en el que nuestro cálculo no devolvió un Error
interior a Left
.
Si esto sucede, significa que algo salió mal en nuestro cálculo, por lo que podemos devolver el Left
con el Error
. Tenemos la lógica, ahora necesitamos el código.
Mapa
El trabajo Map
es bastante simple una vez que hemos hecho los deberes para List
(a pesar de tener que hacerlo genérico):
- Paso uno: transformar el
Map
en unaList
deEither
contiene la tupla (clave, valor). - Paso dos: pasa el resultado a la función que definimos
List
. - Paso tres: transforma el
List
de tuplas dentro delEither
en aMap
.
Pan comido.
Seamos elegantes: un útil convertidor implícito
We introduced Either
and understood it is useful for error handling. We played a bit with projections. We saw how to pass from an Option
to an Either
. We also implemented some useful functions to “extract” Either
from List
and Map
. So far so good.
I would like to conclude our journey in the Either
monad going a little bit further. The utility functions we defined do their jobs, but I feel like something is missing…
It would be amazing to do our conversion directly on the collection. We would have something like myList.toEitherList
or myMap.toEitherMap
. More or less like what we do with Option.toRight
or Option.toLeft
.
Good news: we can do it using implicit classes!
Using implicit classes in Scala lets us extend the capabilities of another class.
In our case, we extend the capability of List
and Map
to automagically “extract” the Either
. The implementation of the conversion is the same we defined before. The only difference is that now we make it generic. Isn’t Scala awesome?
Since this can be a useful utility class, I prepared for you a gist you can copy and paste with ease.
object EitherConverter { implicit class EitherList[E, A](le: List[Either[E, A]]){ def toEitherList: Either[E, List[A]] = { def helper(list: List[Either[E, A]], acc: List[A]): Either[E, List[A]] = list match { case Nil => Right(acc) case x::xs => x match { case Left(e) => Left(e) case Right(v) => helper(xs, acc :+ v) } } helper(le, Nil) } } implicit class EitherMap[K, V, E](me: Map[K, Either[E, V]]) { def toEitherMap: Either[E, Map[K, V]] = me.map{ case (k, Right(v)) => Right(k, v) case (_, e) => e }.toList.toEitherList.map(l => l.asInstanceOf[List[(K, V)]].toMap) } }
Conclusion
That’s all folks. I hope this short story may help you to better understand the Either
monad.
Please note that my implementation is quite simple. I bet there are more complex and elegant ways to do the same thing. I’m a newbie in Scala and I like to KISS, so I prefer readability over (elegant) complexity.
If you have a better solution, especially for the utility class, I will be happy to see it and learn something new! :-)