Series overview
Part 10 of 2050% complete
2026-04-14•20 min read

The monad laws, tested

Part 9 gave you flatMap; this chapter gives you the reason to trust it. Three laws define a monad, and each one translates directly into a refactoring guarantee: what you may inline, what you may extract, and what regroupings are safe.

The three laws

Left identity. pure(a).flatMap(f) = f(a) — wrapping a value then binding is the same as applying the function directly. In jfp terms, Option.some(a).flatMap(f) is f(a). This licenses extracting a chain’s first step into pure without changing behavior.

Right identity. m.flatMap(pure) = m — binding a context to “just wrap it again” is a no-op. some(4).flatMap(Option::some) gives back some(4).

Associativity. m.flatMap(f).flatMap(g) = m.flatMap(x → f(x).flatMap(g)) — flattening left-to-right or right-to-left produces the same chain. This is the law that makes a long pipeline refactorable: any contiguous segment can be extracted into a named function.

Proven, not asserted

These are ordinary JUnit tests over the verified jfp types — they ran green in the project build for this series:

laws/OptionLawsTest.java
@Test
void monadLeftIdentity() {
Fn1<Integer, Option<Integer>> f = x -> Option.some(x * 2);
assertThat(Option.some(4).flatMap(f)).isEqualTo(f.apply(4));
}
@Test
void monadRightIdentity() {
Option<Integer> some = Option.some(4);
Option<Integer> none = Option.none();
assertThat(some.flatMap(Option::some)).isEqualTo(some);
assertThat(none.flatMap(Option::some)).isEqualTo(none);
}
@Test
void monadAssociativity() {
Fn1<Integer, Option<Integer>> f = x -> Option.some(x + 1);
Fn1<Integer, Option<Integer>> g = x -> x > 10 ? Option.none() : Option.some(x * 3);
Option<Integer> m = Option.some(4);
assertThat(m.flatMap(f).flatMap(g))
.isEqualTo(m.flatMap(x -> f.apply(x).flatMap(g)));
}
laws/ResultLawsTest.java
@Test
void monadRightIdentity() {
Result<Integer, String> success = Result.success(4);
Result<Integer, String> failure = Result.failure("boom");
assertThat(success.flatMap(Result::success)).isEqualTo(success);
assertThat(failure.flatMap(Result::success)).isEqualTo(failure);
}

equivalent by left identity + associativity

pure(a)

flatMap f

flatMap g

a

f(a)

flatMap g

equivalent by left identity + associativity

pure(a)

flatMap f

flatMap g

a

f(a)

flatMap g

What a law violation looks like

Laws fail when implementations cheat. A Result.flatMap that logged each bind call would still return the right value but would break right identity — m.flatMap(pure) now performs a log m alone did not. A flatMap that retried internally would break associativity’s spirit by making regrouping observable. The laws are a contract about meaning, not just output: when every context in the library honors them, x.flatMap(f).flatMap(g) and x.flatMap(a -> f(a).flatMap(g)) are interchangeable in a review diff without re-reading the semantics.

Honest caveat: property-based tests would exercise the laws over hundreds of generated inputs; jqwik is the standard choice on the JVM. These JUnit tests are demonstrations, not proofs — they pin the contract on representative values. For a library you intend to publish, promote them to property tests; for learning, three passing cases teach more than three hundred opaque ones.

JavaFunctional Programming

Type to search the site.

↑↓ navigate⏎ openPowered by Pagefind