Ashkan Kh. Nazary
Ashkan Kh. Nazary

Reputation: 22334

Where to use `ApplicativeError` instead of `Either`?

There is ApplicativeError[F,E] + F[A] and there is Either[E, A]. Both convey the message that the function could fail with an E or succeed with an A but I'm not sure about the different message they convey to the client about the intended way of handling the error :

def f1[F[_]: Applicative[F]]: F[Either[E, A]]
def f2[F[_]: ApplicativeError[F,E]]: F[A]

I understand f1 means: client is responsible for error handling. But what does f2 mean to the client about how to handle the error ?

Upvotes: 5

Views: 1206

Answers (1)

Mateusz Kubuszok
Mateusz Kubuszok

Reputation: 27595

ApplicativeError[F, E] assumes that the type of E is somehow encoded in F and you could create instance of ApplicativeError[F, E] only because for this particular F it makes sense to have error E. Examples:

  • ApplicativeError[Task, Throwable] - Task uses Throwable as an error channel so it makes sense to expose Throwable as error algebra. As a matter of the fact Sync[F] from Cats Effect implements MonadError[F, Throwable], which in turns implement ApplicativeError[F, Throwable] - many type classes of Cats Effect assume that you only deal with Throwable
  • ApplicativeError[Task[Either[E, *]], E] - in such combinations you will have E both in type F's specific definition as well as in E parameter - this is typical for all kind of bifunctors: Task[Either[E, *]], EitherT[Task, E, *], ZIO's IO[E, A], and so on

The interface ApplicativeError[F, E] doesn't handle error on its own. But it exposes methods:

  • raiseError[A](e: E): F[A] - which creates an error
  • handleErrorWith[A](fa: F[A])(f: (E) ⇒ F[A]): F[A] - which handled it

so that you could tell it how to handle it.

Both works without assuming anything about the nature of error and F other that it is Applicative which can fail. If you only use F and type classes, you can use these methods to recover from error. And if you know the exact type of F on call site (because it is hardcoded to Task, IO, Coeval, etc) you can use recovery method directly.

The main difference is that result F[Either[E, A]] doesn't tell the caller that E should be treated as a failure. F[A] tells that there could be only A successful value. Additionally one requires Applicative while the other ApplicativeError, so there is difference in "power" required to create a value - if you see ApplicativeError even though there is no E in result you can assume that method might fail because it requires more powerful type class.

But of course it is not set in stone and it is mainly about expressing intentions, because everywhere you have F[A] you can convert to and from F[Either[E, A]] using ApplicativeError[F, E] (there are even methods for it like attempt[A](fa: F[A]): F[Either[E, A]] or fromEither[A](x: Either[E, A]): F[A]). So on one part of your application you can have F[A] with E algebra, but then there is that one pure function that uses Either[E, A] => B which you would like to use - then you could convert, map, and if necessary convert back.

TL;DR it is mainly about expressing intent as both are "morally" equal.

Upvotes: 7

Related Questions