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
, butList
has afold
method 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
AllTrue
accumulation 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.