Getting started with fp-ts: Monoid
Giulio Canti
Posted on March 16, 2019
In the last post we saw that a Semigroup
captures the concept of "merging" values (via concat
). A Monoid
is any Semigroup
that happens to have a special value which is "neutral" with respect to concat
.
Type class definition
As usual in fp-ts
the type class Monoid
, contained in the fp-ts/Monoid
module, is implemented as a TypeScript interface
, where the neutral value is named empty
import { Semigroup } from 'fp-ts/Semigroup'
interface Monoid<A> extends Semigroup<A> {
readonly empty: A
}
The following laws must hold
-
Right identity:
concat(x, empty) = x
, for allx
inA
-
Left identity:
concat(empty, x) = x
, for allx
inA
Whichever side of concat
we put the value empty
, it must make no difference to the value.
Note. If an empty
value exists then is unique.
Instances
Most of the semigroups we saw in the previous post are actually monoids
/** number `Monoid` under addition */
const monoidSum: Monoid<number> = {
concat: (x, y) => x + y,
empty: 0
}
/** number `Monoid` under multiplication */
const monoidProduct: Monoid<number> = {
concat: (x, y) => x * y,
empty: 1
}
const monoidString: Monoid<string> = {
concat: (x, y) => x + y,
empty: ''
}
/** boolean monoid under conjunction */
const monoidAll: Monoid<boolean> = {
concat: (x, y) => x && y,
empty: true
}
/** boolean monoid under disjunction */
const monoidAny: Monoid<boolean> = {
concat: (x, y) => x || y,
empty: false
}
You may wonder if all semigroups are also monoids. That's not the case, as a counterexample consider the following semigroup
const semigroupSpace: Semigroup<string> = {
concat: (x, y) => x + ' ' + y
}
We can't find an empty
value such that concat(x, empty) = x
.
Let's write some Monoid
instances for more complex types. We can build a Monoid
instance for a struct like Point
type Point = {
x: number
y: number
}
if we can provide a Monoid
instance for each field
import { getStructMonoid } from 'fp-ts/Monoid'
const monoidPoint: Monoid<Point> = getStructMonoid({
x: monoidSum,
y: monoidSum
})
We can go on and feed getStructMonoid
with the instance just defined
type Vector = {
from: Point
to: Point
}
const monoidVector: Monoid<Vector> = getStructMonoid({
from: monoidPoint,
to: monoidPoint
})
Folding
When using a monoid instead of a semigroup, folding is even simpler: we don't need to explicitly provide an initial value (the implementation can use the monoid's empty
value for that)
import { fold } from 'fp-ts/Monoid'
fold(monoidSum)([1, 2, 3, 4]) // 10
fold(monoidProduct)([1, 2, 3, 4]) // 24
fold(monoidString)(['a', 'b', 'c']) // 'abc'
fold(monoidAll)([true, false, true]) // false
fold(monoidAny)([true, false, true]) // true
Monoids for type constructors
We already know that given a semigroup instance for A
we can derive a semigroup instance for Option<A>
.
If we can find a monoid instance for A
then we can derive a monoid instance for Option<A>
(via getApplyMonoid
) which works like this
x | y | concat(x, y) |
---|---|---|
none | none | none |
some(a) | none | none |
none | some(a) | none |
some(a) | some(b) | some(concat(a, b)) |
import { getApplyMonoid, some, none } from 'fp-ts/Option'
const M = getApplyMonoid(monoidSum)
M.concat(some(1), none) // none
M.concat(some(1), some(2)) // some(3)
M.concat(some(1), M.empty) // some(1)
We can derive two other monoids for Option<A>
(for all A
)
1) getFirstMonoid
...
Monoid returning the left-most non-None
value
x | y | concat(x, y) |
---|---|---|
none | none | none |
some(a) | none | some(a) |
none | some(a) | some(a) |
some(a) | some(b) | some(a) |
import { getFirstMonoid, some, none } from 'fp-ts/Option'
const M = getFirstMonoid<number>()
M.concat(some(1), none) // some(1)
M.concat(some(1), some(2)) // some(1)
2) ...and its dual: getLastMonoid
Monoid returning the right-most non-None
value
x | y | concat(x, y) |
---|---|---|
none | none | none |
some(a) | none | some(a) |
none | some(a) | some(a) |
some(a) | some(b) | some(b) |
import { getLastMonoid, some, none } from 'fp-ts/Option'
const M = getLastMonoid<number>()
M.concat(some(1), none) // some(1)
M.concat(some(1), some(2)) // some(2)
As an example, getLastMonoid
can be useful for managing optional values
import { Monoid, getStructMonoid } from 'fp-ts/Monoid'
import { Option, some, none, getLastMonoid } from 'fp-ts/Option'
/** VSCode settings */
interface Settings {
/** Controls the font family */
fontFamily: Option<string>
/** Controls the font size in pixels */
fontSize: Option<number>
/** Limit the width of the minimap to render at most a certain number of columns. */
maxColumn: Option<number>
}
const monoidSettings: Monoid<Settings> = getStructMonoid({
fontFamily: getLastMonoid<string>(),
fontSize: getLastMonoid<number>(),
maxColumn: getLastMonoid<number>()
})
const workspaceSettings: Settings = {
fontFamily: some('Courier'),
fontSize: none,
maxColumn: some(80)
}
const userSettings: Settings = {
fontFamily: some('Fira Code'),
fontSize: some(12),
maxColumn: none
}
/** userSettings overrides workspaceSettings */
monoidSettings.concat(workspaceSettings, userSettings)
/*
{ fontFamily: some("Fira Code"),
fontSize: some(12),
maxColumn: some(80) }
*/
Next post Introduction to property based testing
Posted on March 16, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024