Contents
- 1. Guarded defensive programming
- 2. Treat errors as values via the Result type
- 3. Typical list operations (recursion)
- 4. Accumulator-style recursion (tail-recursive flavor)
- 5. Readable transformation chains via pipe
- 6. Point-free style with function composition
- 7. Cap-passing (capability) pattern
- 8. Immutable record update
- 8.5. "Update only one element" inside a collection of records
- 8.6. Chain immutable updates with same-name rebinding
- 9. List construction idioms
- 10. Destructuring nested structures
- 11. Threading "next position" through a parser
- 12. Debug print is `show`
- 13. Side-effect loops via `iter_n`
- 14. Ordered side effects with block expressions
- 14.5. Phase 36 sugar idioms
- Flatten nested matches with `?` / `?!`
- Combine filter + map with list comprehension
- `for-in-do` for side-effect loops; `while-do` for loops inside fn bodies
- Single-shot `Option` extraction with `if let`
- Simpler log output via string interpolation
- Range + ::, <|, @@ for readable expressions
- 15. Using polymorphic helpers
- Anti-patterns / gotchas
- 1. Passing the literal `-1` as a function argument
- 2. ~~Verbose single-char comparisons~~ → fixed (char literals + match)
- 3. ~~Match exhaustiveness is checked at runtime~~ → Phase 1 added warnings
- 4. Record update needs the base's type
- 5. Top-level fn names collide with libc / libm / C keywords (C codegen)
- 6. The empty list literal `[]` is polymorphic `'a list` and breaks codegen
- 7. `Map[K, V]` only takes two args; you need `Map[R, K, V]` (3 args)
- 8. ~~Closure capture leaks for inner-lifted fns through anonymous Fun~~ ✅
- 9. `substring s start end` — `end` is an exclusive position, not a length
- 10. Single-arg builtins (`int_of_str` etc.) used in value position become unbound in C codegen
- 12. Record literals inside list literals require `Type {…}` (the type prefix)
- 11. Destructure 3-tuples or larger via `let (a, b, c) = ...`, not `fst`/`snd`
- See also
Patterns / cookbook (mere)
Common idioms encountered when actually writing Mere programs.
1. Guarded defensive programming
let safe_div = fn (a: int, b: int) ->
if b == 0 then fail "div by zero"
else a / b;
fail is polymorphic (str -> 'a), so it unifies correctly at branch merges. assert (cond) "msg" serves a similar purpose.
2. Treat errors as values via the Result type
type ('a, 'b) result = Ok of 'a | Err of 'b;
let parse_age = fn (s: str) ->
let n = try_or (fn () -> int_of_str s) (- 1) in
if n < 0 then Err ("invalid: " ++ s)
else if n > 150 then Err "unrealistic age"
else Ok n;
match parse_age "42" with
| Ok n -> "age: " ++ show n
| Err e -> "error: " ++ e
Catch panics with try_or and repack them into a Result type.
3. Typical list operations (recursion)
type 'a list = Nil | Cons of 'a * 'a list;
// sum
let rec sum = fn xs -> match xs with
| [] -> 0
| [h, ...t] -> h + sum t;
// length
let rec len = fn xs -> match xs with
| [] -> 0
| [_, ...t] -> 1 + len t;
// map (hand-written)
let rec map = fn (f, xs) -> match xs with
| [] -> []
| [h, ...t] -> Cons (f h, map f t);
map (fn x -> x * x) [1, 2, 3, 4] // [1, 4, 9, 16]
Note: Phase 36 added general-purpose list helpers (list_filter / list_map / list_fold / list_sum / list_max etc.) to the prelude (34 entries total). See stdlib-reference.md for details.
4. Accumulator-style recursion (tail-recursive flavor)
When direct recursion accumulates in reverse, finish with rev:
let rec rev_aux = fn (xs, acc) -> match xs with
| [] -> acc
| [h, ...t] -> rev_aux t (Cons (h, acc));
let rev = fn xs -> rev_aux xs [];
let rec map_acc = fn (f, xs, acc) -> match xs with
| [] -> rev acc
| [h, ...t] -> map_acc f t (Cons (f h, acc));
5. Readable transformation chains via pipe
" 42 "
|> str_trim // "42"
|> int_of_str // 42
|> incr // 43
|> show // "43"
|> is left-associative and lowest precedence, so it works above let/if without parens.
6. Point-free style with function composition
let show_inc = str_of_int << (fn x -> x + 1);
show_inc 41 // "42"
let process = str_trim >> to_upper >> (str_replace " " "_");
process " hello world " // "HELLO_WORLD"
<< is right-to-left; >> is left-to-right. Both right-associative.
7. Cap-passing (capability) pattern
Bundle multiple dependencies in one go:
signature ctx = (db: int, log: int);
let save_user = fn (...ctx, name: str) ->
// db and log come into scope
print ("saving " ++ name ++ " (db=" ++ show db ++ ", log=" ++ show log ++ ")");
let log_event = fn (...ctx, evt: str) ->
print ("event: " ++ evt ++ " (log=" ++ show log ++ ")");
// Call sites unroll via currying
save_user 100 10 "alice";
log_event 100 10 "logged-in";
signature declarations expand at parse time, so the call sites are ordinary curried applications.
8. Immutable record update
type Config = { name: str, port: int, debug: bool };
let default_cfg = Config { name = "app", port = 8080, debug = false };
let dev_cfg = { default_cfg | debug = true, port = 3000 };
let prod_cfg = { default_cfg | name = "app-prod" };
The base record doesn't change (immutable). Multiple fields can be updated at once.
8.5. "Update only one element" inside a collection of records
Vec / OwnedVec are append-only and records are immutable, so there's no direct way to mutate a specific record in a collection. Instead, use `vec_map` + `{ t | f = v }` to build a "new collection with conditional elements replaced":
type Task = { id: int, text: str, done: bool };
let mark_done = fn tasks -> fn target_id ->
region R {
let src = owned_vec_to_vec tasks in
let dst = vec_map src (fn (t: Task) ->
if t.id == target_id then { t | done = true } // ← targeted update
else t) in
vec_to_owned dst
};
Points:
{ t | done = true }is equivalent toTask { id = t.id, text = t.text, done = true }but skips the fields that don't change, making the update intent explicit.- The result is a new `OwnedVec`, so the caller must rebind (see §8.6's same-name rebinding).
- The explicit annotation `fn (t: Task) -> ...` is required. HM doesn't reverse-engineer which record a closure parameter belongs to (from the field names used), so
t.donewould otherwise fail to type.
8.6. Chain immutable updates with same-name rebinding
When chaining functions that return a new collection (like mark_done), rebinding with the same name reads naturally:
let tasks = owned_vec_new () in
let __ = owned_vec_push tasks (Task { id = 1, text = "buy milk", done = false }) in
let __ = owned_vec_push tasks (Task { id = 2, text = "write report", done = false }) in
// Same-name rebinding to "mark tasks 1 and 2 done"
let tasks = mark_done tasks 1 in
let tasks = mark_done tasks 2 in
This is the natural ML-family style and works across all 4 backends — interpreter / C / LLVM / Wasm (codegen internally expands to a 2-step form to avoid C's __auto_type self-init constraint).
9. List construction idioms
// Sugar
let xs = [1, 2, 3, 4];
// Programmatic
let rec range = fn (lo: int, hi: int) ->
if lo > hi then []
else Cons (lo, range (lo + 1) hi);
range 1 5 // [1, 2, 3, 4, 5]
10. Destructuring nested structures
type 'a list = Nil | Cons of 'a * 'a list;
type 'a opt = None | Some of 'a;
match [Some 1, None, Some 3] with
| [Some a, _, Some b] -> a + b // deep destructure in a single arm
| _ -> -1
Use as-pattern to bind a substructure and the whole at once:
match [1, 2, 3, 4] with
| [a, b, ...rest] as whole -> (a + b, whole)
| _ -> (0, [])
11. Threading "next position" through a parser
// Parser takes (str, int), returns (value, next_int)
let parse_num = fn (s: str, i: int) ->
let rec scan = fn (j: int) ->
if j >= str_len s || not (is_digit (char_at s j)) then j
else scan (j + 1)
in
let end_pos = scan i in
if end_pos == i then fail "expected digit"
else (int_of_str (substring s i end_pos), end_pos);
// Destructure to thread "next position" through the chain
let (a, i) = parse_num s 0 in
let (b, i) = parse_num s (i + 1) in
a + b
contrib/json/json.mere actually uses this pattern.
12. Debug print is show
let _ = print ("xs = " ++ show xs); // "xs = [1, 2, 3]"
let _ = print ("user = " ++ show user); // "user = User { name = ..., age = ... }"
let _ = print ("result = " ++ show (parse_json input));
show : 'a -> str is polymorphic, so records/sums/lists/tuples all stringify via the internal to_string. Cons/Nil chains print in the concise [a, b, c] form.
13. Side-effect loops via iter_n
iter_n 5 (fn () -> print "===");
// Print something some number of times
let echo = fn (n: int, s: str) ->
iter_n n (fn () -> print s);
echo 3 "hello"
14. Ordered side effects with block expressions
{
print_no_nl "Name: ";
let n = read_line () in
print ("Hi, " ++ n ++ "!");
0
}
{ e1; e2; ...; eN } is sugar for let _ = e1 in let _ = e2 in ... in eN. The final expression is the value.
14.5. Phase 36 sugar idioms
Flatten nested matches with ? / ?!
// Old: nested match for None / Err propagation
let safe = fn x ->
match parse x with
| None -> None
| Some a ->
match step1 a with
| None -> None
| Some b ->
match step2 b with
| None -> None
| Some c -> Some (a + b + c);
// New: ? for early-return
let safe = fn x ->
let a = parse x ? in
let b = step1 a ? in
let c = step2 b ? in
Some (a + b + c);
Result version uses ?! with the same pattern. examples/calc.mere's parser is a good real example (138 lines / 5 ?! chain sites).
Combine filter + map with list comprehension
// Old: two stages
let xs = list_map (1..100) (fn x -> x * x) in
let ys = list_filter xs (fn x -> x % 2 == 0);
// New: in one shot
let ys = [x * x | x <- 1..100, (x * x) % 2 == 0];
// Multi-gen for cartesian
let pairs = [(a, b) | a <- 1..5, b <- 1..5, a + b == 6];
for-in-do for side-effect loops; while-do for loops inside fn bodies
// Just print
for x in 1..10 do print (show x);
// Accumulating loops via map_set / owned_vec_push etc.
for x in xs do
let _ = owned_vec_push buf (transform x) in ();
// while: usable inside an fn body
let consume_stream = fn stream ->
while !(stream_eof stream) do
let x = stream_next stream in
let _ = process x in ();
Note: while currently has codegen support only inside fn bodies (top-level main is unsupported).
Single-shot Option extraction with if let
if let Some n = map_get config "timeout" then
use_timeout n
else
use_default ();
Simpler log output via string interpolation
// Old: ++ chain
print ("user=" ++ name ++ ", age=" ++ show age ++ ", role=" ++ role);
// New: interpolation
print "user={name}, age={show age}, role={role}";
Caveats:
- Nested string literals are forbidden (
"x = {show \"abc\"}"→ error). Escape via let. \{escapes a literal{.- The interior of
{}is any expr (function applications / arithmetic / match all OK).
Range + ::, <|, @@ for readable expressions
// range + list_map
list_map (1..10) (* 2) // op section
0 :: 1 :: 2 :: 3 :: [] // explicit list construction
print <| "result: " ++ show answer // reverse pipe
print @@ "lengthy message that goes way " ++
"over one line — @@ avoids needing parens"
15. Using polymorphic helpers
fst (pair "hello" 42) // "hello"
snd (pair "hello" 42) // 42
swap (1, 2) // (2, 1)
const "constant" "anything" // "constant"
flip (fn a -> fn b -> a - b) 3 10 // 7 (= sub 10 3)
Anti-patterns / gotchas
1. Passing the literal -1 as a function argument
abs -1 // syntactically read as (abs - 1) (subtraction)
abs (- 1) // OK: parens (with one space)
abs (-1) // The current lexer doesn't recognize `-` followed by digits as a negative literal
2. Verbose single-char comparisons → fixed (char literals + match)
// Old:
if char_at s i == "n" then ... else if char_at s i == "t" then ...
// New: char literal `'X'` + match
match char_at s i with
| 'n' -> ...
| 't' -> ...
| _ -> ...
3. Match exhaustiveness is checked at runtime → Phase 1 added warnings
match opt with
| Some n -> n
// stderr: "line X, col Y: warning: non-exhaustive match (missing None)"
// Evaluation proceeds, but a runtime Eval_error occurs if None arrives
Exhaustiveness for bool and variants is detected at compile time as a warning. int/str/tuple/record still need a wildcard arm. To enforce full coverage, write | _ -> default or | None -> fail "...".
4. Record update needs the base's type
fn p -> { p | x = 0 } // p's type is unknown → type error
fn (p: Point) -> { p | x = 0 } // OK with annotation
Without row polymorphism, record-typed function args need annotations.
5. Top-level fn names collide with libc / libm / C keywords (C codegen)
C codegen emits top-level fns directly as C functions, so names that already exist in macOS / Linux's libc / libm or are C language keywords cause compile errors. Real collisions found in Phases 32-38:
| Mere name | Collides with |
|---|---|
div | stdlib.h's div(int, int) (returns quotient + remainder) |
mergesort | macOS BSD stdlib.h's mergesort(...) |
pow / sqrt / sin / cos / exp / log | math.h libm functions |
system / getenv / setenv / rand / srand | stdlib.h |
time / clock | time.h |
read / write / open / close | POSIX I/O |
short / long / int / char / float / double | C keywords (__auto_type short = ... is a syntax error) |
signed / unsigned / register / static / auto | C storage classes / modifiers |
goto / return / break / continue | C control-flow keywords |
Workarounds: shorten by 1-2 chars (mergesort → msort, div → divi, short → small_doc), use a verb phrase (sort_list / power_int), or add a prefix (mere_sort), etc. The interpreter is unaffected, so verification looks fine until codegen is attempted. Phase 38.A3 added a linter that warns at parse time (lib/pipeline.ml:42-82).
The full reserved-name list (~110 names) is in [docs/reserved-names.md](reserved-names.md).
6. The empty list literal [] is polymorphic 'a list and breaks codegen
let xs = []; // inferred: 'a list
let _ = some_use xs; // even if later inferred to int, codegen sees the 'a leak in xs's type
C / LLVM codegen need a concrete element type, but the narrow value restriction (Phase 36) still generalizes empty lists. Workaround:
let xs = (Nil: int list); // recommended: explicit annotation
let xs: int list = Nil; // equivalent (either works)
When binding an empty list, fix the element type with an annotation. Non-empty lists ([1, 2, 3]) are inferred from elements, so no annotation is needed.
7. Map[K, V] only takes two args; you need Map[R, K, V] (3 args)
Mere's Map has a region parameter, so type annotations must include R:
// NG
let f = fn (m: Map[str, int]) -> map_get m "k";
// type error: expected `Map['c, 'b, 'a]`, got `(str, int) Map`
// Works (write R as a type variable)
let f = fn (m: Map[R, str, int]) -> map_get m "k";
// → but mismatch with the actual region can still cause separate type errors
// Easiest: skip annotations and let inference handle it
let f = fn m -> map_get m "k";
ML-familiar users tend to write K and V only, but Mere requires the region — three args. This is a common stumbling block for first-time users, so it's documented in patterns / tutorial.
8. Closure capture leaks for inner-lifted fns through anonymous Fun ✅
✅ Fully resolved by Phase 39.A2 + Phase 45 (2026-06-23):
- Phase 39.A2: in cases like
list_iter (...) visitwhere inner-lifted fns are used in value position "outside an anonymous Fun", the 3 backends now work (env is allocated in the default region + a closure value calls the lifted fn through an adapter). - Phase 45: mutual references between inner-lifted fns are resolved — added a transitive capture closure step at the end of
lift_inner_fns. When lifted fn A calls B and B capturesbase, A also transitively capturesbaseand puts it in the env. Also, inner-lifted fn names are excluded from direct captures (since they're not runtime values), aligning with emit_expr's existing dispatch (whereApp (Var n, arg)directly emits__lifted_X(caps, arg)). The same algorithm is implemented in C / LLVM / Wasm.
This makes patterns like let rec helper = fn x -> ... let rec caller = fn y -> helper y ... work in all 4 backends. The markdown_to_html workaround that wrote find_double / find_single as module-external top-level fns is no longer needed (the existing code is preserved as-is; a future refactor can move them back into the module).
When you define let rec foo = fn ... inside a function, reference outer-scope variables, and then call it via an anonymous closure (e.g. list_iter ... (fn v -> foo v)), C codegen couldn't carry foo's closure captures into the anonymous closure's env, failing with error: use of undeclared identifier 'x'.
// NG (fails in C codegen)
let dfs = fn graph ->
let visited = map_new () in
let rec visit = fn u ->
let _ = map_set visited (show u) 1 in // visited is in outer scope
list_iter (neighbors graph u) (fn v -> visit v) in // anonymous Fun calls visit
visit 0;
Workarounds (three):
- Rewrite as iterative + explicit stack/queue (managed via Map). The dfs_bfs.mere / topological_sort.mere implementation pattern.
- Explicit recursion to consume the list: write a mutually recursive fn like
visit_listinstead oflist_iter (fn v -> visit v). - Pass outer-scope state explicitly as an argument: avoid closure capture entirely, e.g.
visit visited graph u.
The cleanest would be list_iter (neighbors xs u) visit — pass the fn value directly to the builtin without making a closure. But using inner-lifted fns in value position is currently unsupported (DEFERRED §1.2 related); the partial-app synthesizer (Phase 38.C) is limited to builtins.
9. substring s start end — end is an exclusive position, not a length
Intuitively, you might write substring s 4 (str_len s - 4), but the 3rd argument is a position (exclusive), not a length. Reversed ranges cause runtime errors.
// NG
substring "### Subsection" 4 (str_len "### Subsection" - 4)
// → substring 4 10 → eval error: range [4, 10) on "### Subsection" — not what you want
// That's correct if you want chars 4..10, but if you want "Subsection"
// (= chars 4..14) it's wrong
// Right
substring "### Subsection" 4 (str_len "### Subsection")
// → "Subsection" (from 4 to the end)
Signature: substring : str -> int -> int -> str taking (s, start, end). end is exclusive — s[start..end). Same as Python slicing.
10. Single-arg builtins (int_of_str etc.) used in value position become unbound in C codegen
list_map (str_split s ",") int_of_str
// codegen error: use of undeclared identifier 'int_of_str'
Phase 38.C made curried collection builtins like vec_push / map_set usable in value position via synthesis, but single-arg builtins like `int_of_str` / `str_len` / `ord` / `show` aren't covered (1-arg, so different from Phase 35's nullary factory eta-wrap path). They show up as undefined C functions.
Workaround:
list_map (str_split s ",") (fn x -> int_of_str x)
Wrapping in fn x -> ... lets the existing anonymous-Fun adapter machinery handle the closure conversion. Only one character extra — a light workaround.
A future extension of Phase 38.C's synthesize_curried_eta to 1-arg builtins would make this unnecessary (low priority; issue-driven).
12. Record literals inside list literals require Type {…} (the type prefix)
Record literals inside list literals must always be written `TypeName { ... }`. The field-only form { x = 1, y = 2 } is parsed as if it were "middle of list structure" and cuts the literal short:
type Point = { x: int, y: int };
// NG (parse error or odd type errors)
let ps = [{ x = 1, y = 2 }, { x = 3, y = 4 }];
// OK: each record literal gets a Type prefix
let ps = [Point { x = 1, y = 2 }, Point { x = 3, y = 4 }];
Reason: the parser has no recovery path that interprets the { ... } immediately after [ as a standalone record literal. To reliably close a record literal as an expression, TypeName { is needed; mid-list records require the same prefix.
Furthermore: write record type names as uppercase-starting (Point / Task / User) to reduce ambiguity in literal-only contexts (list elements, the RHS of match arms) and to stay consistent with patterns / examples. Lowercase type names (type point = { ... }) are hard to distinguish from constructor syntax; task_scheduler.mere needed the rewrite type task → type Task (2026-06-23 dogfood).
11. Destructure 3-tuples or larger via let (a, b, c) = ..., not fst/snd
fst / snd are 2-tuple only. For 3-tuples and larger, destructure via patterns:
let r = ext_gcd 30 18; // r: int * int * int
// NG (type error)
let g = fst r in
let x = fst (snd r) in // snd : (int * int * int) -> ? — won't pass
// OK
let (g, x, y) = ext_gcd 30 18 in ...
A 3-tuple is internally one tuple (int * int * int), not a nested pair (a, (b, c)). fst/snd are defined as 2-tuple-only builtins in the OCaml tradition.
See also
- Full syntax: language-reference.md
- All builtins: stdlib-reference.md
- Getting started: tutorial.md