El poder de Either en Scala

Hace tiempo escribí acerca de la versatilidad que nos da el usar Option en Scala, cuando se puede manejar un valor que podría ser nulo.

Esto es muy útil por ejemplo para un método de login: pasamos usuario y password, y obtenemos un Usuario, siempre y cuando exista el usuario con ese nombre y su password sea correcto. Entonces podemos implementar el método de estar forma:

def login(username:String, password:String):Option[Usuario]={
  val user = //Buscamos el usuario en la base de datos
  if (user != null && passwordEsValido) Some(user)
  else None
}

Entonces cuando invocamos el método login, ya no tenemos que validar contra null en un if para proceder de una forma, y presentar un error en el else; en vez de eso podemos hacer distintas funciones:

def continuar(user:Usuario)=//esto lo invocaremos si el login estuvo OK
def error()=//Esto lo invocaremos si el login falla

//Aquí viene lo bonito de usar Option:
//Esta línea de código expresa muy claramente lo que hay que hacer
login("username", "password") map continuar orElse error

Casos más sofisticados

Supongamos que queremos manejar errores de manera más específica. Internamente, el login pudo fallar por distintas razones: el usuario no existe, o el password es inválido, o el status del usuario es inactivo, ha sido bloqueado por alguna razón, etc.

Tal vez hacia afuera, en una aplicación web por ejemplo, solamente queremos presentar condiciones de éxito o error, y es suficiente con un Option, pero si internamente queremos tomar distintas acciones dependiendo del error que haya ocurrido, entonces usar un Option ya no es suficiente. Afortunadamente, tenemos otra clase similar, llamada Either, la cual puede contener dos objetos completamente distintos. Entonces supongamos que implementamos una clase ErrorLogin que indica la razón por la que falló, y reimplementamos nuestro método así:

def login(username:String, password:String):Either[Usuario,LoginError]={
  val user = //Buscamos el usuario en la base de datos
  if (user == null) {
    Right(LoginError(UsuarioNoExiste))
  } else if (!passwordEsValido) {
    Right(LoginError(PasswordInvalido))
  } else if (user.status != 1) {
    Right(LoginError(StatusInvalido, "Status es " + user.status))
  } else {
    Left(user)
  }
}

Either es una clase abstracta, igual que Option. Y así como Option tiene dos encarnaciones, Some y None, Either tiene también dos versiones concretas: Left y Right.

Todo esto sirve para expresar, en este caso, que el método login puede devolver un Usuario, o un LoginError. Y entonces podemos actuar de distintas formas dependiendo del resultado del login. Una forma muy simple sería así:

//Podemos reimplementar el método error para que ahora reciba el error
//El método continuar no sufre cambios
def error(err:LoginError)=//Manejar el error

def login = login("usuario", "password")
if (login.isLeft) {
  //El login salió OK
  continuar(login.left.get)
} else error(login.right.get)

Pero ese código no aprovecha realmente las ventajas de tener un Either. Así como con Option podemos usar map, orElse, getOrElse, etc, podemos también aprovechar algunas funciones que implementa Either para expresar de manera más clara nuestro propósito. En este caso, hay un método llamado fold que recibe dos funciones: la primera función debe recibir un parámetro del tipo especificado para el Left y la segunda función debe recibir un parámetro del tipo especificado para el Right. Solamente una de las funciones será invocada, dependiendo del contenido real del Either. La única restricción es que ambas funciones deben tener el mismo tipo de retorno. En este caso, nuestroas métodos continuar y error podrían devolver un URL por ejemplo, para saber a dónde redirigir al usuario según el resultado del login:

def continuar(user:Usuario):URL=//blabla
def error(err:LoginError)=//blabla

val login = login("usuario", "password")
val url = login.fold(continuar, error)

Aquí estamos aprovechando la característica call-by-name que nos permite simplemente pasar el nombre de un método o función como parámetro; dado que el método continuar recibe un Usurio y el método error recibe un LoginError, podemos usar únicamente sus nombres como parámetros; es importante recordar que en esa última línea no estamos invocando a ninguno de los dos métodos, sino que se los pasamos a fold y ahí se ejecutará uno de ellos nada más.

Una vez que se comprende bien Either, se puede utilizar en este tipo de métodos que pueden devolver dos valores distintos dependiendo del éxito o fracaso de una operación, en donde un Option no es lo suficientemente expresivo porque solamente puede contener un valor o no contener nada.

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de bferro

Otros ejemplos sencillos de Either

Contribuyo al post de Enrique con el siguiente ejemplo de Either disponible en varios lugares:
Definimos una función que al ejecutar un bloque de código devolverá Right (result) si el bloque se ejecuta con éxito o Left(throwable) si el bloque dispara un Throwable.

def throwableToLeft[T](block: => T): Either[java.lang.Throwable, T] =
  try {
    Right(block)
  } catch {
    case ex => Left(ex)
}

Usamos ahora la función para ambos casos: que el bloque se ejecute con éxito o que falle disparando un Throwable:

var s = "hello"
throwableToLeft { s.toUpperCase } match {
  case Right(s) => println(s)
  case Left(e) => e.printStackTrace
}
// prints "HELLO"

s = null
throwableToLeft { s.toUpperCase } match {
  case Right(s) => println(s)
  case Left(e) => e.printStackTrace
}
// prints NullPointerException stack trace

Left y Right son clases cases, definidas en el mismo archivo que la clase Either que es selllada (sealed) para poder determinar todos sus posibles cases.

Imagen de ezamudio

cierto

También se puede hacer pattern matching con Right y Left. Y de hecho la convención es alrevés de mi ejemplo: Left suele usarse para devolver un error y Right para el resultado exitoso. Será que soy zurdo, y me pasa como cuando quiero acomodar los cubiertos en una mesa y a veces los pongo bien, a veces alrevés porque le pienso demasiado y hago memoria de cómo es para los diestros y lo quiero invertir pero a veces me acuerdo mal, etc.

Imagen de bferro

Yo también soy zurdo

Pues ya somos dos, y que recuerden los diestros, que la chingonería está de parte nuestra por el trabajo que nos hacen pasar en este mundo de derechos.

Pues ya somos muchos zurdos

Pues ya somos muchos zurdos no? Yo hasta la fecha no puedo decir sin tomar una llave ( o grifo ) para donde debe de girar, me pasa que a veces estoy apretando una tuerca cuando quiero zafarla. En fin, había estado queriendo leer este artículo y ahora tengo una razón más

¿Habrá otra forma de no tener

¿Habrá otra forma de no tener que preguntar si es left o right? Una cosa que entendi y me agradó de Option es que evitar tener que preguntar si es Some or None y simplemente se usa el valor y ya sabiendo que no va a fallar ( meeeh.. algo similar al NullObjectPattern en OO , pero.. no .. ) y entonces, se puede escapar de NullLand ( para más sobre Option, leer las respuestas de aquí y acá y claro acá). Hasta ahí todo bien.

Acá con Either yo esperaba que fuera algo similar, y de hecho lo es, pero en los ejemplos puestos se muestra o pattern matching o un if, debe de haber una forma de no hacer esto y dejar que el código fluya dependiendo de los dos valores, como con los encadenamientos map, flatMap, orElse o for comprehensions.

Lo que es muy valioso es poder expresar un valor como uno de dos posibles valores, ahí ya es seguro que una cosa es Either left or right :)

Imagen de bferro

El uso de Either es "limitado"

Scala define la clase Either como sigue:
Un objeto de clase Either respresenta un valor de uno de dos tipos posibles para implementar la operación de "union disjunta", que no es otra cosa que la operación de unión modificada para indexar elementos de conjuntos disjuntos.
La clase Either es extendida por dos clases case: Left y Right que se usan como constructores datos y representan los dos valores posibles de un objeto Either.
El caso de uso típico de Either es una alternativa a la clase Option y por convención Left debe representar un fallo y Right debe representar algo similar a Some.
De las cosas que he leído sobre Either, hay algo en común y es que casi todo el mundo coincide en que su uso básico es el de los ejemplos que aquí se han descrito, usando por supuesto las ventajas de pattern matching para discriminar un resultado exitoso de un resultado fallido, y sobre esa base tomar acciones. Para ese propósito es muy útil.
De los libros que he consultado de Scala, solamente uno hace mención de la clase Either y el ejemplo es similar.

Imagen de ezamudio

sin if

Oscar, lee el articulo completo. Al final muestro la manera de usarlo sin if, similar a como usas map/flatMap con Option, usas fold con el Either.

Y para apretar/aflojar tuercas y tornillos, abrir y cerrar llaves, piensa en el sentido de las manecillas del reloj, tu que todavia conociste los relojes analogicos con manecillas. En el sentido de las manecillas, es cerrar/apretar y en el sentido opuesto es abrir/aflojar.

Imagen de bferro

Cuidado con los tanques de gas LP @OscarRyz

Pero no vayas a seguir el consejo de Enrique con algunos contenedores de gas LP, pues ellos tienen cuerda inversa o rosca izquierda como se dice en casi todos los países de habla hispana.

Imagen de Shadonwk

jeje presisamente, leí el

jeje presisamente, leí el comentario de Enrique y me acorde del contenedor de gas, intente aplicar su consejo, pero no cuadra, pues ahí es al revez, como dice el Dr. Ferro.

Saludos.