This content is aspirational or placeholder. Syntax and semantics may be incorrect or subject to change.
Build flexible event-driven systems with selective handling. Optional branches use the `|?` catch-all to handle supplementary outcomes generically, enabling patterns like event pumps without exhaustive handling.
By default, Koru requires handlers to handle every branch an event can return. Optional branches, marked with ?, can be caught by the |? catch-all pattern:
// Event with required and optional branches
import std/io
tor process { value: i64 }
| success i64 // REQUIRED - must handle
| ?warning string // OPTIONAL - can be ignored or caught by |?
| ?debug string // OPTIONAL - can be ignored or caught by |?
// Handler with |? catch-all for optional branches
process(value: 10)
| success v |> std/io:print.ln("Result: {{ v:d }}")
|? |> std/io:print.ln("optional branch fired") // Catches warning and debugThe success branch is required—handlers must handle it explicitly. The ?warning and ?debug branches are optional—handlers can use |? to catch them generically, or simply omit them: an optional branch that fires unhandled is a silent no-op. A |? catch-all must do real work in its body—a bare |? |> _ discard is rejected by the parser.
Events can provide rich diagnostic information—warnings, debug output, profiling stats—that handlers only care about in specific contexts. Optional branches let you include this without forcing every handler to deal with it.
Write richly instrumented procs without performance cost. When handlers ignore optional branches, the compiler eliminates that code entirely. You get debug-rich code in development, zero-cost in production.
Different handlers legitimately care about different outcomes. A validation event might return valid data, warnings, and detailed error context—but not every handler needs all three. Optional branches let each handler choose what matters for its use case.
This is THE motivating use case for optional branches: wrapping event-based systems like WIN32, SDL, or other event APIs with dozens of event types where you only care about a subset.
// Event pump with many optional event types
// This is THE use case: wrapping WIN32, SDL, etc.
// Host-embedded .kz — a |zig proc wraps the OS event queue
~tor pump { iteration: u32 }
| ?mouse-event i32
| ?keyboard-event i32
| ?window-event i32
| ?timer-event i32
| quit
// ... imagine 50+ more event types
// Loop using label/jump pattern - only handle keyboard and mouse
~#loop pump(iteration: 0)
| mouse-event m |> handle-mouse(button: m) |> @loop(iteration: iteration + 1)
| keyboard-event k |> handle-keyboard(code: k) |> @loop(iteration: iteration + 1)
| quit |> _ // Exit loop
|? |> @loop(iteration: iteration + 1) // Catches window-event, timer-event, etc.
// Loop continues even when unhandled events fire!
// Without |?, the flow would end the first time an unhandled optional event firedWhy this works: The |? catch-all satisfies the branch interface for all unhandled optional branches. When window_event or timer_event fire, they're caught by |? and the loop continues.
Without |?: The loop would stop the first time an unhandled event fires, making the pattern impossible. You'd need exhaustive handling of all 50+ event types, which defeats the purpose.
|?You can mix explicit handling of specific optional branches with |? catch-all for the rest. This is the most realistic real-world pattern:
// Host-embedded .kz — the proc body is Zig
~tor validate { data: string }
| valid string
| ?warning string
~proc validate|zig {
if (data.len > MAX_SIZE) {
// Return optional warning
return .{ .warning = "Data exceeds recommended size" };
}
return .{ .valid = data };
}
// Handler 1: Cares about warnings - handles explicitly
~validate(data: input1)
| valid v |> use(value: v)
| warning m |> log(msg: m) // Explicit handling
// Handler 2: Doesn't care which one fired - catches them with |?
~validate(data: input2)
| valid v |> use(value: v)
|? |> log(msg: "optional branch fired") // Catches warning and any other optional branchesHandler 1 explicitly handles the warning branch because it cares about warnings. Handler 2 uses |? to catch all optional branches generically. Both are valid patterns.
Events can have multiple optional branches. Different handlers can choose different subsets to handle explicitly, using |? for the rest:
tor analyze { text: string }
| success string
| ?warning string
| ?debug string
| ?trace string
// Handler 1: Cares about warnings, |? for rest
analyze(text: source1)
| success r |> use(result: r)
| warning m |> log(msg: m) // Explicit
|? |> log(msg: "optional branch") // Catches debug, trace
// Handler 2: Cares about debug/trace, |? for rest
analyze(text: source2)
| success r |> use(result: r)
| debug i |> log-debug(info: i) // Explicit
| trace d |> log-trace(details: d) // Explicit
|? |> log(msg: "optional branch") // Catches warning
// Handler 3: Doesn't care about any optional branches - just omit them
analyze(text: source3)
| success r |> use(result: r)This is the real-world pattern: APIs provide multiple supplementary branches (warnings, debug info, profiling stats), and each handler explicitly handles the ones it cares about, using |? to catch the rest.
The Koru compiler eliminates code for unhandled optional branches. This means you can write rich, instrumented procs without worrying about production performance:
// Host-embedded .kz — the proc body is Zig
~tor compute { n: u32 }
| result u32
| ?profile u64
~proc compute|zig {
// Profiling code
const start = timer.read();
// Main computation
const value = expensiveCalculation(n);
// Optional profiling output
const duration = timer.read() - start;
_ = duration;
return .{ .result = value };
}
// Production handler - simply omits the profile branch
~compute(n: 100)
| result v |> use(value: v)
// Compiler eliminates unused profiling code!When the handler omits the profile branch, the compiler eliminates the timer instrumentation entirely. You get debug-rich procs in development and zero-cost in production.
Optional branches can be omitted, but when you do handle them, shape checking still applies:
tor process { value: i64 }
| success { result: i64, cost: i64 }
| ?warning string
// CORRECT ✓
process(value: 10)
| success { result } |> std/io:print.ln("{{ result:d }}")
| warning m |> std/io:print.ln("{{ m:s }}")
// WRONG ✗ - Shape mismatch!
process(value: 10)
| success { nonexistent } |> std/io:print.ln("{{ nonexistent:d }}")
// ^^^^^^^^^^^ ERROR: success has 'result' and 'cost', no 'nonexistent'!
| warning m |> std/io:print.ln("{{ m:s }}")
// Optional doesn't mean "skip type checking"!"Optional" means "can be omitted," not "can be misused." The compiler still verifies that payload types match when branches are handled.
|? Is NOT the F# Discard PatternThis is fundamentally different from F#'s _ discard, which silently catches ALL future cases (including important ones). Koru's |? catches OPTIONAL branches ONLY:
// Why |? is NOT the F# discard pattern
//
// F# Problem: _ discard catches EVERYTHING (including future cases)
// Koru Solution: |? catches OPTIONAL branches ONLY
tor process { value: i64 }
| success i64 // REQUIRED
| ?warning string // OPTIONAL
process(value: 10)
| success v |> handle(value: v) // Must handle required
|? |> log(msg: "optional") // Catches optional only
// API Evolution Scenario 1: Add REQUIRED branch
tor process { value: i64 }
| success i64
| error string // NEW REQUIRED BRANCH
| ?warning string
// Result: COMPILE ERROR! Handler missing 'error' continuation
// This is GOOD - forces you to consider error handling
// API Evolution Scenario 2: Add OPTIONAL branch
tor process { value: i64 }
| success i64
| ?warning string
| ?debug string // NEW OPTIONAL BRANCH
// Result: No compile error, |? silently catches it
// This is ALSO GOOD - optional branches are supplementaryThe key difference: When you add a REQUIRED branch to an event, ALL handlers without that branch fail to compile. Cascade compilation errors force you to consider the new branch everywhere.
When you add an OPTIONAL branch, handlers with |? continue working (correct! it's optional supplementary info). The |? catch-all preserves exhaustive handling for required branches while allowing flexibility for optional ones.
// ❌ ANTIPATTERN: Using |? to avoid API evolution
// When you add a NEW REQUIRED branch:
tor process { value: i64 }
| success i64
| error string // NEW REQUIRED BRANCH
| ?warning string
// WRONG approach: "I'll use |? to avoid updating my handlers"
process(value: 10)
| success v |> use(value: v)
|? |> log(msg: "optional") // DON'T use this to dodge required branches!
// This will cause COMPILE ERROR - required 'error' branch not handled!
// ✓ RIGHT approach: Update all handlers to handle new required branch
process(value: 10)
| success v |> use(value: v)
| error e |> handle-error(msg: e) // Explicitly handle required branch
|? |> log(msg: "optional") // For optional branches only
// Cascade compilation errors are HOW you evolve APIs correctly!Don't make primary outcomes optional. If a branch represents a core result, keep it required to ensure handlers address it. And don't use optional branches to avoid updating handlers when evolving an API—Koru's exhaustive handling is how you ensure API changes are complete. Cascade compilation errors are a feature, not a bug!
// ✓ Primary outcomes = required
tor connect { url: string }
| connected i64 // REQUIRED - the connection handle
| error string // REQUIRED
// ✓ Supplementary data = optional
tor parse { source: string }
| ast string // REQUIRED - the parse result
| ?warnings string // OPTIONAL - nice to have
// ✓ Debug/profiling = optional
tor compute { n: i64 }
| result i64 // REQUIRED
| ?profile u64 // OPTIONAL - for debugging