"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 Rand
implement the usual combinators on it
unit
flatMap
map
map2
Monoid
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 |