-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add ErrorControl #2231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ErrorControl #2231
Conversation
4b00540
to
7399c43
Compare
implicit val x: ErrorControl[Kleisli[Option, Int, ?], Kleisli[Id, Int, ?], Unit] = | ||
Kleisli.catsErrorControlForKleisli[Option, Id, Int, Unit] | ||
|
||
checkAll("Kleisli[Option, Int, ?]", ErrorControlTests[Kleisli[Option, Int, ?], Kleisli[Id, Int, ?], Unit].errorControl[Int]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason Scala can't seem to find an implicit ErrorControl[Kleisli[Option, Int, ?], Kleisli[Id, Int, ?], Unit]
even though it is defined in the companion object of Kleisli
. Anyone know a way to help scalac here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if you call Kleisli.catsErrorControlForKleisli[Option, Id, Int, Unit]
directly? does it complain that couldn't find ErrorControl[Option, Id, Unit]
?
Edit : oh you mean that implicit val is required to help scalac there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah it won't compile without it :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, since StateT has exactly the same shape but passes with an implicit Monad[G], I wonder what if you add Monad[G]
to Kleisli
might help?
Generally, when I get into this type of error (the implicit method is available and legit but scalac refuses to find it) I'll just throw in a Lazy
and see if it helps. but that's not an option here. And I suspect that it's not the same scalac bug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, I needed to add the extra implicit to WriterT and StateT as well, so it's the same problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(╯°□°)╯︵ ┻━┻ time!
Just to experiment, what if add them to the companion of ErrorControl? (maybe it's easier to find if they are togther? )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, I'll try that :)
new IndexedStateTAlternative[F, S] { implicit def F = FM; implicit def G = FA } | ||
|
||
implicit def catsErrorControlForStateT[F[_], G[_], S, E] | ||
(implicit E: ErrorControl[F, G, E], M: Monad[G]): ErrorControl[StateT[F, S, ?], StateT[G, S, ?], E] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
M: Monad[G]
no longer required since it's provided by ErrorControl
right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, will fix, this was needed when G was an Applicative so that StateT[G, S, A]
could be an Applicative
Ok, I'm gonna bikeshed here :) This is my opinion, but without proposed resolution. I'm not convinced yet it's the best way to handle errors, so would at least want some discussion before it becomes part of cats. First, let's point out that it seems possible to write a "@jdegoes transformer" to give precise exception type on top of stuff that has This might have very negligible impact on performance for existing typess, so it's alone is worth pursuing IMO, as right now our only option for typed errors is The proposed abstraction seems to capture the relationship between exceptional and unexceptional types, but it doesn't do it (at least I don't see it straight away) for e.g. "restricted" exceptional IO and unexceptional one ( |
Codecov Report
@@ Coverage Diff @@
## master #2231 +/- ##
==========================================
+ Coverage 95.05% 95.08% +0.02%
==========================================
Files 333 337 +4
Lines 5789 5896 +107
Branches 211 226 +15
==========================================
+ Hits 5503 5606 +103
- Misses 286 290 +4
Continue to review full report at Codecov.
|
The I hear your point about context bounds, but that just comes with the territory, we can't use them with any of the mtl classes, so it's some necessary boilerplate that's to be expected in some parts. |
Anybody who wants to take advantage of new abstraction would have to change their code. I don't see how bifunctor version would break anything (except if we remove MonadError too, which I didn't propose to do).
Yes. It's a question whether we should do stuff, not whether we could. Adding something to cats might be easy, but removing it is likely to be nearly impossible. And I do quite like the minimalism in cats API. So I'd rather not add stuff if it will become obsolete (like I said, I'm not sure what approach is the best, so that is a possibility).
It's more like |
import cats.data.EitherT | ||
|
||
trait ErrorControl[F[_], G[_], E] extends Serializable { | ||
val monadErrorF: MonadError[F, E] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like using abstract vals on traits since it invites the val init order bug. Can we make these def?
|
||
import cats.data.EitherT | ||
|
||
trait ErrorControl[F[_], G[_], E] extends Serializable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like to use abstract types as "output types". It seems to me we might want G[_]
to be an abstract type here, since we might often want to write algorithms to work in F go through G, but return an F. Then we could accept an implicit ErrorControl[F, E]
to do that, which might be nice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that as well, see my recent issue in parallel, though I'm not 100% sure if it applies here, have to think about it more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we'd be able to express these https://github.com/typelevel/cats/pull/2231/files#diff-aadaecd5f11de32b7cdacd702c9a5393R38 maybe you know a way?
implicit def catsDataMonadForOptionT[F[_]](implicit F0: Monad[F]): Monad[OptionT[F, ?]] = | ||
new OptionTMonad[F] { implicit val F = F0 } | ||
|
||
implicit def catsEndeavorForOptionT[F[_]: Monad, E]: ErrorControl[OptionT[F, ?], F, Unit] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be catsErrorControlForOptionT?
val monadG: Monad[G] | ||
|
||
def controlError[A](fa: F[A])(f: E => G[A]): G[A] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems like a def control[A,B](fa: F[A])(f: Either[E, A] => G[B]): G[B]
might be a nice function to have. It is more general than controlError and can also be used to implement trial. I think users might often want this variant personally.
The name might not be great, but I would love to see this and have syntax for it.
Given @LukaJCB's cool adapter ("@jdegoes transformer" 😆), I personally prefer the much simpler: trait Fallible[B[_, _]] extends Bifunctor[B] {
def applicative[E]: Applicative[B[E, ?]]
def attempt[E1, E2, A](b: B[E1, A]): B[E2, Either[E1, A]]
def fail[E, A](e: E): B[E, A]
} This is a sufficient basis to implement everything else, and has very precise types and laws. The only disadvantage, if you consider it such, is that it requires you use an adapter in order to use the type class. |
Right, I don't disagree here, though with For existing applications I think this is the better way forward. Cats-1.0.0-RC1 was just released and we aren't going to change the nature of
Again, I agree in principal, but I see these two as rather orthogonal (kind of like how |
I'm not proposing to change all existing effect types, it is probably not feasible with current ecosystem state and b/c the best approach is not yet set in stone. Newtyping would be the way to go here, so we will have e.g. We will also be able to provide e.g. a It's also more general than unexceptional types. To represent lack of errors, you can use
Similar can be said about three-parameter version of |
👍 Fully agree here.
I agree here as well, however I'd like to argue
That's fair, I've been experimenting with both approaches for some time now, and concluded for myself that existing code bases are probably better of leveraging unexceptional types, which provide more interoperatability with existing code. Of course, that's only my opinion and my experimentation has only been going on for a couple of months, so my conclusion might still be a bit premature. :) I would certainly love to hear more input from others. Btw, implementing implicit def errorControlForFallible[F[_, _]: Fallible, E]: ErrorControl[F[E, ?], F[Nothing, ?], E] =
new ErrorControl[F[E, ?], F[Nothing, ?], E] {
def controlError[A](fa: F[E, A])(f: E => F[Void, A]): F[Void, A] =
Fallible[F].attempt(fa).flatMap {
case Right(a) => pure(a)
case Left(e) => f(e)
}
def accept[A](fa: F[Nothing, A]): F[E, A] = fa.leftMap(_.absurd[E])
} |
There was a typeclass |
c8c35a1
to
e78901b
Compare
Thanks for that link @wedens! Another problem is that unlike def foo[F[_]](fi: F[Int])(implicit EC: ErrorControl[F, String]): EC.G[Int] =
F.intercept(fi)(_ => 0) Now we have to go into the type class instance and grab the type from there, not really pretty unfortunately. |
If we do make object ErrorControl {
type Aux[F[_], G0[_], E] = ErrorControl[F, E] { type G = G0 }
} so that user can write def foo[F[_], G[_]](fi: F[Int])(implicit EC: ErrorControl.Aux[F, G, String]): G[Int] = Then user can basically pick to use this The syntax machinery in |
Right that makes sense, thank you @kailuowang! Though now I'm more leaning towards keeping the two type constructors, as it doesn't seem to provide a large benefit and adds complexity for people not familiar with the |
d866e28
to
ed4bfbd
Compare
Anyone have any additional comments? I don't want to rush this, but interested in more feedback :) |
TBH, I'm still not sure how I feel about adding this to
And as soon as the computation involves a sub-computation that does not satisfy the conditions, the whole computation has to maintain the error effect. So it seems to me that the scope of this abstraction would be rather limited within an application. A real-world application example would really shed more light on this. Without it, IMO, it would be hard to convince people that this is actually more practical than |
I, for one, would love to revive this PR. A useful scenario is having This would make it much easier to work with sealed error ADTs that aren't also Throwables (while working with single-functor IO). While @kailuowang do you have any thoughts after this time? @LukaJCB do you still consider I'd also love to read what @SystemFw thinks about this. |
I'm still quite fond of this little type class, though I admit it's not that strong an abstraction. But from my time with using |
WDYT about publishing it as a small separate lib for now? Just so we don't have to commit to having this in cats in the long term. |
I am +1 on experimenting it with a separate lib or module. |
An extra module sounds good 👍 though I probably won't have any time to do it anytime soon :( |
I created a repo for this, we can transfer to you later @LukaJCB, hope you don't mind :) I'll try to publish some time this week. error-control |
Not at all, thanks for running with it :) |
Should fix #2229
Bikeshedding welcome :)