"In abstract algebra, a branch of mathematics, a monoid is an algebraic structure with
(1) a single associative binary operation
and
(2) an identity element."
trait Monoid[A] {
def op(a1: A, a2: A): A
def zero: A
}
Note:
Our usual combinators are not inside the Monoid trait.
Every lecture so far has repeated the pattern:
Option, or function, like Randimplement the usual combinators on it
unitflatMapmapmap2Monoid is a departure from this pattern.
Strings under concatenation form a Monoid
trait Monoid[A] {
def op(a1: A, a2: A): A
def zero: A
}
val monoidString = new Monoid[String] {
def op(s1: String, s2: String) = s1 + s2
val zero = ""
}
in common.lecture10.Monoid
val foo = "foo"
val bar = "bar"
val foobar = monoidString.op(foo,bar)
println(foobar)
// prints "foobar"
in slideCode.lecture10.BasicExamples
String Monoid and foldLeft
val words: List[String] =
List("foo", "bar", "baz", "biz")
val foobarbazbiz: String =
words.foldLeft(monoidString.zero)
(monoidString.op)
println(foobarbazbiz)
// prints "foobarbazbiz"
in slideCode.lecture10.BasicExamples
Integers under addition form a monoid
val monoidIntAddition: Monoid[Int] =
new Monoid[Int] {
def op(i1: Int, i2: Int): Int = i1 + i2
val zero: Int = 0
}
in common.lecture10.Monoid
Monoids are the natural algebraic basis for fold operations.
val ints = (0 to 10).toList
val s = ints.foldLeft(monoidIntAddition.zero)
(monoidIntAddition.op)
println(s)
// 55
in slideCode.lecture10.BasicExamples
sealed trait FPOption[+A] {
def orElse[B >: A](otherOp: => FPOption[B]):
FPOption[B] =
this match {
case Some(get) => this
case None => otherOp
}
...
}
in common.lecture4.FPOption
Options form a Monoid using None and orElse.
def monoidOption[A]: Monoid[FPOption[A]] =
new Monoid[FPOption[A]] {
def op(a1: FPOption[A], a2: FPOption[A]):
FPOption[A] =
a1.orElse(a2)
val zero: FPOption[A] = None
}
in common.lecture10.Monoid
val listOptions = List(Some(6), None,
Some(8), Some(9),
None, Some(11))
val folded: FPOption[Int] =
listOptions.foldLeft(monoidOption[Int].zero)
(monoidOption[Int].op)
println(folded)
// prints "Some(6)"
Note that orElse is "left-biased." Which Some in the list would be returned if orElse were "right-biased"?
in slideCode.lecture10.BasicExamples
In the monoid of Options under orElse, None is the identity element. Why?
def monoidOption[A]: Monoid[FPOption[A]] =
new Monoid[FPOption[A]] {
def op(a1: FPOption[A], a2: FPOption[A]):
FPOption[A] =
a1.orElse(a2)
val zero: FPOption[A] = None
}
a1.orElse(None) = a1
and
None.orElse(a2) = a2
Endofunctions under composition form a monoid
def endoMonoid[A]: Monoid[A=>A] =
new Monoid[A=>A] {
def op(f1: A=>A, f2: A=>A): A=>A =
(a: A) => f2(f1(a))
def zero: A=>A =
(a: A) => a
}
in common.lecture10.Monoid
def dual[A](m: Monoid[A]): Monoid[A] =
new Monoid[A] {
def op(x: A, y: A): A = m.op(y, x)
val zero = m.zero
}
Dual of our Option Monoid
def monoidOption[A]: Monoid[FPOption[A]] = ...
def monoidOptionDual[A]: Monoid[FPOption[A]] =
dual(monoidOption[A])
Which Option from the List will println print?
val listOptions = List(Some(6), None, Some(8),
Some(9), None, Some(11))
val foldedByDual: FPOption[Int] =
listOptions.foldLeft(monoidOptionDual[Int].zero)
(monoidOptionDual[Int].op)
println(foldedByDual)
// ???
in slideCode.lecture10.BasicExamples
What is the implementation of op in the dual of an endofunction monoid?
def dualEndo[A]: Monoid[A=>A] =
dual[A=>A](endoMonoid[A])
dualEndo[A] {
def op(f1: A=>A, f2: A=>A): A=>A =
(a: A) => f1(f2(a))
def zero: A=>A =
(a: A) => a
}
Under what condition will m and dual(m) be the same?
A homomorphism between two monoids (M, *) and (N, •) is a function f : M → N such that
$$ f(x * y) = f(x) • f(y) for all x, y in M $$
$$ f(e_M) = e_N $$
where $e_M$ and $e_N$ are the identities on M and N respectively.
A bijective monoid homomorphism is called a monoid isomorphism.
Two monoids are said to be isomorphic if there is a monoid isomorphism between them.
Booleans under or form a Monoid
val booleanOr: Monoid[Boolean] =
new Monoid[Boolean] {
def op(x: Boolean, y: Boolean) = x || y
val zero = false
}
Booleans under and form a Monoid
val booleanAnd: Monoid[Boolean] =
new Monoid[Boolean] {
def op(x: Boolean, y: Boolean) = x && y
val zero = true
}
in slideCode.lecture10.BooleanIsomorphism
val booleans: List[Boolean] =
List(true, true, false, true, false)
val reducedOr = booleans.reduce(booleanOr.op)
println(reducedOr)
// true
val reducedAnd = booleans.reduce(booleanAnd.op)
println(reducedAnd)
// false
in slideCode.lecture10.BooleanIsomorphismExamples
// x && y == !((!x)||(!y))
// x || y == !((!x)&&(!y))
def booleanIsomorphism(mb: Monoid[Boolean]):
Monoid[Boolean] =
new Monoid[Boolean] {
def op(x: Boolean, y: Boolean) =
!mb.op(!x, !y)
def zero = !mb.zero
}
val booleanOr2: Monoid[Boolean] =
booleanIsomorphism(booleanAnd)
val booleanAnd2: Monoid[Boolean] =
booleanIsomorphism(booleanOr)
in slideCode.lecture10.BooleanIsomorphism
val booleans =
List(true, true, false, true, false)
val reducedOr2 =
booleans.foldLeft(booleanOr2.zero)
(booleanOr2.op)
// true
val reducedAnd2 =
booleans.foldLeft(booleanAnd2.zero)
(booleanAnd2.op)
// false
in slideCode.lecture10.BooleanIsomorphismExamples
def monoidFunction[A,B](monoidB: Monoid[B]):
Monoid[A => B] = new Monoid[A => B] {
def op(f1: A => B, f2: A => B): A => B =
(a: A) => {
val b1: B = f1(a)
val b2: B = f2(a)
val b3: B = monoidB.op(b1, b2)
b3
}
def zero: Function1[A,B] =
(a: A) => monoidB.zero
}
}
in common.lecture10.Monoid
def monoidProduct[A,B](mA: Monoid[A],
mB: Monoid[B]) =
new Monoid[(A,B)] {
def op(ab1: (A,B), ab2: (A,B)):
(A,B) = {
val a3: A = mA.op(ab1._1, ab2._1)
val b3: B = mB.op(ab1._2, ab2._2)
(a3, b3)
}
val zero: (A,B) = (mA.zero, mB.zero)
}
in common.lecture10.Monoid
We can use the monoid of integers under addition to count the elements of a List.
val listChars: List[Char] =
List('f','o','o','b','a','r')
val listCounts: List[Int] = listChars.map(_ => 1)
val charCount =
listCounts.foldRight(monoidIntAddition.zero)
(monoidIntAddition.op)
// 6
in slideCode.lecture10.CountingSimpleExamples
Refactor into a def so we can count the elements of any List
def countBasic[A](listA: List[A]): Int = {
val listCount: List[Int] = listA.map(_ => 1)
listCount.foldLeft(monoidIntAddition.zero)
(monoidIntAddition.op)
}
countBasic maps elements of type A to "counts" of type Int.
These Ints can then be reduced by the monoid of integers under addition.
in slideCode.lecture10.CountSimple
Generalize countBasic:
foldMap maps elements of type A to elements of type B.
These B elements can then be reduced by Monoid[B].
def foldMap[A, B](as: List[A], m: Monoid[B])
(f: A => B): B =
as.foldLeft(m.zero)((b, a) => m.op(b, f(a)))
in slideCode.lecture10.FoldList
val listChars: List[Char] =
List('f','o','o','b','a','r')
foldMap(listA, monoidIntAddition)((_: A) => 1)
// 6
in slideCode.lecture10.CountSimple and slideCode.lecture10.CountingExamples
The notion of a monoid is closely related to folding.
Folding necessarily implies a binary associative operation that has an initial value.
A fold is a specific type of catamorphism.
def foldMap[A,B](listA: List[A], mb: Monoid[B])
(f: Function1[A,B]): B = {
def g(b: B, a: A): B = mb.op(b, f(a))
foldLeft(listA)(mb.zero)(g)
}
def foldRight[A,B](listA: List[A])
(z: B)
(f: (A, B) => B): B = {
val g: A => (B => B) = f.curried
foldMap(listA, (endoMonoid[B]))(g)(z)
}
(a.avg, a.count) + (b.avg, b.count) =
((a.count*a.avg + b.count*b.avg)/
(a.count + b.count),
a.count + b.count)
def op(
a: (Double, Int),
b: (Double, Int)
): (Double, Int) = {
val cCount: Int = a._2 + b._2
val cAverage: Double =
(a._2.toDouble * a._1 + b._2.toDouble * b._1) / cCount
(cAverage, cCount)
}
val monoidAverageAndCount:
Monoid[(Double, Int)] =
new Monoid[(Double, Int)] {
def op(
a: (Double, Int),
b: (Double, Int)
): (Double, Int) = ...
def zero: (Double, Int) =
(0.0, monoidIntAddition.zero)
}
in slideCode.lecture10.Homomorphism
val monoidAverageAndCount:
Monoid[(Double, Int)] = ...
val doubles = (0 to 18).toList.
map(i => i.toDouble/10)
foldMap(doubles, monoidAverageAndCount)
{(d: Double) => (d, 1)}
// (0.8999999999999999,19)
in slideCode.lecture10.HomomorphismExample
The free monoid on a set A corresponds to lists of elements from A with concatenation as the binary operation.
A monoid homomorphism from the free monoid to any other monoid $(M,\cdot)$ is a function $f$ such that
$$
f(x_1 \dots x_n) = f(x_1) \cdot \dots \cdot f(x_n)
f() = e
$$
where $e$ is the identity on $M$.
Every such homomorphism corresponds to a map operation applying f to all the elements of a list, followed by a fold operation which combines the results using the binary operator.
This computational paradigm has inspired the MapReduce software framework.
object FoldList {
def foldMap[A,B](listA: List[A],
monoidB: Monoid[B])
(f: Function1[A,B]): B = ...
def foldLeft[A, B](as: List[A])
(z: B)
(f: (B, A) => B): B = ...
}
trait Foldable[F[_]] {
def foldLeft[A, B](fA: F[A])
(z: B)
(f: (B, A) => B): B = ...
}
| Lecture 10: Monoids | 1 |
|---|---|
| - | 2 |
| - | 3 |
| - | 4 |
| - | 5 |
| - | 6 |
| - | 7 |
| - | 8 |
| - | 9 |
| Option | 10 |
| - | 11 |
| - | 12 |
| Exercise | 13 |
| - | 14 |
| - | 15 |
| Every Monoid has a dual | 16 |
| Exercise | 17 |
| Exercise | 18 |
| Answer | 19 |
| Exercise | 20 |
| Monoid Isomorphism | 21 |
| - | 22 |
| - | 23 |
| - | 24 |
| - | 25 |
| Derived monoids | 26 |
| - | 27 |
| Example: Counting | 28 |
| - | 29 |
| foldMap | 30 |
| - | 31 |
| - | 32 |
| Example: foldRight | 33 |
| Example: Averaging | 34 |
| - | 35 |
| - | 36 |
| - | 37 |
| - | 38 |
| - | 39 |
| Generalize away from List | 40 |
| Generalize away from List | 41 |
| Table of Contents | t |
|---|---|
| Exposé | ESC |
| Full screen slides | e |
| Presenter View | p |
| Source Files | s |
| Slide Numbers | n |
| Toggle screen blanking | b |
| Show/hide slide context | c |
| Notes | 2 |
| Help | h |