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:
@Testvoid monadLeftIdentity() { Fn1<Integer, Option<Integer>> f = x -> Option.some(x * 2); assertThat(Option.some(4).flatMap(f)).isEqualTo(f.apply(4));}
@Testvoid 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);}
@Testvoid 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)));}@Testvoid 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);}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.