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 |
Type names collide too, and for one more reason. A type lowers to typedef struct , which claims C's tag namespace as well as its ordinary one — so a type named wait collides with union wait in and with the wait() declared beside it. The list above applies unchanged (a typedef is an ordinary identifier), plus the struct tags the emitted headers bring in: wait, tm, timeval, timespec, stat, dirent, sockaddr, addrinfo, hostent, termios, winsize, sigaction, iovec, fd_set, div_t and family.
The mraft dogfood named a type wait and found this from clang rather than from the compiler: the check existed and had only ever looked at let names. Warned about since v0.1.223, on the type name itself, on both the compiler's path and the editor's.
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