Open Function Chaining
Milang supports open function chaining — when you redefine a function
that uses pattern matching (the -> arrow), your new alternatives are
automatically prepended to the existing definition. The previous definition
becomes the fallback for values that don’t match your new patterns.
This is milang’s answer to typeclasses: no new syntax, no special declarations.
Just define the same function again with new patterns.
How It Works
When a binding is redefined in the same scope and the new definition uses
pattern matching (->) without a catch-all wildcard (_), the compiler
chains the two definitions together. The new alternatives are tried first;
if none match, the old definition handles the value.
If the new definition includes a catch-all wildcard, it fully replaces the old definition — the catch-all means “I handle everything.”
-- Base: has catch-all
describe val = val -> _ = "unknown"
-- Extension: no catch-all — chains with base
describe val = val -> Circle = "a circle"; Rect = "a rectangle"
Now describe (Circle 5) returns "a circle" and describe 42 falls
through to the base, returning "unknown".
Extensible Builtins
Three core prelude functions are designed to be extended this way:
truthy
truthy is the universal boolean coercion point. It is called internally
by if, guards, not, &&, and ||. The prelude default treats 0,
0.0, "", False, Nil, and Nothing as falsy (returns 0);
everything else is truthy (returns 1).
Extend truthy to teach the language how your types behave in boolean
contexts:
Result = {Err msg; Ok val}
truthy val = val -> Err = 0; Ok = 1
main world =
world.io.println (toString (if (Ok 42) "yes" "no"))
world.io.println (toString (if (Err "oops") "yes" "no"))
world.io.println (toString (not (Err "fail")))
yes
no
1
toString
toString converts values to their string representation. The prelude
handles True, False, Nil, Nothing, and Just symbolically, then
falls through to _toString (the C-level primitive) for ints, floats, and
strings. Extend it for your own types:
Pair = {Pair fst snd}
toString val = val -> Pair = "(" + toString val.fst + ", " + toString val.snd + ")"
main world =
world.io.println (toString (Pair 1 2))
world.io.println (toString (Pair "hello" True))
(1, 2)
(hello, True)
eq
eq is the extensible equality function. The prelude default falls through
to structural ==. The contains function uses eq, so extending eq
automatically affects list membership checks:
Card = {Card rank suit}
eq a b = a -> Card = a.rank == b.rank
main world =
world.io.println (toString (eq (Card 10 "H") (Card 10 "S")))
world.io.println (toString (contains [Card 10 "H", Card 5 "D"] (Card 10 "S")))
1
0
Scope Chaining
Open chaining works across scopes. A redefinition inside a function body
(a With block) chains with the outer scope’s definition, not just
same-scope duplicates. Multiple levels of chaining compose naturally:
Result = {Err msg; Ok val}
truthy val = val -> Err = 0; Ok = 1
main world =
Severity = {Low; High}
truthy val = val -> Low = 0; High = 1
-- truthy now handles Result, Severity, AND all prelude types
world.io.println (toString (truthy (Ok 1)))
world.io.println (toString (truthy (Err "x")))
world.io.println (toString (truthy High))
world.io.println (toString (truthy Low))
world.io.println (toString (truthy Nothing))
1
0
1
0
0
Writing Extensible Functions
To make your own functions extensible, follow this pattern:
- Define a base with a catch-all wildcard — this provides default behavior.
- Extend without a catch-all — your new alternatives are prepended; the base stays as fallback.
-- Base definition (has catch-all)
describe val = val -> _ = "something"
-- Extension (no catch-all — chains)
Shape = {Circle radius; Rect width height}
describe val = val -> Circle = "a circle"; Rect = "a rectangle"
main world =
world.io.println (describe (Circle 5))
world.io.println (describe (Rect 3 4))
world.io.println (describe 42)
a circle
a rectangle
something
If you include a catch-all in an extension, it fully replaces the base — use this when you genuinely want to override all behavior.