Semigroup

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.

There are instances of Semigroup defined for many types found in the scala common library. For example, Int values are combined using addition by default but multiplication is also associative and forms another Semigroup.

import cats.Semigroup

Now that you've learned about the Semigroup instance for Int try to guess how it works in the following examples:

import cats.implicits._

Semigroup[Int].combine(1, 2) should be(res0)
Semigroup[List[Int]].combine(List(1, 2, 3), List(4, 5, 6)) should be(res1)
Semigroup[Option[Int]].combine(Option(1), Option(2)) should be(res2)
Semigroup[Option[Int]].combine(Option(1), None) should be(res3)

And now try a slightly more complex combination:

import cats.implicits._

Semigroup[Int => Int].combine(_ + 1, _ * 10).apply(6) should be(res0)

Many of these types have methods defined directly on them, which allow for such combining, e.g. ++ on List, but the value of having a Semigroup type class available is that these compose, so for instance, we can say

Map("foo" -> Map("bar" -> 5)).combine(Map("foo" -> Map("bar" -> 6), "baz" -> Map()))
Map("foo" -> List(1, 2)).combine(Map("foo" -> List(3, 4), "bar" -> List(42)))

which is far more likely to be useful than

Map("foo" -> Map("bar" -> 5)) ++ Map("foo" -> Map("bar" -> 6), "baz" -> Map())
Map("foo" -> List(1, 2)) ++ Map("foo" -> List(3, 4), "bar" -> List(42))

since the first version uses the Semigroup's combine operation, it will merge the map's values with combine.

import cats.implicits._

val aMap = Map("foo" -> Map("bar" -> 5))
val anotherMap = Map("foo" -> Map("bar" -> 6))
val combinedMap = Semigroup[Map[String, Map[String, Int]]].combine(aMap, anotherMap)

combinedMap.get("foo") should be(Some(res0))

There is inline syntax available for Semigroup. Here we are following the convention from scalaz, that |+| is the operator from Semigroup.

You'll notice that instead of declaring one as Some(1), I chose Option(1), and I added an explicit type declaration for n. This is because there aren't type class instances for Some or None, but for Option.

import cats.implicits._

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

one |+| two should be(res0)
n |+| two should be(res1)
n |+| n should be(res2)
two |+| n should be(res3)