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.