Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Semigroup typeclass (Either) with slightly altered combine

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?

like image 301
Florian Baierl Avatar asked Mar 14 '19 14:03

Florian Baierl


People also ask

What is the ‘semigroup’ typeclass?

The ‘Semigroup’ typeclass in Haskell is an abstraction that serves to provide the semigroup algebraic structure for types that implement it. Any type ‘a’ can implement the Semigroup typeclass, providing that the type has an associative binary operation of the type: The binary function needs only to be associative, not commutative.

What is a semigroup in Haskell?

In abstract algebra, a semigroup is a set together with a binary operation. For set, in Haskell, you can more or less substitute the word type; there are ways in which types do not perfectly correspond to sets, but it is close enough for this purpose. A binary operation is a function that takes two arguments.

What is an example of a semigroup in math?

Example: Consider a semigroup (N, +), where N is the set of all natural numbers and + is an addition operation. The algebraic system (E, +) is a subsemigroup of (N, +), where E is a set of +ve even integers. Consider a non empty set A = {a 1 ,a 2 ,.....a n }. Now, A* is the set of all finite sequences of elements of A, i.e.,

What is the difference between a semigroup and a subsemigroup?

Hence, the algebraic system (A, *), is a semigroup. Consider a semigroup (A, *) and let B ⊆ A. Then the system (B, *) is called a subsemigroup if the set B is closed under the operation *. Example: Consider a semigroup (N, +), where N is the set of all natural numbers and + is an addition operation.


1 Answers

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.

like image 124
Travis Brown Avatar answered Sep 25 '22 22:09

Travis Brown