Owning Invariants with Modules
If invariants matter, we must control how values are created and updated.
That control is ownership. Modules are one of the most important tools for enforcing it.
1. Why Ownership Is Necessary
Suppose we export a raw tree representation type. Clients can build object literals directly.
Even if all exported operations preserve the BST invariant, client code can still bypass those operations and inject invalid values.
Result:
- your functions are correct only for valid inputs
- clients can still provide invalid inputs
- the invariant is no longer reliable globally
2. Module Boundary as a Trust Boundary
A good module separates:
- internal representation (private)
- external API (public)
Only the module should be able to construct representation values directly. Clients should use constructors and operations that preserve invariants.
Design principle:
If clients cannot create invalid states, many bugs disappear.
3. Two Useful Strategies in This Course
Strategy A: Opaque/Branded Types
Use a representation internally, but export an opaque type so clients cannot fabricate values.
High-level pattern:
- Define internal representation type.
- Define exported type with a private brand.
- Export constructor and operations only.
- Keep representation constructors unexported.
This is the idea in the branded BST example: clients can hold BST values but cannot create fake ones by object literal.
Strategy B: Closure-Based Object API
Return object values containing operations, with representation captured in closure.
High-level pattern:
- Keep tree value in a local closure variable.
- Return methods like insert/get/has.
- Each method returns a new wrapped value or updates internal state (depending on style).
This bundles data and behavior, and naturally centralizes invariant maintenance.
4. API Design for Invariant Ownership
When designing a module API, ask:
How are valid values created? Usually through one constructor such as empty.
Which operations can change values? Only those that preserve invariant should be exported.
Is representation leaked? If yes, clients may bypass invariant-preserving operations.
Are contracts visible? Each exported function should state preconditions and guarantees.
5. Example Contract Sketches for a BST Module
empty:
Post: returns a valid empty BSTinsert(tree, key, value):
Pre: tree is a valid BST
Post:
- result is a valid BST
- result binds key to value
- all other bindings preservedget(tree, key):
Pre: tree is a valid BST
Post: returns absence/presence result consistent with tree contents6. Tradeoffs
Encapsulation and ownership add design complexity.
You may need:
- helper functions inside module
- wrappers and conversion helpers
- more deliberate API shape
But the payoff is significant:
- stronger guarantees for clients
- fewer invalid states
- cleaner reasoning about correctness
7. Bridge to OOP
Module ownership and class ownership are closely related.
In this section, ownership is enforced by module scope and exported functions. Later in OOP, ownership is often enforced by class methods and visibility modifiers.
Same idea, different mechanism:
- protect representation
- expose safe operations
- preserve invariant after each operation
Summary
Invariants do not maintain themselves. They require ownership.
Modules give us that ownership by controlling construction and update paths. If invariant correctness matters, module boundaries should be part of the design, not an afterthought.