From 7f7188ee578cd2b8b3803e3730eeaf16f94004a5 Mon Sep 17 00:00:00 2001 From: Adelbert Chang Date: Wed, 26 Oct 2016 18:12:08 -0700 Subject: [PATCH 1/2] Revamp Semigroup doc --- docs/src/main/tut/typeclasses/monoid.md | 160 ++++++++++++------ docs/src/main/tut/typeclasses/semigroup.md | 180 ++++++++++++++------- 2 files changed, 240 insertions(+), 100 deletions(-) diff --git a/docs/src/main/tut/typeclasses/monoid.md b/docs/src/main/tut/typeclasses/monoid.md index 6ecf34b285..223dfb5212 100644 --- a/docs/src/main/tut/typeclasses/monoid.md +++ b/docs/src/main/tut/typeclasses/monoid.md @@ -7,70 +7,138 @@ scaladoc: "#cats.kernel.Monoid" --- # Monoid -`Monoid` extends the [`Semigroup`](semigroup.html) type class, adding an -`empty` method to semigroup's `combine`. The `empty` method must return a -value that when combined with any other instance of that type returns the -other instance, i.e. +`Monoid` extends the power of `Semigroup` by providing an additional `empty` value. -```scala -(combine(x, empty) == combine(empty, x) == x) -``` - -For example, if we have a `Monoid[String]` with `combine` defined as string -concatenation, then `empty = ""`. +```tut:book:silent +trait Semigroup[A] { + def combine(x: A, y: A): A +} -Having an `empty` defined allows us to combine all the elements of some -potentially empty collection of `T` for which a `Monoid[T]` is defined and -return a `T`, rather than an `Option[T]` as we have a sensible default to -fall back to. +trait Monoid[A] extends Semigroup[A] { + def empty: A +} +``` -First some imports. +This `empty` value should be an identity for the `combine` operation, which means the following equalities hold +for any choice of `x`. -```tut:silent -import cats._ -import cats.implicits._ +``` +combine(x, empty) = combine(empty, x) = x ``` -Examples. +Many types that form a `Semigroup` also form a `Monoid`, such as `Int`s (with `0`) and `Strings` (with `""`). -```tut:book -Monoid[String].empty -Monoid[String].combineAll(List("a", "b", "c")) -Monoid[String].combineAll(List()) +```tut:reset:book:silent +import cats.Monoid + +implicit val intAdditionMonoid: Monoid[Int] = new Monoid[Int] { + def empty: Int = 0 + def combine(x: Int, y: Int): Int = x + y +} + +val x = 1 ``` -The advantage of using these type class provided methods, rather than the -specific ones for each type, is that we can compose monoids to allow us to -operate on more complex types, e.g. - ```tut:book -Monoid[Map[String,Int]].combineAll(List(Map("a" -> 1, "b" -> 2), Map("a" -> 3))) -Monoid[Map[String,Int]].combineAll(List()) +Monoid[Int].combine(x, Monoid[Int].empty) + +Monoid[Int].combine(Monoid[Int].empty, x) +``` + +# Exploiting laws: associativity and identity + +In the `Semigroup` section we had trouble writing a generic `combineAll` function because we had nothing +to give if the list was empty. With `Monoid` we can return `empty`, giving us + +```tut:book:silent +def combineAll[A: Monoid](as: List[A]): A = + as.foldLeft(Monoid[A].empty)(Monoid[A].combine) ``` -This is also true if we define our own instances. As an example, let's use -[`Foldable`](foldable.html)'s `foldMap`, which maps over values accumulating -the results, using the available `Monoid` for the type mapped onto. +which can be used for any type that has a `Monoid` instance. + +```tut:book:silent +import cats.instances.all._ +``` ```tut:book -val l = List(1, 2, 3, 4, 5) -l.foldMap(identity) -l.foldMap(i => i.toString) +combineAll(List(1, 2, 3)) + +combineAll(List("hello", " ", "world")) + +combineAll(List(Map('a' -> 1), Map('a' -> 2, 'b' -> 3), Map('b' -> 4, 'c' -> 5))) + +combineAll(List(Set(1, 2), Set(2, 3, 4, 5))) ``` -To use this -with a function that produces a tuple, cats also provides a `Monoid` for a tuple -that will be valid for any tuple where the types it contains also have a -`Monoid` available, thus. +This function is provided in Cats as `Monoid.combineAll`. + +# The `Option` monoid + +There are some types that can form a `Semigroup` but not a `Monoid`. For example, the +following `NonEmptyList` type forms a semigroup through `++`, but has no corresponding +identity element to form a monoid. + +```tut:book:silent +import cats.Semigroup + +final case class NonEmptyList[A](head: A, tail: List[A]) { + def ++(other: NonEmptyList[A]): NonEmptyList[A] = NonEmptyList(head, tail ++ other.toList) + + def toList: List[A] = head :: tail +} + +object NonEmptyList { + implicit def nonEmptyListSemigroup[A]: Semigroup[NonEmptyList[A]] = + new Semigroup[NonEmptyList[A]] { + def combine(x: NonEmptyList[A], y: NonEmptyList[A]): NonEmptyList[A] = x ++ y + } +} +``` + +How then can we collapse a `List[NonEmptyList[A]]` ? For such types that only have a `Semigroup` we can +lift into `Option` to get a `Monoid`. + +```tut:book:silent +import cats.syntax.semigroup._ + +implicit def optionMonoid[A: Semigroup]: Monoid[Option[A]] = new Monoid[Option[A]] { + def empty: Option[A] = None + + def combine(x: Option[A], y: Option[A]): Option[A] = + x match { + case None => y + case Some(xv) => + y match { + case None => x + case Some(yv) => Some(xv |+| yv) + } + } +} +``` + +This is the `Monoid` for `Option`: for any `Semigroup[A]`, there is a `Monoid[Option[A]]`. + +Thus: + +```tut:reset:book:silent +import cats.Monoid +import cats.data.NonEmptyList +import cats.instances.option._ + +val list = List(NonEmptyList(1, List(2, 3)), NonEmptyList(4, List(5, 6))) +val lifted = list.map(nel => Option(nel)) +``` ```tut:book -l.foldMap(i => (i, i.toString)) // do both of the above in one pass, hurrah! +Monoid.combineAll(lifted) ``` -------------------------------------------------------------------------------- - +This lifting and combining of `Semigroup`s into `Option` is provided by Cats as `Semigroup.combineAllOption`. + +----- + N.B. -Cats defines the `Monoid` type class in cats-kernel. The [`cats` package object](https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/package.scala) -defines type aliases to the `Monoid` from cats-kernel, so that you can -`import cats.Monoid`. Also the `Monoid` instance for tuple is also [implemented in cats-kernel](https://github.com/typelevel/cats/blob/master/project/KernelBoiler.scala), -cats merely provides it through [inheritance](https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/std/tuple.scala). +Cats defines the `Monoid` type class in cats-kernel. The +[`cats` package object](https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/package.scala) +defines type aliases to the `Monoid` from cats-kernel, so that you can simply import `cats.Monoid`. diff --git a/docs/src/main/tut/typeclasses/semigroup.md b/docs/src/main/tut/typeclasses/semigroup.md index 39a2687ea3..fa4313a0df 100644 --- a/docs/src/main/tut/typeclasses/semigroup.md +++ b/docs/src/main/tut/typeclasses/semigroup.md @@ -7,92 +7,164 @@ scaladoc: "#cats.kernel.Semigroup" --- # 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: +If a type `A` can form a `Semigroup` it has an **associative** binary operation. -```scala -((a combine b) combine c) +```tut:book:silent +trait Semigroup[A] { + def combine(x: A, y: A): A +} ``` -must be the same as +`combine` also needs to be associative, which means the following equality must hold for any choices of `x`, `y`, and +`z`. -```scala -(a combine (b combine c)) +``` +combine(x, combine(y, z)) = combine(combine(x, y), z) +``` + +A common example of a semigroup is the type `Int` with the operation `+`. + +```tut:reset:book:silent +import cats.Semigroup + +implicit val intAdditionSemigroup: Semigroup[Int] = new Semigroup[Int] { + def combine(x: Int, y: Int): Int = x + y +} + +val x = 1 +val y = 2 +val z = 3 +``` + +```tut:book +Semigroup[Int].combine(x, y) + +Semigroup[Int].combine(x, Semigroup[Int].combine(y, z)) + +Semigroup[Int].combine(Semigroup[Int].combine(x, y), z) +``` + +Infix syntax is also available for types that have a `Semigroup` instance. + +```tut:book +import cats.syntax.semigroup._ + +1 |+| 2 +``` + +# Example instances + +Cats provides many `Semigroup` instances out of the box such as `Int` (`+`) and `String` (`++`)... + +```tut:reset:book:silent +import cats.Semigroup +import cats.instances.all._ ``` -for all possible values of a,b,c. +```tut:book +Semigroup[Int] +Semigroup[String] +``` -There are instances of `Semigroup` defined for many types found in the -scala common library: +Instances for type constructors regardless of their type parameter such as `List` (`++`) +and `Set` (`union`)... -First some imports. +```tut:book +Semigroup[List[Byte]] +Semigroup[Set[Int]] -```tut:silent -import cats._ -import cats.implicits._ +trait Foo +Semigroup[List[Foo]] ``` -Examples. +And instances for type constructors that depend on (one of) their type parameters having instances such +as tuples (pointwise `combine`). ```tut:book -Semigroup[Int].combine(1, 2) -Semigroup[List[Int]].combine(List(1,2,3), List(4,5,6)) -Semigroup[Option[Int]].combine(Option(1), Option(2)) -Semigroup[Option[Int]].combine(Option(1), None) -Semigroup[Int => Int].combine({(x: Int) => x + 1},{(x: Int) => x * 10}).apply(6) +Semigroup[(List[Foo], Int)] ``` -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 +# Example usage: Merging maps + +Consider a function that merges two `Map`s that combines values if they share +the same key. It is straightforward to write these for `Map`s with values of +type say, `Int` or `List[String]`, but we can write it once and for all for +any type with a `Semigroup` instance. + +```tut:book:silent +import cats.instances.all._ +import cats.syntax.semigroup._ + +def optionCombine[A: Semigroup](a: A, opt: Option[A]): A = + opt.map(a |+| _).getOrElse(a) + +def mergeMap[K, V: Semigroup](lhs: Map[K, V], rhs: Map[K, V]): Map[K, V] = + lhs.foldLeft(rhs) { + case (acc, (k, v)) => acc.updated(k, optionCombine(v, acc.get(k))) + } +``` ```tut:book -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))) +val xm1 = Map('a' -> 1, 'b' -> 2) +val xm2 = Map('b' -> 3, 'c' -> 4) + +val x = mergeMap(xm1, xm2) + +val ym1 = Map(1 -> List("hello")) +val ym2 = Map(2 -> List("cats"), 1 -> List("world")) + +val y = mergeMap(ym1, ym2) ``` -which is far more likely to be useful than +It is interesting to note that the type of `mergeMap` satisfies the type of `Semigroup` +specialized to `Map[K, ?]` and is associative - indeed the `Semigroup` instance for `Map` +uses the same function for its `combine`. ```tut:book -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)) +Semigroup[Map[Char, Int]].combine(xm1, xm2) == x + +Semigroup[Map[Int, List[String]]].combine(ym1, ym2) == y ``` -There is inline syntax available for `Semigroup`. Here we are -following the convention from scalaz, that `|+|` is the -operator from `Semigroup`. +# Exploiting laws: associativity + +Since we know `Semigroup#combine` must be associative, we can exploit this when writing +code against `Semigroup`. For instance, to sum a `List[Int]` we can choose to either +`foldLeft` or `foldRight` since all that changes is associativity. -```tut:silent -import cats.implicits._ +```tut:book +val leftwards = List(1, 2, 3).foldLeft(0)(_ |+| _) -val one = Option(1) -val two = Option(2) -val n: Option[Int] = None +val rightwards = List(1, 2, 3).foldRight(0)(_ |+| _) ``` -Thus. +Associativity also allows us to split a list apart and sum the parts in parallel, gathering the results in +the end. + +```tut:book:silent +val list = List(1, 2, 3, 4, 5) +val (left, right) = list.splitAt(2) +``` ```tut:book -one |+| two -n |+| two -n |+| n -two |+| n +val sumLeft = left.foldLeft(0)(_ |+| _) +val sumRight = right.foldLeft(0)(_ |+| _) +val result = sumLeft |+| sumRight ``` -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`. If we try to use `Some` and `None`, we'll get errors: +However, given just `Semigroup` we cannot write the above expressions generically. For instance, we quickly +run into issues if we try to write a generic `combineAll` function. -```tut:nofail -Some(1) |+| None -None |+| Some(1) +```scala +def combineAll[A: Semigroup](as: List[A]): A = + as.foldLeft(/* ?? what goes here ?? */)(_ |+| _) ``` +`Semigroup` isn't powerful enough for us to implement this function - namely, it doesn't give us an identity +or fallback value if the list is empty. We need a power expressive abstraction, which we can find in the +`Monoid` type class. + N.B. -Cats defines the `Semigroup` type class in cats-kernel. The [`cats` package object](https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/package.scala) -defines type aliases to the `Semigroup` from cats-kernel, so that you can -`import cats.Semigroup`. +Cats defines the `Semigroup` type class in cats-kernel. The +[`cats` package object](https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/package.scala) +defines type aliases to the `Semigroup` from cats-kernel, so that you can simply import `cats.Semigroup`. From 8911efbc061d360e1b7d6b9d1503295e7c96d7b1 Mon Sep 17 00:00:00 2001 From: Adelbert Chang Date: Sun, 30 Oct 2016 16:10:56 -0700 Subject: [PATCH 2/2] Grammar and examples for Semigroup and Monoid tutorial --- docs/src/main/tut/typeclasses/monoid.md | 2 +- docs/src/main/tut/typeclasses/semigroup.md | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/src/main/tut/typeclasses/monoid.md b/docs/src/main/tut/typeclasses/monoid.md index 223dfb5212..07e7c6fa0a 100644 --- a/docs/src/main/tut/typeclasses/monoid.md +++ b/docs/src/main/tut/typeclasses/monoid.md @@ -45,7 +45,7 @@ Monoid[Int].combine(x, Monoid[Int].empty) Monoid[Int].combine(Monoid[Int].empty, x) ``` -# Exploiting laws: associativity and identity +# Example usage: Collapsing a list In the `Semigroup` section we had trouble writing a generic `combineAll` function because we had nothing to give if the list was empty. With `Monoid` we can return `empty`, giving us diff --git a/docs/src/main/tut/typeclasses/semigroup.md b/docs/src/main/tut/typeclasses/semigroup.md index fa4313a0df..bfcde3a7e7 100644 --- a/docs/src/main/tut/typeclasses/semigroup.md +++ b/docs/src/main/tut/typeclasses/semigroup.md @@ -15,7 +15,7 @@ trait Semigroup[A] { } ``` -`combine` also needs to be associative, which means the following equality must hold for any choices of `x`, `y`, and +Associativity means the following equality must hold for any choice of `x`, `y`, and `z`. ``` @@ -52,6 +52,22 @@ import cats.syntax.semigroup._ 1 |+| 2 ``` +A more compelling example which we'll see later in this tutorial is the `Semigroup` +for `Map`s. + +```tut:book:silent +import cats.instances.map._ + +val map1 = Map("hello" -> 0, "world" -> 1) +val map2 = Map("hello" -> 2, "cats" -> 3) +``` + +```tut:book +Semigroup[Map[String, Int]].combine(map1, map2) + +map1 |+| map2 +``` + # Example instances Cats provides many `Semigroup` instances out of the box such as `Int` (`+`) and `String` (`++`)...