Semigroup typeclass (Either) with slightly altered combine
Asked Answered
C

1

5

Using cats.Semigroup one can write this:

import cats.Semigroup
import cats.implicits._

val l1: String Either Int = Left("error")
val r1: String Either Int = Right(1)
val r2: String Either Int = Right(2)

l1 |+| r1 // Left("error")
r1 |+| r2 // Right(3)

I would like to have an equally idiomatic operator (combine-like) that works like this:

  • if there is (at least) one Right in my computation, return a Right
  • if there are only Lefts, return a Left

E.g.:

Right(1) |+| Right(2) // Right(3) 
Right(1) |+| Left("2") // Right(1) 
Left("1") |+| Left("2") // Left("12") // in my particular case the wrapped value here does not really matter (could also be e.g. Left("1") or Left("2")), but I guess Left("12") would be the must logical result

Is there something like this already defined in e.g. cats on Either?

Coelacanth answered 14/3, 2019 at 14:11 Comment(3)
Wouldn't just swapping Left and Right exactly satisfy your requirements? Maybe use the LeftProjection? Either is right-biased. Reference: Cats: EitherAsyndeton
So you want the behaviour like that: Right(1) |+| Right(2) // Right(3); Right(1) |+| Left("2") // Right(1); Left("1") |+| Left("2") // Left(???);?Wrac
You can create your own instance of the typeclass, with the behavior you want. However, you should be sure it follows the laws.Asia
F
11

There are a bunch of lawful semigroup instances for Either, and which of them should be included in Cats was a matter of some debate. Cats, Scalaz, and Haskell all make different choices in this respect, and the instance you're describing (flipped but with both lefts and right combining) is different from all three of those, it doesn't have a specific name that I'm aware of, and it isn't provided under any name or in any form by Cats.

That's of course not a problem in itself, since as we'll see below it's pretty easy to verify that this instance is lawful, but there is one potential issue you should be aware of. You don't really explain your intended semantics, but if you ever want to promote this to a Monoid, the fact that you pick the Right when you have both a Left and a Right means that your zero will have to be Left. This might be kind of weird if you're thinking of rights as successes and lefts as errors that are safe to ignore when combining values.

You're asking about Semigroup, though, not Monoid, so let's just ignore that for now and show that this thing is lawful. First for the definition:

import cats.kernel.Semigroup

implicit def eitherSemigroup[A, B](implicit
  A: Semigroup[A],
  B: Semigroup[B]
): Semigroup[Either[A, B]] = Semigroup.instance {
  case (Right(x), Right(y)) => Right(B.combine(x, y))
  case (r @ Right(_), Left(_)) => r
  case (Left(_), r @ Right(_)) => r
  case (Left(x), Left(y)) => Left(A.combine(x, y))
}

And then the checking part:

import cats.instances.int._
import cats.instances.string._
import cats.kernel.instances.either.catsStdEqForEither
import cats.kernel.laws.discipline.SemigroupTests
import org.scalacheck.Test.Parameters

SemigroupTests(eitherSemigroup[String, Int]).semigroup.all.check(Parameters.default)

And yeah, it's fine:

+ semigroup.associative: OK, passed 100 tests.
+ semigroup.combineAllOption: OK, passed 100 tests.
+ semigroup.repeat1: OK, passed 100 tests.
+ semigroup.repeat2: OK, passed 100 tests.

Personally if I wanted something like this I'd probably use a wrapper to avoid confusing future readers of my code (including myself), but given that nobody really knows what the semigroup of Either should do, I don't think using a custom instance is as big of a problem as it is for most other types from the standard library.

Ferreous answered 14/3, 2019 at 15:17 Comment(1)
Thank you very much for the detailed answer, it taught me a lot. :)Coelacanth

© 2022 - 2024 — McMap. All rights reserved.