Laws¶
Algebraic laws describe relationships between operations, such as mapping an identity function leaving a value unchanged. They are part of the contract under the assumptions below. Tests check these relationships for representative types; implementing a trait alone does not prove that an instance satisfies its laws.
Assumptions¶
The equations below use ordinary mathematical equality. They hold for well-typed operations whose callbacks are pure and terminating, and whose values have equivalent native ownership and copy behavior on both sides of the equation. Ownership transfers are implicit in the notation.
Callbacks with effects are allowed everywhere in the library, but for them the contract is the specified invocation order and count, not an equation. Tests of stateful callbacks check the observable trace of calls instead.
Functions¶
identity(x) = x
compose(identity, f)(x) = f(x)
compose(f, identity)(x) = f(x)
compose(h, compose(g, f))(x) = compose(compose(h, g), f)(x)
pipe(x, f, g) = g(f(x))
Functors and monads¶
For every instance I (IdentityFamily, OptionalFamily, ResultFamily[E],
ListFamily and the transformers):
map[I](identity, v) = v
map[I](g, map[I](f, v)) = map[I](compose(g, f), v)
flat_map[I](f, pure[I](x)) = f(x)
flat_map[I](pure[I], m) = m
flat_map[I](g, flat_map[I](f, m)) = flat_map[I](x -> flat_map[I](g, f(x)), m)
For ResultFamily[E] the laws hold on both cases: an Err passes through every
operation unchanged, because callbacks never run on the inactive case. The
instance's operations are the Result methods, so map[ResultFamily[E]](f, r) is
r.map(f) and flat_map[ResultFamily[E]](f, r) is r.flat_map(f).
Traversal preserves the source's shape and callback order, and stops at the first failure or absence without pulling another element; these are tested as observable behavior rather than as equations.
Monoids¶
combine(empty(), a) = a
combine(a, empty()) = a
combine(combine(a, b), c) = combine(a, combine(b, c))
StringMonoid and ListMonoid[A] satisfy these because they are standard
concatenation; the tests check their empty and combine values. A third-party
monoid is responsible for its own laws. WriterT combines logs in execution
order.
Matching¶
fp.match(C(fields), clauses) = the first clause for C, or a catch-all, that applies
Exactly one clause produces the result, and declining guards run in clause
order before it. The value matched is unchanged: a match borrows its subject,
whether a Node, a Choice, a Result or an Optional.
Folds¶
For finite inputs and compatible value semantics:
fold_left(step, init, chain(xs, ys)) = fold_left(step, fold_left(step, init, xs), ys)
This law does not require step to be associative, and the tests use
subtraction to make that visible. Reassociating a reduction would need
additional assumptions and is never done implicitly.
Lazy adapters obey the list laws of their eager counterparts: map preserves
order and length, map(g, map(f, xs)) equals map(compose(g, f), xs), filter
keeps the relative order of accepted elements, and flat_map equals map
followed by flatten.
Where the laws are tested¶
| Area | Tests |
|---|---|
| Pipelines and composition | tests/laws/test_callable_pipeline_inferred_laws.mojo, test_foundation_qualification_laws.mojo |
| Folds | tests/laws/test_fold_laws.mojo |
| Lazy iteration | tests/laws/test_iteration_laws.mojo |
| Result | tests/laws/test_result_laws.mojo |
| Matching | tests/runtime/test_match_single.mojo, test_match_inline.mojo, test_match_tuples.mojo |
| Algebra instances | tests/runtime/test_algebra.mojo (functor and monad laws, derived operations, monoid values) and the test_algebra_* runtime tests (traversal order, pulls and layering) |
A generic "map over every field of matching type" is deliberately absent: reflection alone does not say which fields a lawful mapping should change, so a mapping needs a declared semantic parameter that preserves constructor structure and unrelated fields.