Cet article est en cours de rédaction, son contenu peut évoluer sans préavis et les informations qu'il contient peuvent manquer de précisions.
Un des types intéressants dans ZIO, qui est un peu caché sous le capôt, c’est zio.Cause.
ZIO[-R, +E, +A]
indique de manière succincte que l’on peut gérer les erreurs avec E, qui va représenter soit un échec (ZIO.fail) de type E, ou les défaults (ZIO.die) de type Throwable. On va avoir aussi les Fatal Errors où l’on peut réagir un peu avant que cela tue l’application.
ZIO accumule les erreurs de manière parallèle et séquentielle, et permet de faire de la récupération sur les différentes erreurs (faillure/defect).
Le type intéressant qui permet de faire ça c’est zio.Cause[+E]. Cause est un type de data algébrique, qui facilite la représentation de la composition des erreurs :
sealed trait Cause[+E]
case object Empty extends Cause[Nothing]
case class Fail[+E](value : E, trace : zio.StackTrace) extends Cause[E]
case class Die(value : Throwable, trace : zio.StackTrace) extends Cause[Nothing]
case class Interrupt(fiberId : zio.FiberId, trace : zio.StackTrace) extends Cause[Nothing]
case class Stackless[+E](cause : Cause[E], stackless : Boolean) extends Cause[E]
case class Then[+E](left : Cause[E], right : Cause[E]) extends Cause[E]
case class Both[+E](left : Cause[E], right : Cause[E]) extends Cause[E]
API
trait ZIO[-R, +E, +A] {
def sandbox: ZIO[R, Cause[E], A]
def tapErrorCause[R1 <: R, E1 >: E](f: Cause[E] => ZIO[R1, E1, Unit]): ZIO[R1, E1, A]
def catchAllCause[R1 <: R, E2, A1 >: A](h : Cause[E] => ZIO[R1, E2, A1]]: ZIO[R1, E2, A1]
def catchSomeCause[R1 <: R, E1 >: E, A1 >: A](pf : Cause[E] => ZIO[R1, E1, A1]): ZIO[R1, E1, A1]
def cause: URIO[R, Cause[E]] //pour un A, on va avoir un Cause.Empty
def foldCause[B](failure: Cause[E] => B, success: A => B): URIO[R, B]
def foldCauseZIO[R1 <: R, E2, B](failure : Cause[E] => ZIO[R1, E2, B], success: A => ZIO[R1, E2, B]): ZIO[R1, E2, B]
def mapErrorCause[E2](h : Cause[E] => Cause[E2]): ZIO[R, E2, A]
def onDoneCause[R1 <: R](error: Cause[E] => zio.ZIO[R1, Nothing, Any], success: A => zio.ZIO[R1, Nothing, Any]): ZIO[R1, Nothing, Unit]
}
on[R, E, A](zio: ZIO[R, Cause[E], A] {
def unsandbox: ZIO[R, E, A]
}
on[R, E](zio: ZIO[R, E, Cause[E]) {
def uncause: ZIO[R, E, Unit]
}
object ZIO {
def failCause[E](cause : => Cause[E]): IO[E, Nothing]
def fromEitherCause[E, A](v : => Either[Cause[E], A]): IO[E, A]
def logCause(cause: => Cause[Any]) : zio.UIO[scala.Unit]
def logCause(message: => String, cause : => Cause[Any]): UIO[scala.Unit]
def logDebugCause(message: => String, cause : => Cause[scala.Any]): zio.UIO[scala.Unit]
def logDebugCause(cause: => Cause[scala.Any]): UIO[scala.Unit]
def logErrorCause(message: => String, cause : => Cause[scala.Any]): zio.UIO[scala.Unit]
def logErrorCause(cause: => Cause[scala.Any]): zio.UIO[scala.Unit]
def logFatalCause(message: => String, cause : => zio.Cause[scala.Any]): zio.UIO[scala.Unit]
def logFatalCause(cause: => Cause[scala.Any]): zio.UIO[scala.Unit]
def logInfoCause(message: => String, cause : => zio.Cause[scala.Any]): zio.UIO[scala.Unit] =
def logInfoCause(cause: => zio.Cause[scala.Any]): UIO[Unit]
def logTraceCause(message: => String, cause : => zio.Cause[scala.Any]): zio.UIO[Unit]
def logTraceCause(cause: => zio.Cause[scala.Any]): zio.UIO[Unit]
def logWarningCause(message: => String, cause : => zio.Cause[scala.Any]): UIO[scala.Unit]
def logWarningCause(cause: => Cause[scala.Any]): zio.UIO[scala.Unit]
def refailCause[E](cause : Cause[E]): zio.ZIO[Any, E, Nothing]
def unsandbox[R, E, A](v : => ZIO[R, Cause[E], A]) : ZIO[R, E, A]
}
Cas d’utilisation
Comprendre ce qui se passe quand tout se déroule mal
Parfois, on ne comprend pas pourquoi un effet est en échec, cause permet de récupérer de quoi tout expliquer s’il n’y a pas d’autres moyens
effect.tapErrorCause(cause => ZIO.logCause("please help me understand", cause))
Récupérer les erreurs des exécutions parallèles
Composer d’autres façons de lancer les effets
un des problèmes que l’on a récemment sur un pipeline de gestion de notification en batch, c’est l’envoi simultané de SMS et d’EMAIL. L’utilisation de zipPar pour faire ça n’est pas pratique. Cela tente d’interrompre l’effet restant en cas d’erreur d’un côté. On peut éviter ce comportement au niveau de la composition soit :
- en rendant certains effets non-interruptibles (
.uninterruptible
) - soit avec un nouveau combinateur
def zipParNoInterruptOnError[R, E, A, B](zio1:ZIO[R, E, A], zio2:ZIO[R, E, B]): ZIO[R, E, (A, B)] = { zio1.sandbox.either.zipPar(zio2.sandbox.either).map({ case (Right(a), Right(b)) => Right((a,b)) case (Left(e1), Left(e2)) => Left(Cause.Both(e1, e2)) case (Left(e1), _) => Left(e1) case (_, Left(e2)) => Left(e2) }).absolve.unsandbox }
L’avantage de ce type de combinateur, ce n'est que cela permet toujours d’interrompre plus tard la composition, et de récupérer plusieurs erreurs s’il y en a plusieurs
Conclusion
C’est intéressant que ZIO nous permette sans rentrer des structures interne de contrôler plus finement la gestion d’erreur.
Si vous voulez aller plus loin, récemment Wiem a fait un talk sur le sujet : https://www.youtube.com/watch?v=CnXR7RPMAs8