Function Monoids
Earlier in this series, I talked about how tuple monoids.
This time it is monoids for functions.
Inductive Monoid for functions
If a type B has a Monoid instance, then so does any function A => B. When combining, the Semigroup instance gathers the output of two functions and combines the values using the underlying Monoid[B].
trait Function1Semigroup[A, B] extends Semigroup[A => B] {
implicit def B: Semigroup[B]
override def combine(x: A => B, y: A => B): A => B =
(a: A) => B.combine(x(a), y(a))
}
trait Function1Monoid[A, B]
extends Function1Semigroup[A, B] with Monoid[A => B] {
implicit def B: Monoid[B]
val empty: A => B =
(_: A) => B.empty
}Example
val int2IntFunctions: List[Int => Int] =
List(
(i: Int) => i * i,
(i: Int) => i * 5,
(i: Int) => i + 6
)
(0 to 10).foreach {
i =>
val total: Int => Int = int2IntFunctions.foldMap(identity)
println(s"$i => ${total(i)}")
}Note: this code would ideally be written using
Foldable.fold, ieint2IntFunctions.foldMap, butListhas afoldmethod that doesn’t know about monoids. Instead,int2IntFunctions.foldMap(identity)is equivalent toint2IntFunctions.fold.
Output is
The function monoid combines the three functions in int2IntFunctions, and returning a function that will apply each of those functions in turn to an input value, and combine the three outputs using Monoid[Int], which sums them. For example, value 4 yields 16 + 20 + 10, ie 46.
Example variant
This time, each function maps to a Max rather than Int. Monoid[Max] accumulates the maximum value, and is defined as
final case class Max(i: Int) extends AnyVal
implicit val intMaxMonoid =
new Monoid[Max] {
override def empty: Max = Max(Int.MinValue)
override def combine(x: Max, y: Max): Max = if (x.i > y.i) x else y
}This time
val int2MaxFunctions: List[Int => Max] =
List(
(i: Int) => Max(i * i),
(i: Int) => Max(i * 5),
(i: Int) => Max(i + 6)
)
(0 to 10).foreach {
i =>
val bestf: Int => Max = int2MaxFunctions.foldMap(identity)
val best: Max = bestf(i)
println(s"$i => ${best}")
}yields
0 => Max(6)
1 => Max(7)
2 => Max(10)
3 => Max(15)
4 => Max(20)
5 => Max(25)
6 => Max(36)
7 => Max(49)
8 => Max(64)
9 => Max(81)
10 => Max(100)Behaviour is similar to the previous, however, this time the monoid just selects the largest value returned by any of the functions. From input 0 to 1, the equation i + 6 dominates. From 2 to 5, i * 5 takes over. Finally, from 6 onwards, i * i wins.
Uses
This kind of behaviour can be used to check a collection of predicates, accumulating their boolean results using a AllTrue or AnyTrue monoid. (These are called All and Any in Haskell).
Note: these monoidal predicates do not provide short-circuiting behaviour. They apply all predicates even if the result has already been decided, such as an
AllTrueaccumulation that has already encountered afalse.
Despite the short-circuiting limitation, such monoidal accumulations of functions can provide a simple and expressive way of dynamically configuring application runtime behaviour.