SemigroupK

Before introducing a SemigroupK, it makes sense to talk about what a Semigroup is. A semigroup for some given type A has a single operation (which we will call combine), which takes two values of type A, and returns a value of type A. This operation must be guaranteed to be associative. That is to say that:

((a combine b) combine c)

must be the same as

(a combine (b combine c))

for all possible values of a, b, c.

Cats does not define a Semigroup type class itself. Instead, we use the Semigroup trait which is defined in the algebra project. The cats package object defines type aliases to the Semigroup from algebra, so that you can import cats.semigroup.

There are instances of Semigroup defined for many types found in the scala common library:

import cats._
import cats.implicits._

Examples.

Semigroup[Int].combine(1, 2)
// res0: Int = 3

Semigroup[List[Int]].combine(List(1,2,3), List(4,5,6))
// res1: List[Int] = List(1, 2, 3, 4, 5, 6)

Semigroup[Option[Int]].combine(Option(1), Option(2))
// res2: Option[Int] = Some(3)

Semigroup[Option[Int]].combine(Option(1), None)
// res3: Option[Int] = Some(1)

Semigroup[Int => Int].combine({(x: Int) => x + 1},{(x: Int) => x * 10}).apply(6)
// res4: Int = 67

SemigroupK has a very similar structure to Semigroup, the difference is that SemigroupK operates on type constructors of one argument. So, for example, whereas you can find a Semigroup for types which are fully specified like Int or List[Int] or Option[Int], you will find SemigroupK for type constructors like List and Option. These types are type constructors in that you can think of them as "functions" in the type space. You can think of the List type as a function which takes a concrete type, like Int, and returns a concrete type: List[Int]. This pattern would also be referred to having kind: * -> *, whereas Int would have kind * and Map would have kind *,* -> *, and, in fact, the K in SemigroupK stands for Kind.

For List, the Semigroup instance's combine operation and the SemigroupK instance's combineK operation are both list concatenation:

SemigroupK[List].combineK(List(1,2,3), List(4,5,6)) == Semigroup[List[Int]].combine(List(1,2,3), List(4,5,6))
// res5: Boolean = true

However for Option, the Semigroup's combine and the SemigroupK's combineK operation differ. Since Semigroup operates on fully specified types, a Semigroup[Option[A]] knows the concrete type of A and will use Semigroup[A].combine to combine the inner As. Consequently, Semigroup[Option[A]].combine requires an implicit Semigroup[A].

In contrast, since SemigroupK[Option] operates on Option where the inner type is not fully specified and can be anything (i.e. is "universally quantified"). Thus, we cannot know how to combine two of them. Therefore, in the case of Option the SemigroupK[Option].combineK method has no choice but to use the orElse method of Option:

Semigroup[Option[Int]].combine(Some(1), Some(2))
// res6: Option[Int] = Some(3)

SemigroupK[Option].combineK(Some(1), Some(2))
// res7: Option[Int] = Some(1)

SemigroupK[Option].combineK(Some(1), None)
// res8: Option[Int] = Some(1)

SemigroupK[Option].combineK(None, Some(2))
// res9: Option[Int] = Some(2)

There is inline syntax available for both Semigroup and SemigroupK. Here we are following the convention from scalaz, that |+| is the operator from semigroup and that <+> is the operator from SemigroupK (called Plus in scalaz).

import cats.implicits._

val one = Option(1)
val two = Option(2)
val n: Option[Int] = None

Thus.

one |+| two
// res11: Option[Int] = Some(3)

one <+> two
// res12: Option[Int] = Some(1)

n |+| two
// res13: Option[Int] = Some(2)

n <+> two
// res14: Option[Int] = Some(2)

n |+| n
// res15: Option[Int] = None

n <+> n
// res16: Option[Int] = None

two |+| n
// res17: Option[Int] = Some(2)

two <+> n
// res18: Option[Int] = Some(2)

You'll notice that instead of declaring one as Some(1), we chose Option(1), and we added an explicit type declaration for n. This is because the SemigroupK type class instances is defined for Option, not Some or None. If we try to use Some or None, we'll get errors:

scala> Some(1) <+> None
<console>:22: error: value <+> is not a member of Some[Int]
       Some(1) <+> None
               ^

scala> None <+> Some(1)
res20: Option[Int] = Some(1)

results matching ""

    No results matching ""