Contents

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:


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:

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 nameCollides with
divstdlib.h's div(int, int) (returns quotient + remainder)
mergesortmacOS BSD stdlib.h's mergesort(...)
pow / sqrt / sin / cos / exp / logmath.h libm functions
system / getenv / setenv / rand / srandstdlib.h
time / clocktime.h
read / write / open / closePOSIX I/O
short / long / int / char / float / doubleC keywords (__auto_type short = ... is a syntax error)
signed / unsigned / register / static / autoC storage classes / modifiers
goto / return / break / continueC control-flow keywords

Workarounds: shorten by 1-2 chars (mergesortmsort, divdivi, shortsmall_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):

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):

  1. Rewrite as iterative + explicit stack/queue (managed via Map). The dfs_bfs.mere / topological_sort.mere implementation pattern.
  2. Explicit recursion to consume the list: write a mutually recursive fn like visit_list instead of list_iter (fn v -> visit v).
  3. 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 endend 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 tasktype 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