scala nested option scala-cats

Evitar cascadas de opción anidadas profundamente en Scala



nested option (4)

Supongamos que tengo tres funciones de acceso a la base de datos foo , bar y baz que pueden devolver la Option[A] donde A es una clase de modelo, y las llamadas dependen unas de otras.

Me gustaría llamar a las funciones secuencialmente y en cada caso, devolver un mensaje de error apropiado si el valor no se encuentra ( None ).

Mi código actual se ve así:

Input is a URL: /x/:xID/y/:yID/z/:zID foo(xID) match { case None => Left(s"$xID is not a valid id") case Some(x) => bar(yID) match { case None => Left(s"$yID is not a valid id") case Some(y) => baz(zID) match { case None => Left(s"$zID is not a valid id") case Some(z) => Right(process(x, y, z)) } } }

Como se puede ver, el código está mal anidado.

Si, en cambio, uso una for comprensión, no puedo dar mensajes de error específicos, porque no sé qué paso falló:

(for { x <- foo(xID) y <- bar(yID) z <- baz(zID) } yield { Right(process(x, y, z)) }).getOrElse(Left("One of the IDs was invalid, but we do not know which one"))

Si uso map y getOrElse , termino con el código casi tan anidado como el primer ejemplo.

¿Es esta una mejor forma de estructurar esto para evitar el anidamiento al tiempo que permite mensajes de error específicos?


En lugar de usar una Option , usaría un Try . De esta forma, tiene la composición Monadic que le gustaría mezclar con la capacidad de retener el error.

def myDBAccess(..args..) = thingThatDoesStuff(args) match{ case Some(x) => Success(x) case None => Failure(new IdError(args)) }

Asumo en lo anterior que no controlas las funciones y no puedes refactorizarlas para darte una Option no sea Option . Si lo hiciste, simplemente sustituye Try .


Puede hacer funcionar su loop for usando proyecciones correctas.

def ckErr[A](id: String, f: String => Option[A]) = (f(id) match { case None => Left(s"$id is not a valid id") case Some(a) => Right(a) }).right for { x <- ckErr(xID, foo) y <- ckErr(yID, bar) z <- ckErr(zID, baz) } yield process(x,y,z)

Esto todavía es un poco torpe, pero tiene la ventaja de ser parte de la biblioteca estándar.

Las excepciones son otro camino por recorrer, pero desaceleran mucho las cosas si los casos de falla son comunes. Solo usaría eso si el fracaso fuera realmente excepcional.

También es posible utilizar devoluciones no locales, pero es un poco incómodo para esta configuración en particular. Creo que las proyecciones correctas de Either son el camino a seguir. Si realmente te gusta trabajar de esta manera pero no .right gusta poner " .right todos lados, hay varios lugares en los que puedes encontrar un "O bien polarizado" que actuará como la proyección correcta por defecto (por ejemplo, ScalaUtils, Scalaz, etc.).


Se me ocurrió esta solución (basada en la solución de @ Rex y sus comentarios):

def ifTrue[A](boolean: Boolean)(isFalse: => A): RightProjection[A, Unit.type] = Either.cond(boolean, Unit, isFalse).right def none[A](option: Option[_])(isSome: => A): RightProjection[A, Unit.type] = Either.cond(option.isEmpty, Unit, isSome).right def some[A, B](option: Option[A])(ifNone: => B): RightProjection[B, A] = option.toRight(ifNone).right

Ellos hacen lo siguiente:

  • ifTrue se usa cuando una función devuelve un Boolean , siendo true el caso de "éxito" (por ejemplo: isAllowed(userId) ). De hecho, devuelve Unit por lo que debe usarse como _ <- ifTrue(...) { error } en a for comprensión.
  • none se usa cuando una función devuelve una Option con None como el caso de "éxito" (por ejemplo: findUser(email) para crear cuentas con direcciones de correo electrónico únicas). En realidad, devuelve Unit por lo que debe usarse como _ <- none(...) { error } en a for comprensión.
  • some se usan cuando una función devuelve una Option con Some() siendo el caso de "éxito" (por ejemplo: findUser(userId) para un GET /users/userId ). Devuelve el contenido de Some : user <- some(findUser(userId)) { s"user $userId not found" } .

Se usan en a for comprensión:

for { x <- some(foo(xID)) { s"$xID is not a valid id" } y <- some(bar(yID)) { s"$yID is not a valid id" } z <- some(baz(zID)) { s"$zID is not a valid id" } } yield { process(x, y, z) }

Esto devuelve un Either[String, X] donde la String es un mensaje de error y la X es el resultado del process de llamada.


Sé que esta pregunta fue respondida hace un tiempo, pero quería dar una alternativa a la respuesta aceptada.

Dado que, en su ejemplo, las tres Option son independientes, puede tratarlas como Funcionadores Aplicativos y usar ValidatedNel de Gatos para simplificar y agregar el manejo de la ruta infeliz.

Dado el código:

import cats.data.Validated.{invalidNel, valid} def checkOption[B, T](t : Option[T])(ifNone : => B) : ValidatedNel[B, T] = t match { case None => invalidNel(ifNone) case Some(x) => valid(x) def processUnwrappedData(a : Int, b : String, c : Boolean) : String = ??? val o1 : Option[Int] = ??? val o2 : Option[String] = ??? val o3 : Option[Boolean] = ???

A continuación, puede replicar para obtener lo que desea con:

//import cats.syntax.cartesian._ ( checkOption(o1)(s"First option is not None") |@| checkOption(o2)(s"Second option is not None") |@| checkOption(o3)(s"Third option is not None") ) map (processUnwrappedData)

Este enfoque le permitirá agregar fallas, lo que no fue posible en su solución (dado que el uso de las comprensiones impone la evaluación secuencial). Se pueden encontrar más ejemplos y documentación aquí y aquí .

Finalmente, esta solución usa los gatos Validated pero podría traducirse fácilmente a la Validation Scalaz