Tofu

Tofu

  • Docs
  • API

›Core

Getting Started

  • Getting started
  • Installation

Documentation

    Core

    • Error management
    • Forking and Racing
    • Timeout
    • Typeclasses

    Logging

    • Home (Start here)
    • Core concepts
    • Key Features
    • tofu.syntax.logging
    • Loggable typeclass
    • Logback Layouts

    Logging Recipes

    • Recipe list
    • The simplest form
    • Logs for a service
    • Contextual Logging
    • Logging auto derivation
    • ZIO Logging

    Contextual

    • Env
    • WithContext
    • Cats MTL interop

    Concurrent

    • Agent
    • MakeRef

    Higher-Kind

    • Mid

    Optics

    • Tofu optics

    Utilities

    • Config
    • Console
    • Memo

    Streams

    • Streams

Error management

Producing errors

Problem

One of the major issues of the MTL style is an error handling.

The weakest Cats typeclass, which enables operations with errors, is an ApplicativeError. It brings a full Applicative instance apart from error-related methods and this means, that we are not allowed to have a few FunctorRaise or ApplicativeError instances in the scope, since their underlying Functor/Applicative instances will come into conflict:

    import cats._
    case class ArithmeticError() extends Throwable
    case class ParseError() extends Throwable

    def divideBad[F[_]](x: String, y: String)(implicit 
        F1: ApplicativeError[F, ArithmeticError],
        F2: ApplicativeError[F, ParseError]): F[String] = 
        // using Functor / Applicative syntax here will cause an
        // "ambiguous implicit values" error
        ???

So we are forced to choose a single unified error type.

Solution

The simplest solution here is to create a typeclass, that is not a subtype of Functor:

trait Raise[F[_], E]{
  def raise[A](err: E): F[A]
}

(see also cats-mtl 's FunctorRaise).

It would allow us to distinguish between different types of errors:

import cats.effect.IO
import tofu._
import tofu.syntax.monadic._
import tofu.syntax.raise._

def divide[F[_]: Monad](x: String, y: String)(implicit 
    F1: Raise[F, ArithmeticError],
    F2: Raise[F, ParseError]
    ): F[String] = 
    ( x.toIntOption.orRaise(ParseError()),
      y.toIntOption.orRaise(ParseError())
       .verified(_ != 0)(ArithmeticError())
    ).mapN(_ / _).map(_.toString)

divide[IO]("10", "3").attempt.unsafeRunSync()

divide[IO]("10","0").attempt.unsafeRunSync()

divide[IO]("1", "0").attempt.unsafeRunSync()
        

Recovering from errors

Problem

ApplicativeError provides the following method for error handling:

  def handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A]

Here, if f does not fail, F[A] should describe a successful computation. The types, however, do not convey this fact, since we have no type for Unexeptional partner. Read more here

Solution

Tofu is shipped with a few typeclasses targeting the problem. The simplest one is

trait RestoreTo[F[_], G[_]] {
  def restore[A](fa: F[A]): G[Option[A]]
}

which can be used to restore from any failure condition.

Another one is

trait HandleTo[F[_], G[_], E] extends RestoreTo[F, G] {
  def handleWith[A](fa: F[A])(f: E => G[A]): G[A]

  def handle[A](fa: F[A])(f: E => A)(implicit G: Applicative[G]): G[A] =
    handleWith(fa)(e => G.pure(f(e)))

  def attempt[A](fa: F[A])(implicit F: Functor[F], G: Applicative[G]): G[Either[E, A]] =
    handle(F.map(fa)(_.asRight[E]))(_.asLeft)
}

which can handle concrete error type:

import cats._
import cats.data.EitherT
import cats.instances.vector._
import cats.syntax.foldable._
import cats.syntax.traverse._
import tofu._
import tofu.syntax.handle._
import tofu.syntax.monadic._
import tofu.syntax.raise._

def splitErrors[
  T[_]: Traverse: Alternative, 
  F[_]: Functor, G[_]: Applicative, E, A](ls: T[F[A]])(
    implicit errors: ErrorsTo[F, G, E]
): G[(T[E], T[A])] =
  ls.traverse(_.attemptTo[G, E]).map(_.partitionEither(identity))

def parseInt[F[_]: Applicative: Raise[*[_], String]](s: String): F[Int] =
  s.toIntOption.orRaise(s"could not parse $s")

type Calc[A] = EitherT[Eval, String, A]

splitErrors[Vector, Calc, Eval, String, Int](
  Vector("1", "hello", "2", "world", "3").map(parseInt[Calc])
).value

HandleTo, empowered with Raise, is called ErrorsTo:

trait ErrorsTo[F[_], G[_], E] extends Raise[F, E] with HandleTo[F, G, E]

There are also specialized versions of RestoreTo, HandleTo and ErrorsTo without To:

trait Restore[F[_]] extends RestoreTo[F, F] {
  def restoreWith[A](fa: F[A])(ra: => F[A]): F[A]
}

trait Handle[F[_], E] extends HandleTo[F, F, E] with Restore[F] {

  def tryHandleWith[A](fa: F[A])(f: E => Option[F[A]]): F[A]

  def tryHandle[A](fa: F[A])(f: E => Option[A])(implicit F: Applicative[F]): F[A] =
    tryHandleWith(fa)(e => f(e).map(F.pure))

  def handleWith[A](fa: F[A])(f: E => F[A]): F[A] =
    tryHandleWith(fa)(e => Some(f(e)))

  def recoverWith[A](fa: F[A])(pf: PartialFunction[E, F[A]]): F[A] =
    tryHandleWith(fa)(pf.lift)

  def recover[A](fa: F[A])(pf: PartialFunction[E, A])(implicit F: Applicative[F]): F[A] =
    tryHandle(fa)(pf.lift)

  def restoreWith[A](fa: F[A])(ra: => F[A]): F[A] = handleWith(fa)(_ => ra)
}


trait Errors[F[_], E] extends Raise[F, E] with Handle[F, E] with ErrorsTo[F, F, E]
← InstallationForking and Racing →
  • Producing errors
    • Problem
    • Solution
  • Recovering from errors
    • Problem
    • Solution
Tofu
Docs
Getting Started
Community
User ShowcaseStack OverflowTelegram Group (EN|RU)Gitter Group (EN)
More
BlogGitHubStar
Copyright © 2021 Tinkoff.ru