Skip to content
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

Removed and replaced inconsistent Parallel derivation for EitherT #3777

Merged
merged 3 commits into from
Feb 9, 2021

Conversation

djspiewak
Copy link
Member

Fixes #3776

This is source breaking, but silently so. Anyone relying on the error-accumulating behavior in the old instance will be surprised by the new one, which will continue to compile on all the same call-sites, but which will exhibit different semantics than previously.

@larsrh
Copy link
Contributor

larsrh commented Feb 8, 2021

Possibly stupid question: since the signature of the old and new implicit are the same, why not just ... change the implementation?

I take it back, the new method has fewer implicits.

@larsrh larsrh added this to the 2.4 milestone Feb 8, 2021
Co-authored-by: Lars Hupel <lars.hupel@mytum.de>
@djspiewak
Copy link
Member Author

Had a long discussion with @Baccata and he pointed out that it's very important that we enable users to get access to the alternative Parallel semantic when they need it. This is analogous to the toValidated method on Either. Unfortunately, the obvious analogue, toNestedValidated, doesn't really work to get Parallel back because Nested cannot have a Parallel (because it cannot form a lawful Monad in general). This is… unfortunate.

One option here is to expose the old catsDataParallelForEitherTWithParallelEffect (the one which takes a Semigroup[E]) under a more ergonomic name and allow it to be imported explicitly into scope, similarly to Applicative.monoid or similar. A second option is to newtype EitherT to allow users to opt-in for the alternative semantic in a type-indexed fashion. I rather like the first option, and it's more consistent with the rest of Cats, but it runs straight into the problem of implicit priority conflicts with the Concurrent ~> Parallel orphan derivation in Cats Effect. If users have opted in to the semigroupal semantic in this area, it shouldn't be re-overridden for them even if they have the CE implicit in scope.

Basically we need to find an answer for that orphan.

@djspiewak
Copy link
Member Author

Oh, just so it's preserved for reference… The first principles argument here is surrounding the nature of what it means to be "an effect", and also what it means to merge the semantics of those effects together. The existing Parallel[EitherT[F, E instance is effectively implementing the Parallel effect by merging together the semantics of both Either and F, as opposed to unambiguously resolving in favor of one or the other. This is similar to the following situation:

type F[A] = StateT[State[Int, *], Int, A]

If we do Stateful[F, Int].set(42), there is a single unambiguous resolution to how this will be set up: I believe the inner-most layer is chosen (so in lexical terms, the StateT above). An equally possible semantic would be that both layers are chosen and the state of the inner and the outer is set to 42, but this is simply not how StateT works. This Parallel[EitherT situation is analogous to this.

Monad transformers always apply effects in a single layer (another example is MonadError[EitherT[Either[E, *], E, *]). They don't strictly need to behave in this fashion, but they do, and thus the default semantic for Parallel[EitherT[ should similarly apply in a single layer.

None of this is meant to imply that we shouldn't make the old semantic available, it's still very useful, it's simply the best "first principles" argument for a default semantic that we were able to come up with.

@djspiewak
Copy link
Member Author

Requesting review from @Baccata. I added a mechanism for restoring the non-default semantics in a lexical scope, and it will take precedence over the orphan in Cats Effect. You can even abuse package objects to force it into a whole package (recursively!) if you need to.

@joroKr21
Copy link
Member

joroKr21 commented Feb 8, 2021

Monad transformers always apply effects in a single layer (another example is MonadError[EitherT[Either[E, *], E, *]). They don't strictly need to behave in this fashion, but they do, and thus the default semantic for Parallel[EitherT[ should similarly apply in a single layer.

Doesn't that mean that only the error-accumulating instance should be available?

@djspiewak
Copy link
Member Author

Doesn't that mean that only the error-accumulating instance should be available?

I was hoping no one would notice that. :-P Yes it does imply that. However, this would be… essentially useless. It's also impossible to derive that semantic in terms of Concurrent. So in general, I'm coming around to the view that this is one of those cases where some things are true, some things are arbitrary, and some arbitrary things are useful, and when you can't pick the first one you should pick the last.

@joroKr21
Copy link
Member

joroKr21 commented Feb 8, 2021

I added a mechanism for restoring the non-default semantics in a lexical scope, and it will take precedence over the orphan in Cats Effect.

I'm not sure it would take precedence if both are in scope, at least on Scala 2.

You can even abuse package objects to force it into a whole package (recursively!) if you need to.

Not sure what you mean 😄

@Baccata
Copy link
Contributor

Baccata commented Feb 9, 2021

You can even abuse package objects to force it into a whole package (recursively!) if you need to.

@joroKr21 Daniel's change means that the current behaviour is not provided implicitly out of the box anymore, but the user can easily use the explicit accumulatingParallel method to do override the non-accumulating behaviour in userland :

package object myPackage {
    implicit def overridingBehaviour[F[_] : Parallel, E : Semigroup] : Parallel[EitherT[F, E, *] = EitherT.accumulatingParallel[F, E]
}

Because this accumulating behaviour is not default anymore, it means that a Parallel instance for EitherT derived from cats.effect.kernel.Concurrent (in cats-effect-3) would behave exactly the same than the new (non-accumulating) default here. Which means that the typeclasses are coherent and all is well.

What this means is that you and I weirdos that use EitherT's parallel to accumulate errors are essentially being softly sacrificed for the greater good 😄 (aka the people who rightly think that concurrency should imply parallelism, which is a very correct intuition).

Copy link
Contributor

@Baccata Baccata left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@djspiewak, not that I'm acting as a figure of authority on this in any way, shape or form, but you have my personal blessing (provided the same change is performed on IorT)

@LukaJCB, @larsrh , if this change is approved, could we maybe discuss the addition of a monadic ValidatedT (or a different name) that'd require both Parallel[F] and Semigroup[E] to derive a Parallel, filling the hole that's being created ?

@larsrh
Copy link
Contributor

larsrh commented Feb 9, 2021

could we maybe discuss the addition of a monadic ValidatedT (or a different name)

Fine with me.

(provided the same change is performed on IorT)

@djspiewak Can you add that to this PR?

@LukaJCB
Copy link
Member

LukaJCB commented Feb 9, 2021

I'm a bit queasy about the fact that this will silently change the behaviour of an instance, but I also don't have any idea how to alert these users except through release announcements etc, but I fear that cats is so far upstream that most upgrades will be pulled in transitively anyways, so I'm sure this will unfortunately lead to some surprises. :/
Also 👍 on adding something like ValidatedT to make this behaviour easy to access again.

@djspiewak
Copy link
Member Author

(provided the same change is performed on IorT)

The original issue was in err; IorT doesn't have the same semantic as far as I can tell, but it does take a Semigroup because that's just what Ior itself needs.

discuss the addition of a monadic ValidatedT

Well, ValidatedT can't be a monad, but I assume you're talking about an effective newtype around EitherT that defaults to this alternative semantic?

@Baccata
Copy link
Contributor

Baccata commented Feb 9, 2021

Well, ValidatedT can't be a monad

Well, you see, you've established that the MonadTransformers as a concept isn't well outlined :trollface: . So nothing would prevent ValidatedT to be monadic without being considered "MonadTransformer", and have validated semantics on its Parallel instance, without CE having to support it. Reusing the name Validated would help create the intuition for what it's about, I think .

@larsrh
Copy link
Contributor

larsrh commented Feb 9, 2021

After hours of deliberation with Daniel I have concluded that I definitely think that this maybe works. So let's ship it! 2.4.0 imminent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Parallel for EitherT and IorT is probably over constrained
5 participants