Monoids

Monoids

Monoids are algebraic structures that have an identity element and an associative binary operation. So what is a monoid? It’s a …

trait Monoid[A] {
  def mempty: A
  def mappend(a0: A, a1: A): A
}
defined trait Monoid

… and the associativity and identity rules which cannot be enforced by Scala 😞

Okay, but what can we do with it? Well the simplest thing to do is if we have a collection (monad) of monoids, we can apply the monoid’s binary operation recursively over the elements of the collection. That means we can fold over the elements, starting from the monoid’s identity and applying the binary operation recursively.

import Predef.{implicitly => as} // `implicitly` is just too long :)
// Haskell calls it mconcat, but it's also known as reduce
def mconcat[A: Monoid](xs: A*): A = xs.fold(as[Monoid[A]].mempty)(as[Monoid[A]].mappend)
import Predef.{implicitly => as}
defined function mconcat

Now, create some instances, so we can try it out!

// Addition on numbers is a monoid
def add[A: Numeric] = new Monoid[A] {
  def mempty = as[Numeric[A]].zero
  def mappend(a0: A, a1: A) = as[Numeric[A]].plus(a0, a1)
}

// as well as multiplication
def multiply[A: Numeric] = new Monoid[A] {
  def mempty = as[Numeric[A]].one
  def mappend(a0: A, a1: A) = as[Numeric[A]].times(a0, a1)
}
defined function madd
defined function multiply

Note on syntax: our context bound acts the same way as it were an implicit evidence. So

  def mconcat[A: Monoid](xs: A*): A

and

  def mconcat[A](xs: A*)(implicit ev: Monoid[A]): A

are the same, the former being syntactic sugar. Which means…

// ... you specify your instance this way.
val sumResult = mconcat(1,2,4)(add)

assert(mconcat(1,2,4)(add) == 7)

// okay but call this thing sum:
def sum[A: Numeric](xs: A*) = mconcat[A](xs:_*)(add)

assert(sum(1,2,4) == 7)

assert(mconcat(1,2,4)(multiply) == 8)

//name the product fold as well
def product[A: Numeric](xs: A*) = mconcat[A](xs:_*)(multiply)

assert(product(1,2,4) == 8)

// Gr8! Let's try out if the associativity rule really holds for a
// specific case
def mconcatRight[A: Monoid](xs: A*): A =
  xs.foldRight(as[Monoid[A]].mempty)(as[Monoid[A]].mappend)

// ((1 + 2) + 4) = (1 + (2 + 4))
assert(mconcat(1,2,4)(add) == mconcatRight(1,2,4)(add))
sumResult: Int = 7
defined function sum
defined function product
defined function mconcatRight

Now, let’s concatenate lists! More generally, let’s do it for all things that are iterable!

Sure, simple as a cake!

def multiply[A: Iterable] = new Monoid[A] {
  def mempty = as[Iterable[A]].empty
  def mappend(a0: A, a1: A) = as[Iterable[A]].concat(a0, a1)
}

Well, well, not so fast. There isn’t a typeclass for standard Scala collection iterables. Instead we must use CanBuildFrom for these.

import scala.language.higherKinds

// this is a bit nasty. we don't have a typeclass for iterable structures in scala, we have to use
// CanBuildFrom instead
import scala.collection.Iterable, scala.collection.generic.CanBuildFrom
def concat[A, I <% Iterable[A]](implicit bf: CanBuildFrom[I, A, I]) = new Monoid[I] {
  def mempty = bf().result()
  def mappend(a0: I, a1: I) = bf.apply().++=(a0).++=(a1).result()
}
import scala.language.higherKinds
import scala.collection.Iterable, scala.collection.generic.CanBuildFrom
defined function concat

Basically, we restrict our monoid to be a implicitly convertible to an Iterable. Then we use an implicit builder for this collection. Another method would be to use a lower type bound instead of a lower view bound like this:

def concat[A, I <: Iterable[A]](implicit bf: CanBuildFrom[I, A, I]): Monoid[i]

The view bound has the advantage over a type bound to be able to work with String, which doesn’t subclass nor Iterable, nor anything like it (it’s just the plain java.lang.String), but Scala has an implicit conversion from it to Seq.

// works for all sequences as long both all arguments are exactly T[U]
val resultSeq = mconcat(Seq(1,2,3), Seq(4,5,6))(concat)
val resultList = mconcat(List(1,2,3), List(4,5,6))(concat)

import scala.collection._
val mutableSeq = mconcat(mutable.Seq(1,2,3), mutable.Seq(4,5,6))(concat)

// even sets!
val resultSet = mconcat(Set(1,1,3), Set(4,4,6))(concat)
assert(mconcat(Set(1,1,3), Set(4,4,6))(concat) == mconcatRight(Set(1,1,3), Set(4,4,6))(concat))
resultSeq: Seq[Int] = List(1, 2, 3, 4, 5, 6)
resultList: List[Int] = List(1, 2, 3, 4, 5, 6)
import scala.collection._
mutableSeq: collection.mutable.Seq[Int] = ArrayBuffer(1, 2, 3, 4, 5, 6)
resultSet: collection.Set[Int] = Set(1, 3, 4, 6)

That’s great! Does it work for strings? With view bounds? Yes.

assert(mconcat("abc", "def")(concat) == "abcdef")

def join(xs: String*) = mconcat(xs:_*)(concat)

assert(join("abc", "def") == "abcdef")
defined function join

And that’s all I wanted to say. Next it’ll be something on applicative functors I guess.

(++) <$> ["Boom", "Bang"] <*> ["?", "!"]
println("Bye!")
Bye!