-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
data.Xor and instances/either.scala: Either is now right-biased #1192
Comments
I like removing I'm sure there will be some opposition so I'll wait for them to speak up :-) Otherwise if nobody complains I'll try to put a patchset together this weekend. |
@adelbertc Any thoughts on 2.10/2.11 compatibility? E.g., would |
@mpilquist Yeah that's what I was thinking. Users will have to pay a penalty if they wish to stick with a la carte imports, I'm guessing via It also makes for some funny explanations when I say "Cats sticks with stdlib types when possible... oh except for Also if simple right-biased monadic scala> val e0: Either[String, Int] = Right(5)
e0: Either[String,Int] = Right(5)
scala> val e1: Either[String, Int] = Left("boo")
e1: Either[String,Int] = Left(boo)
scala> for {
| a <- e0
| b <- e1
| } yield b
<console>:15: error: value flatMap is not a member of Either[String,Int]
a <- e0
^
<console>:16: error: value map is not a member of Either[String,Int]
b <- e1
^
scala> import cats.implicits._
import cats.implicits._
scala> for {
| a <- e0
| b <- e1
| } yield b
res1: scala.util.Either[String,Int] = Left(boo) |
Oo I also just realized you can do (possibly immoral) tricks like this: import scala.util.{Either => SEither}
package object foo {
implicit class Either[A, B](val either: SEither[A, B]) extends AnyVal {
def map[C](f: B => C): SEither[A, C] = either match {
case Left(a) => Left(a)
case Right(b) => Right(f(b))
}
}
}
// in REPL
scala> val e: Either[String, Int] = Right(5)
e: Either[String,Int] = Right(5)
scala> e.map(_ + 1)
<console>:13: error: value map is not a member of Either[String,Int]
e.map(_ + 1)
^
scala> import foo.Either
import foo.Either
scala> e.map(_ + 1)
res1: scala.util.Either[String,Int] = Right(6) |
@mpilquist You could have a version-specific import, which would add the missing methods to Either on 2.10/2.11 and would be empty on 2.12. |
So assuming nobody objects to this today I'll start work on this tomorrow. My plan:
|
I finally get to use the 👎 on GitHub! 😄 As I said on Gitter, I'm strongly opposed to removing Relying on extension methods for something as basic as |
@travisbrown Is there as big of a performance difference with an implicit value class? |
@adelbertc It helps in the |
@travisbrown This sounds all very unspecific to me. Are there some specific issues that you can share? |
@soc For the record, here's one concrete example of that kind of prioritization issue. |
Strongly in favor. Data classes like I feel the runtime overhead of implicits needed in the short term is very manageable. I don't disagree that the prioritization issue is real if done via imports. I think the better approach would be to add the implicit to the Does anyone know if members of supertypes of imported objects are at a lower priority than members of the imported object itself? Somehow I doubt that would override import ordering though. (edit they aren't at a lower priority) |
@djspiewak I don't understand what you mean by the |
@travisbrown Oh derp. For some reason I thought we could put the implicit magic in |
In my projects I already preferred Either to Xor, even before finding out that Either will be right biased in 2.12. The reason is that Either is standard and in use much more than alternatives like Xor will ever be. And this is important for interoperability and learning curve. Don't get me wrong, the right bias is cool, but using a right projection on Either is a bearable nuisance. Plus I actually miss those projections when the left value is not just some error I want to ignore. So now that Either will be right biased, I'll probably never use Xor. That said, consider that Scala 2.11 will be around for a really long time, because 2.12 is the version that breaks compatibility with Java 6. |
Since it looks like there's a bit of debate going on I'll hold off on submitting a PR (if necessary) until we reach some sort of agreement. |
This is a half-baked implementation of typelevel/cats#1192. Several things are commented out because we don't yet use kind projector. EitherOps should be a value class for performance. This would fit much more nicely with `fs2.Task`, preventing any conversions between `Xor` and `Either` for `Task.async` and `.attempt` results. Without this, we're probably looking at `Task.asyncXor` and `task.attemptXor` syntax, which will come at the cost of extra conversions. The `widen` is irritating. `Left` and `Right` have two type parameters in the standard library, versus their one in cats. This seems to come up regularly in pattern matching. Maybe most of it is hidden in `EitherOps` in practice? The greater irritation is the fight between type classes and `EitherOps` for names like `.map` and `.flatMap`. One has to be very careful with imports. This is discussed a bit on Gitter: https://gitter.im/typelevel/cats?at=57a52c88483751d50f2d62fd
The commit above has an exploratory, ugly implementation of this. I won't step on @adelbertc's toes for a pull request, but the commit discusses some pros and cons in the context of a cats library that has to deal with |
It seems the fiddly-ness with regards to getting right-biased behavior on stdlib As for the behavior for Scala 2.1{0, 1} and Scala 2.12, it should just work right? It won't look or an implicit conversion/class unless the method is missing. In Scala 2.1{0,1} calling How do we want to move forward on this? We seem to have a lot of folks in favor, but I also want to make sure there's no strong-arming or anything happening to those opposed. EDIT @rossabaker and I just discovered some evilness - it seems implicit syntax does not play nice with aliases. Consider: trait Batteries extends cats.syntax.AllSyntax with cats.std.AllInstances with P0
trait P0 {
implicit class EitherSyntax[X, A](either: Either[X, A]) {
def map[B](f: A => B): Either[X, B] = either match {
case Left(x) => Left(x)
case Right(a) => Right(f(a))
}
}
}
object batteries extends Batteries
object MyApp {
import batteries._
type Result[A] = Either[String, A]
val e: Result[Int] = Right(5)
e.map(_ + 1)
} The compiler complains with: EitherSyntax.scala:19: type mismatch;
[error] found : MyApp.e.type (with underlying type MyApp.Result[Int])
[error] required: ?{def map: ?}
[error] Note that implicit conversions are not applicable because they are ambiguous:
[error] both method toFunctorOps in trait ToFunctorOps of type [F[_], A](target: F[A])(implicit tc: cats.Functor[F])cats.Functor.Ops[F,A]
[error] and method EitherSyntax in trait P0 of type [X, A](either: Either[X,A])batteries.EitherSyntax[X,A]
[error] are possible conversion functions from MyApp.e.type to ?{def map: ?}
[error] e.map(_ + 1)
[error] ^
[error] one error found
[error] (compile:compileIncremental) Compilation failed Adding @milessabin 's SI-2712 plugin allows the alias to work. Not aliasing at all works as well. Aliasing with a binary type constructor (e.g. If we don't have a way around this then it may spell bad news for users. EDIT 2 I suppose the |
scala> object EitherSyntax { // this would be added to cats
| implicit class EitherOps[A, B](self: Either[A, B]) {
| def map[C](f: B => C) = self.fold(Left(_), b => Right(f(b)))
| }
| }
defined object EitherSyntax
scala> import cats.std.either._, cats.syntax.functor._, EitherSyntax._
import cats.std.either._
import cats.syntax.functor._
import EitherSyntax._
scala> Right(5).map(_ * 2)
res0: Product with Serializable with scala.util.Either[Nothing,Int] = Right(10)
scala> type ParseResult[+A] = Either[String, A]
defined type alias ParseResult
scala> (Right(5): ParseResult[Int]).map(_ * 2)
<console>:23: error: type mismatch;
found : ParseResult[Int]
(which expands to) scala.util.Either[String,Int]
required: ?{def map: ?}
Note that implicit conversions are not applicable because they are ambiguous:
both method toFunctorOps in trait ToFunctorOps of type [F[_], A](target: F[A])(implicit tc: cats.Functor[F])cats.Functor.Ops[F,A]
and method EitherOps in object EitherSyntax of type [A, B](self: Either[A,B])EitherSyntax.EitherOps[A,B]
are possible conversion functions from ParseResult[Int] to ?{def map: ?}
(Right(5): ParseResult[Int]).map(_ * 2)
^
scala> type Disjunction[+A, +B] = Either[A, B]
defined type alias Disjunction
scala> (Right(5): Disjunction[Nothing, Int]).map(_ * 2)
res3: Product with Serializable with scala.util.Either[Nothing,Int] = Right(10) This is only going to bite people not using SI-2712, supporting Scala < 2.12, with |
In general I'm all for both using stdlib types and optimising for the future, but the kind of subtle inconsistencies that @rossabaker and @travisbrown mention do make me nervous. I wouldn't know which way to cast a vote, so I'm not sure this is a terribly helpful contribution, but I'm glad to see it getting plenty of thought. |
Keeping |
I think if we manage to solve the |
Cats adoption is growing while Scala 2.11 is near its crest. More Cats apps will be written on Scala >= 2.12 than on Scala < 2.12, so I give more weight to the 2.12+ experience. Secondly, the more Cats apps have already been written, the more expensive change becomes. The worst choice is to keep Thirdly, I hope that Typelevel projects reach a consensus. Our projects all gain value when they interoperate with minimal friction. I am actively porting http4s to Cats, and intend to steer toward Cats' preferred disjunction, regardless of it being mine. Standard library types need to be grossly deficient or little used to justify forfeiting their network effects. People will draw the line at different places, but Scala 2.12 brings |
I am reluctantly pro-either. In most cases in cats, we have |
This rings the most true with me. A couple years ago using Argonaut, Remotely, Doobie, Http4s, and Scalaz in a project was super nice, everything just clicked together and it was one of the most pleasant experiences I've had composing libraries across a variety of domains. If everyone used |
Given the feedback in this ticket I'm going to start working on this again soon unless someone yells at me again /cc @travisbrown |
@adelbertc I've resigned myself to having methods like |
@travisbrown Sounds good 👍 The plan:
These changes should be in the future 0.8.0 release. Note that |
One interesting thing: if for {
b <- toEitherTId(e)
c <- toEitherTId(fn(b))
} yield c Not sure that is better than using |
Here ya go: #1289 |
Replace Xor with Either, fixes #1192
How did you guys manage (or did you manage) to work around the ugliness of having two type parameters on Left/Right instead of one? |
@mpilquist - That is how Xor works, as do most of the other Either clones. But I want to know how you deal with the headache caused by the two type parameterization of |
I'm still stuck on 2.11 because I use EMR & Spark. How do I get If your going to remove something, would be nice to document how to get it back somewhere. This is ungooglable! |
@samthebest you can't get it back, unless you use 0.8.0, however, you can use |
Either has become right-biased in Scala 2.12, so I think it would make sense to
Xor
has benefits that are greater than converging on a singleEither
implementation in the Scala ecosystemThe text was updated successfully, but these errors were encountered: