⚠️

Dubious Content

This content is aspirational or placeholder. Syntax and semantics may be incorrect or subject to change.

Optional Branches

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.

What Are Optional Branches?

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
~event process { value: u32 }
| success { result: u32 }        // REQUIRED - must handle
| ?warning { msg: []const u8 }   // OPTIONAL - can use |?
| ?debug { info: []const u8 }    // OPTIONAL - can use |?

// Handler with |? catch-all for optional branches
~process(value: 10)
| success { result } |> std.debug.print("Result: {}\n", .{result})
|? |> _  // Catches all optional branches (warning, debug), does nothing

The success branch is required—handlers must handle it explicitly. The ?warning and ?debug branches are optional—handlers can use |? to catch them generically. Without |?, if an optional branch fires, execution stops.

Why Optional Branches?

Supplementary Information

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.

Zero-Cost Abstractions

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.

Context-Specific Handling

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.

The Event Pump Pattern (THE 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.
~pub event pump {}
| ?mouse_event { x: i32, y: i32, button: i32 }
| ?keyboard_event { code: i32 }
| ?window_event { type: i32 }
| ?timer_event { id: i32 }
| ?quit {}
// ... imagine 50+ more event types

// Loop using label/jump pattern - only handle keyboard and mouse
~#pump_loop pump()
| mouse_event m |> handleMouse(m.x, m.y, m.button) |> @pump_loop
| keyboard_event k |> handleKeyboard(k.code) |> @pump_loop
| quit |> _  // Exit loop
|? |> @pump_loop  // Catches window_event, timer_event, etc., continues loop

// Loop continues even when unhandled events fire!
// Without |?, loop would stop on first unhandled event

Why 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.

Mixing Explicit Handling with |?

You can mix explicit handling of specific optional branches with |? catch-all for the rest. This is the most realistic real-world pattern:

~event validate { data: []const u8 }
| valid { result: Result }
| ?warning { msg: []const u8 }

~proc validate {
    if (data.len > MAX_SIZE) {
        // Return optional warning
        return .{ .warning = .{ .msg = "Data exceeds recommended size" } };
    }
    const result = processData(data);
    return .{ .valid = .{ .result = result } };
}

// Handler 1: Cares about warnings - handles explicitly
~validate(data: input1)
| valid { result } |> use(result)
| warning { msg } |> log(msg)  // Explicit handling
|? |> _                         // For any other optional branches

// Handler 2: Doesn't care about warnings - uses |?
~validate(data: input2)
| valid { result } |> use(result)
|? |> _  // Catches warning and any other optional branches

Handler 1 explicitly handles the warning branch because it cares about warnings. Handler 2 uses |? to catch all optional branches generically. Both are valid patterns.

Selective Handling

Events can have multiple optional branches. Different handlers can choose different subsets to handle explicitly, using |? for the rest:

~event analyze { text: []const u8 }
| success { result: Analysis }
| ?warning { msg: []const u8 }
| ?debug { info: []const u8 }
| ?trace { details: []const u8 }

// Handler 1: Cares about warnings, |? for rest
~analyze(text: source1)
| success { result } |> use(result)
| warning { msg } |> log(msg)  // Explicit
|? |> _                         // Catches debug, trace

// Handler 2: Cares about debug/trace, |? for rest
~analyze(text: source2)
| success { result } |> use(result)
| debug { info } |> logDebug(info)      // Explicit
| trace { details } |> logTrace(details) // Explicit
|? |> _                                  // Catches warning

// Handler 3: Doesn't care about any optional branches
~analyze(text: source3)
| success { result } |> use(result)
|? |> _  // Catches warning, debug, trace

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.

Zero-Cost Abstractions

The Koru compiler eliminates code for unhandled optional branches. This means you can write rich, instrumented procs without worrying about production performance:

~event compute { n: u32 }
| result { value: u32 }
| ?profile { duration_ns: u64 }
| ?trace { steps: []Step }

~proc compute {
    // Profiling code
    const start = timer.read();

    // Main computation
    const value = expensiveCalculation(n);

    // Optional profiling output
    const duration = timer.read() - start;
    return .{ .profile = .{ .duration_ns = duration } };
}

// Production handler - ignores profiling with |?
~compute(n: 100)
| result { value } |> use(value)
|? |> _
// 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.

Shape Checking Still Applies

Optional branches can be omitted, but when you do handle them, shape checking still applies:

~event process { value: u32 }
| success { result: u32 }
| ?warning { msg: []const u8 }

// CORRECT ✓
~process(value: 10)
| success { result } |> std.debug.print("{}\n", .{result})
| warning { msg } |> std.debug.print("{s}\n", .{msg})
|? |> _

// WRONG ✗ - Type mismatch!
~process(value: 10)
| success { result } |> std.debug.print("{}\n", .{result})
| warning { result } |> std.debug.print("{}\n", .{result})
//         ^^^^^^^ ERROR: warning has 'msg', not 'result'!
|? |> _

// 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.

Why |? Is NOT the F# Discard Pattern

This 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

~event process { value: u32 }
| success { result: u32 }        // REQUIRED
| ?warning { msg: []const u8 }   // OPTIONAL

~process(value: 10)
| success { result } |> handle(result)  // Must handle required
|? |> _                                  // Catches optional only

// API Evolution Scenario 1: Add REQUIRED branch
~event process { value: u32 }
| success { result: u32 }
| error { msg: []const u8 }      // NEW REQUIRED BRANCH
| ?warning { msg: []const u8 }

// Result: COMPILE ERROR! Handler missing 'error' continuation
// This is GOOD - forces you to consider error handling

// API Evolution Scenario 2: Add OPTIONAL branch
~event process { value: u32 }
| success { result: u32 }
| ?warning { msg: []const u8 }
| ?debug { info: []const u8 }    // NEW OPTIONAL BRANCH

// Result: No compile error, |? silently catches it
// This is ALSO GOOD - optional branches are supplementary

The 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.

Best Practices

When to Use Optional Branches

  • Supplementary information: Warnings, debug output, profiling stats that aren't core to the event's purpose
  • Context-specific handling: When different handlers legitimately care about different aspects of the same operation
  • Instrumentation: Profiling, tracing, or diagnostic data that shouldn't impact production code

When NOT to Use Optional Branches

// ❌ ANTIPATTERN: Using |? to avoid API evolution
// When you add a NEW REQUIRED branch:
~event process { value: u32 }
| success { result: u32 }
| error { msg: []const u8 }   // NEW REQUIRED BRANCH
| ?warning { msg: []const u8 }

// WRONG approach: "I'll use |? to avoid updating my handlers"
~process(value: 10)
| success { result } |> use(result)
|? |> _  // 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 { result } |> use(result)
| error { msg } |> handleError(msg)  // Explicitly handle required branch
|? |> _                               // 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!

Recommended Patterns

// ✓ Primary outcomes = required
~event connect { url: []const u8 }
| connected { handle: Handle }  // REQUIRED
| error { msg: []const u8 }     // REQUIRED

// ✓ Supplementary data = optional
~event parse { source: []const u8 }
| ast { tree: AST }              // REQUIRED
| ?warnings { list: []Warning }  // OPTIONAL - nice to have

// ✓ Debug/profiling = optional
~event compute { n: u32 }
| result { value: u32 }          // REQUIRED
| ?profile { stats: Stats }      // OPTIONAL - for debugging

Related Tutorials