CONCEPT 13 · DESIGN

Laws as Design Tools

"Write equations, not just examples — laws find bugs nothing else catches."

lawsproperty testingcorrectness
Plain English

Every meaningful abstraction has laws — equations that any correct implementation must satisfy. Writing these down and expressing them as property-based tests is the most powerful verification technique in software. Laws catch bugs that types, code review, and hand-written tests all miss — including bugs that cause deadlocks and race conditions.

Analogy

Physics. F = ma is not documentation — it's a constraint. Any physical model that violates it is wrong, period. Software laws work identically: if your map implementation violates map(x)(identity) === x, your implementation is wrong regardless of whether your unit tests pass. Laws are executable specifications, not suggestions.

You already know this
Idempotency in HTTP: PUT /users/1 called twice gives the same result as once — that's a lawDatabase ACID: after a rollback, state must equal state before the transaction — that's a lawCache transparency: with-cache and without-cache must produce the same observable result — lawJSON round-trip: JSON.parse(JSON.stringify(x)) must equal x — a law you can testSort stability: equal elements must appear in their original relative order — a law
Code Example
JavaScript (fast-check)
import fc from 'fast-check';

// FUNCTOR LAWS — any correct functor must satisfy both
// Law 1: map with identity must change nothing
test('functor: identity law', () => {
  fc.assert(fc.property(fc.array(fc.integer()), arr => {
    const result = arr.map(x => x);
    return JSON.stringify(result) === JSON.stringify(arr);
  }));
});

// Law 2: two maps must equal one map with composed functions
test('functor: composition law', () => {
  const double = (x: number) => x * 2;
  const addOne = (x: number) => x + 1;
  fc.assert(fc.property(fc.array(fc.integer()), arr => {
    const twoMaps = arr.map(double).map(addOne);
    const oneMap  = arr.map(x => addOne(double(x)));
    return JSON.stringify(twoMaps) === JSON.stringify(oneMap);
  }));
});

// THE DEADLOCK THE BOOK FOUND WITH LAWS:
// Law: fork(computation) should equal computation
// (forking shouldn't change the result)
//
// Writing this law and testing it revealed:
// If fork blocks a thread waiting for a result,
// and the thread pool has exactly 1 thread,
// the thread is blocked waiting for itself → DEADLOCK
//
// This bug was invisible to: code review, unit tests, type checking.
// Only the law test exposed it by generating the edge case.

// ROUND-TRIP LAW — serialize/deserialize must be inverses
test('JSON round-trip law', () => {
  fc.assert(fc.property(fc.jsonValue(), data => {
    return JSON.stringify(JSON.parse(JSON.stringify(data)))
      === JSON.stringify(data);
  }));
});
Apply when
Designing any abstraction (cache, queue, event bus) — write its laws before implementing
Serialization/deserialization — the round-trip law: decode(encode(x)) === x
Any commutative operation — order(a, b) === order(b, a)
API idempotency — verify with property tests across random inputs
Any time you have a rule that "should always be true" — express it as a law and test it automatically
Check Your Understanding
Your map() passes the identity law: map(x)(v => v) === x. But it fails the composition law: map(map(x)(f))(g) !== map(x)(v => g(f(v))). What does this tell you?