Reputation: 22334
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
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 onThe interface ApplicativeError[F, E]
doesn't handle error on its own. But it exposes methods:
raiseError[A](e: E): F[A]
- which creates an errorhandleErrorWith[A](fa: F[A])(f: (E) ⇒ F[A]): F[A]
- which handled itso 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