Lecture 10: Monoids

Presenter Notes

"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."



https://en.wikipedia.org/wiki/Monoid

Presenter Notes

trait Monoid[A] {
  def op(a1: A, a2: A): A
  def zero: A
}

Presenter Notes

Note: Our usual combinators are not inside the Monoid trait.

Every lecture so far has repeated the pattern:

  • Give a container, like Option, or function, like Rand
  • implement the usual combinators on it

    • unit
    • flatMap
    • map
    • map2

Monoid is a departure from this pattern.

Presenter Notes

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

Presenter Notes

val foo = "foo"
val bar = "bar"
val foobar = monoidString.op(foo,bar)
println(foobar)
// prints "foobar"





in slideCode.lecture10.BasicExamples

Presenter Notes

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

Presenter Notes

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

Presenter Notes

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

Presenter Notes

Option

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

Presenter Notes

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

Presenter Notes

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

Presenter Notes

Exercise

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
  }

Presenter Notes

Presenter Notes

a1.orElse(None) = a1

and

None.orElse(a2) = a2

Presenter Notes

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

Presenter Notes

Every Monoid has a dual

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])

Presenter Notes

Exercise

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

Presenter Notes

Exercise

What is the implementation of op in the dual of an endofunction monoid?

def dualEndo[A]: Monoid[A=>A] =
  dual[A=>A](endoMonoid[A])

Presenter Notes

Presenter Notes

Answer

dualEndo[A] {
  def op(f1: A=>A, f2: A=>A): A=>A =
    (a: A) => f1(f2(a))

  def zero: A=>A = 
    (a: A) => a
}

Presenter Notes

Presenter Notes

Exercise

Under what condition will m and dual(m) be the same?

Presenter Notes

Monoid Isomorphism

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.





https://en.wikipedia.org/wiki/Monoid#Monoid_homomorphisms

Presenter Notes

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

Presenter Notes

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

Presenter Notes

// 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

Presenter Notes

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

Presenter Notes

Derived monoids

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

Presenter Notes

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

Presenter Notes

Example: Counting

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

Presenter Notes

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

Presenter Notes

foldMap

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

Presenter Notes

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

Presenter Notes

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.

Presenter Notes

Example: foldRight

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)
}

Presenter Notes

Example: Averaging

(a.avg, a.count) + (b.avg, b.count) =
   ((a.count*a.avg + b.count*b.avg)/
      (a.count + b.count),
    a.count + b.count)

Presenter Notes

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)
}

Presenter Notes

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

Presenter Notes

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

Presenter Notes

Presenter Notes

Presenter Notes

Presenter Notes

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$.

Presenter Notes

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.

Presenter Notes

Generalize away from List

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 = ...
}

Presenter Notes

Generalize away from List

trait Foldable[F[_]] {

  def foldLeft[A, B](fA: F[A])
                    (z: B)
                    (f: (B, A) => B): B = ...
}

Presenter Notes

Presenter Notes