API Documentation: SemigroupK

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.

First some imports.

import cats._
import cats.syntax.all._

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))
// res0: 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, 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))
// res1: Option[Int] = Some(value = 3)
SemigroupK[Option].combineK(Some(1), Some(2))
// res2: Option[Int] = Some(value = 1)
SemigroupK[Option].combineK(Some(1), None)
// res3: Option[Int] = Some(value = 1)
SemigroupK[Option].combineK(None, Some(2))
// res4: Option[Int] = Some(value = 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.syntax.all._

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


one |+| two
// res5: Option[Int] = Some(value = 3)
one <+> two
// res6: Option[Int] = Some(value = 1)
n |+| two
// res7: Option[Int] = Some(value = 2)
n <+> two
// res8: Option[Int] = Some(value = 2)
n |+| n
// res9: Option[Int] = None
n <+> n
// res10: Option[Int] = None
two |+| n
// res11: Option[Int] = Some(value = 2)
two <+> n
// res12: Option[Int] = Some(value = 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:

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