Conditional Dispatch (pickup)
Normative source (v0.33.0). The
pickupconstruct is specified at version 1.0.0 indocs/PICKUP.md. That document covers the grammar, the static / dynamic / operational semantics, the formatter rules, the algebraic laws, the legacyifmigration story, the diagnostic catalogue, and a soundness proof sketch. This page is the user-facing tutorial complement; for any conflict, the formal specification is the authority.
pickup is the only way in Cure to branch on a free-standing boolean
condition. It replaced the legacy if / elif / else chain and is
governed by a single mental model:
pickupwalks the clauses and picks up the first one whose guard is true.
Every other rule in the construct exists to make that intuition mechanically precise.
The shape
A pickup block is a non-empty list of guarded clauses ending in a
mandatory terminator:
pickup
status >= 500 -> :server_error
status >= 400 -> :client_error
status >= 300 -> :redirect
status >= 200 -> :ok
else -> :informational
Each clause is one of two forms:
- Guarded --
expression -> expression. The left-hand expression is the guard; it MUST type toBool. - Terminal --
else -> expression. There is exactly one, and it MUST be the last clause. The literaltruein last position is accepted as an alternative form and rewritten toelseby the formatter.
The terminator is mandatory. A pickup without else (or
last-position true) is rejected with E-PICKUP-NO-ELSE.
Total by construction
The mandatory terminator means a well-typed pickup cannot fail with
a "no clause matched" condition at runtime. Compare this with match:
non-exhaustive match is only a warning, and the non-covered case
raises case_clause at runtime. With pickup, totality is
syntactically guaranteed.
Strict Bool typing
Each guard MUST type to Bool. There is no truthy / falsy coercion;
pickup is uncompromising about types:
# Rejected: 1 is not Bool
pickup
1 -> :truthy
else -> :falsy
# E-PICKUP-GUARD-TYPE
The branch right-hand sides MUST share a common upper bound under the
language's subtyping relation. If they do not, the program is
rejected with E-PICKUP-BRANCH-MISMATCH:
# Rejected: branches are Int and String
pickup
cond -> 1
else -> "two"
# E-PICKUP-BRANCH-MISMATCH
Evaluation order
Guards evaluate in source order. As soon as one yields true, no
subsequent guard runs and only the selected branch evaluates. If
every guard yields false, the terminator runs.
pickup
log "checking ready" ; ready? -> launch ()
log "checking timeout"; timed_out? -> retry ()
else -> wait ()
If ready? is true, "checking timeout" is never logged. The
order is contractual, not an optimisation; the compiler rearranges
guards only when their value is statically constant.
Per-clause scoping
Each clause introduces its own lexical scope:
- A guard
g_isees the scope enclosing thepickup, extended with bindings introduced byg_i. - The right-hand side
e_isees the scope ofg_i. - Bindings from
g_i/e_iare not visible in any other clause. - Nothing escapes the
pickupexpression.
Refinement narrowing
Inside the i-th branch, the refinement context is strengthened with
g_i ∧ ¬g_1 ∧ ... ∧ ¬g_{i-1}. Inside the else branch, it is
strengthened with the conjunction of every preceding negation. This
lets the type checker prove safety of the branch body without an
explicit refinement annotation:
fn safe_div(n: Int, d: Int) -> Int =
pickup
d != 0 -> n / d # `d` is refined to {x: Int | x != 0}
else -> 0
Tail-position behaviour
A branch right-hand side is in tail position with respect to pickup
iff pickup is itself in tail position. This guarantees proper tail
calls in any branch, including the else:
fn loop(n: Int, acc: Int) -> Int =
pickup
n == 0 -> acc
else -> loop(n - 1, acc + n)
loop(1_000_000, 0) terminates without stack overflow.
pickup as an expression
pickup is an expression, never a statement. It returns the value
of the selected branch and is admissible everywhere an expression is:
let label =
pickup
n > 0 -> "positive"
n < 0 -> "negative"
else -> "zero"
emit(label)
It nests freely with match and other constructs:
match request
%{method: "GET", path: p} ->
pickup
cached?(p) -> serve_cache(p)
stale?(p) -> revalidate(p)
else -> serve_fresh(p)
%{method: "POST", body: b} -> handle_post(b)
_ -> :malformed
Migrating from if / elif / else
The if/elif chain has been removed. The cure rewrite if-to-pickup
tool migrates surviving code mechanically, preserving comments and
running the formatter on every modified file:
-- Before (no longer accepted):
if score >= 90 then "A"
elif score >= 80 then "B"
elif score >= 70 then "C"
else "F"
-- After:
pickup
score >= 90 -> "A"
score >= 80 -> "B"
score >= 70 -> "C"
else -> "F"
Post-migration, the parser rejects if with E-IF-REMOVED and a
fix-it pointing at the rewriter.
Formatter conventions
The formatter aligns all -> tokens within a single pickup block,
including the else clause:
pickup
x > 0 -> :positive
x < 0 -> :negative
even?(x) -> :zero_even
else -> :zero_odd
Other formatter rules:
- A trailing
true ->is rewritten toelse ->with hintH-PICKUP-PREFER-ELSE. - A degenerate
pickupwhose only clause is the terminator collapses to its right-hand side (H-PICKUP-DEGENERATE). - Multi-line right-hand sides switch every clause in the block to
the wrapped form (
->at the end of the guard line, body indented one step deeper). Mixing aligned and wrapped forms is forbidden. - Comments are preserved verbatim. Block-leading and clause-leading
comments stay attached to their construct under refactoring.
Internal stray comments may be relocated by the formatter with
H-PICKUP-COMMENT-RELOCATED. - The formatter is idempotent
(
format(format(s, c), c) = format(s, c)) and round-trip-safe (formatted source re-parses byte-identically).
Diagnostics
The full diagnostic catalogue:
- E-PICKUP-NO-ELSE --
pickuplacks a valid terminator. - E-PICKUP-ELSE-NOT-LAST -- clauses follow the
elseclause. - E-PICKUP-MULTIPLE-ELSE -- more than one
elseclause. - E-PICKUP-GUARD-TYPE -- guard not of type
Bool. - E-PICKUP-BRANCH-MISMATCH -- branch right-hand sides have no common upper bound.
- E-IF-REMOVED -- legacy
ifkeyword encountered; emitted with a fix-it pointing atcure rewrite if-to-pickup. - W-PICKUP-UNREACHABLE -- guard provably unreachable.
- W-PICKUP-DEAD-ELSE -- terminator provably unreachable.
- W-PICKUP-EFFECTFUL-GUARD -- guard observed to have side effects.
- H-PICKUP-PREFER-ELSE -- trailing
true ->rewritten toelse ->. - H-PICKUP-DEGENERATE -- single-arm
pickupcollapsed to its right-hand side. - H-PICKUP-LINE-TOO-LONG -- clause cannot fit within
max_line_widtheven when wrapped. - H-PICKUP-COMMENT-RELOCATED -- internal stray comment relocated by the formatter.
Idioms
Use pickup for predicates, match for shape
If the deciding question is "what shape does this value have?",
use match. If it is "which of these conditions holds?", use
pickup. A match whose patterns are uniformly wildcards is a
pickup in disguise.
Order guards deliberately
Pick one of two orderings and stay consistent within a block:
- By specificity. More specific predicates first, falling through to general ones.
- By likelihood. Most-likely predicates first, optimising the cost of evaluation.
Prefer pure guards
A guard with side effects executes conditionally on every prior
guard's result. Restrict effects to the selected branch unless the
side effect is the test (e.g. lock_acquired?(lock)).
Bind once, dispatch many
# Less clear: each `next_token()` call advances state
pickup
next_token() == :open -> parse_block()
next_token() == :colon -> parse_label()
else -> parse_atom()
# Clearer:
let t = next_token()
pickup
t == :open -> parse_block()
t == :colon -> parse_label()
else -> parse_atom()
Use else, not true
The formatter rewrites true -> to else ->, but human-written
source SHOULD use else directly. The literal true reads as if a
real condition is being tested; else reads as the default arm.
See also
- The full normative specification is at
docs/PICKUP.md. - The
matchconstruct -- the structural-dispatch counterpart -- is documented at/matchand specified normatively atdocs/MATCH.md. Both specifications were published into HexDocs in v0.33.0. - For the broader language reference, see
docs/LANGUAGE_SPEC.md.