Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Option, Result, and ?

Rock represents expected absence and recoverable failure with ordinary enum values. There are no exceptions or implicit stack unwinding. A function that may not produce a value states its carrier in its return type.

Optional Values

Option T has two constructors: Option::Some carries a T, and Option::None carries no value.

find_even: I64 -> Option I64
find_even = value ->
    if value % 2 == 0
    then Option::Some value
    else Option::None

main = !->
    present = find_even 6
    absent = find_even 7
    match present
        Option::Some value => value.println!
        Option::None => -1 .println!
    match absent
        Option::Some value => value.println!
        Option::None => -1 .println!

present has type Option I64 and contains Some 6; absent has the same type and contains None. The output is 6 and -1. Matching makes both paths explicit.

Results and Concrete Errors

Result T, E carries either Result::Ok T or Result::Err E. Always choose a concrete error type at a public boundary; the following function uses I64 as its error code.

divide: I64 -> I64 -> Result I64, I64
divide = numerator, denominator ->
    if denominator == 0
    then Result::Err 1
    else Result::Ok numerator / denominator

main = !->
    successful = divide 10, 2
    failed = divide 10, 0
    match successful
        Result::Ok value => value.println!
        Result::Err error => error.println!
    match failed
        Result::Ok value => value.println!
        Result::Err error => error.println!

The success path is Ok 5, and the failure path is Err 1; the output is 5 and 1. The error is data that the caller can inspect, transform, or return.

Domain errors can use a dedicated enum rather than an unstructured integer.

enum ParseError
    Empty
    Negative

impl Show for ParseError
    @show = ->
        match *self
            ParseError::Empty => String::from "empty input"
            ParseError::Negative => String::from "negative input"

parse_nonnegative: I64 -> Result I64, ParseError
parse_nonnegative = value ->
    if value < 0
    then Result::Err ParseError::Negative
    else Result::Ok value

main = !->
    result = parse_nonnegative -3
    match result
        Result::Ok value => value.println!
        Result::Err error => error.println!

parse_nonnegative returns Err ParseError::Negative; the Show implementation turns that structured error into negative input, which is the output.

Propagating with ?

Postfix ? unwraps a successful Option or Result value and returns early from the enclosing function on the failure branch.

twice_present: Option I64 -> Option I64
twice_present = value ->
    number = value?
    Option::Some number * 2

main = !->
    present = twice_present Option::Some 4
    absent = twice_present Option::None
    match present
        Option::Some value => value.println!
        Option::None => -1 .println!
    match absent
        Option::Some value => value.println!
        Option::None => -1 .println!

For the first call, value? produces 4 and the function returns Some 8. For the second, it returns None before the multiplication. The output is 8 and -1.

The enclosing return type must implement FromResidual for the propagated carrier’s residual. With the standard implementations used here, propagation stays within Option or within a compatible Result.

positive: I64 -> Result I64, I64
positive = value ->
    if value < 0
    then Result::Err 1
    else Result::Ok value

double_positive: I64 -> Result I64, I64
double_positive = value ->
    number = positive value?
    Result::Ok number * 2

main = !->
    success = double_positive 4
    failure = double_positive -4
    success.unwrap_or 0 .println!
    failure.unwrap_or 0 .println!

The successful path returns Ok 8; the failing path returns Err 1, and both are handled with unwrap_or 0, so the output is 8 and 0. Ownership follows ordinary return control flow: a value moved into a failed operation is not restored by ?.

Borrowed Call Arguments

A trailing ? propagates a call’s result even when its last argument is borrowed:

read: &I64 -> Result I64, I64
read = value -> Result::Ok *value

compute: () -> Result I64, I64
compute = ->
    value = 21
    number = read &value?
    Result::Ok number * 2

main = !->
    compute! .unwrap_or 0 .println!

read &value? means (read (&value))?: it borrows value, calls read, then propagates the returned Result. It does not apply ? to the integer. Keep & adjacent to the borrowed operand to distinguish it from a spaced infix &. This program prints 42.

Call Results and Continuations

A ? following an unparenthesized call finishes that call and propagates its result before any following operator, method chain, index, cast, or further call. This rule is independent of the following operator’s precedence:

checked: I64 -> Result I64, &Str
checked = value ->
    if value < 0 then Result::Err "negative" else Result::Ok value

sum_checked: I64 -> I64 -> Result I64, &Str
sum_checked = left, right ->
    total = checked left? + checked right?
    Result::Ok total

main = !->
    sum_checked 20, 22 .println!
    sum_checked -1, 22 .println!

This prints Ok(42) and Err(negative). Each ? acts on a result returned by checked, not on an integer argument. The first error returns from sum_checked before evaluating the rest of the addition.

To propagate inside an argument instead, keep ? inside that argument’s parentheses: checked (checked left?) evaluates and propagates the inner call before calling the outer one. This grouping is also necessary when a propagated argument precedes another comma-separated argument. Parenthesized expressions, array elements, indices, and explicit nested function bodies retain their own expression boundaries.

Consuming Combinators

map changes a successful payload while preserving the carrier. and_then calls a function that returns another carrier. These operations consume an owned receiver so an owned payload can move into the callback.

increment: I64 -> I64
increment = value -> value + 1

keep_even: I64 -> Option I64
keep_even = value ->
    if value % 2 == 0
    then Option::Some value
    else Option::None

main = !->
    mapped = Option::Some 4 .map increment
    chained = Option::Some 4 .and_then keep_even
    flattened = Option::Some Option::Some 9 .flatten!
    mapped.unwrap_or 0 .println!
    chained.unwrap_or 0 .println!
    flattened.unwrap_or 0 .println!

mapped is Some 5, chained is Some 4, and flattened is Some 9; the output is 5, 4, and 9. The three source Option values are consumed independently and are not reused afterward.

ok_or converts an Option T into a Result T, E by supplying the error for the None case:

main = !->
    result = Option::Some 4 .ok_or 1
    result.unwrap_or 0 .println!

Result has the same shape for successful values and preserves the concrete error type. The annotations below specify the error type, which cannot be inferred from Result::Ok alone.

increment: I64 -> I64
increment = value -> value + 1

main = !->
    mapped: Result I64, I64 = Result::Ok 4 .map increment
    chained: Result I64, I64 = Result::Ok 4 .and_then value -> Result::Ok value * 2
    failed = Result::Err 7 .map increment
    mapped.unwrap_or 0 .println!
    chained.unwrap_or 0 .println!
    failed.unwrap_or 0 .println!

The output is 5, 8, and 0; the error 7 is preserved in failed even though unwrap_or chooses the fallback for printing.

Functional Operators

The prelude exports operator spellings for the same stdlib operations. The named methods above are usually clearer while learning the carriers.

increment: I64 -> I64
increment = value -> value + 1

keep_even_option: I64 -> Option I64
keep_even_option = value ->
    if value % 2 == 0
    then Option::Some value
    else Option::None

main = !->
    mapped = Option::Some 4 <&> increment
    chained = Option::Some 4 >>= keep_even_option
    fallback = Option::None <|> Option::Some 9
    converted = Option::Some 4 !> 1
    mapped.unwrap_or 0 .println!
    chained.unwrap_or 0 .println!
    fallback.unwrap_or 0 .println!
    converted.unwrap_or 0 .println!

<&> maps, >>= binds, <|> chooses a fallback, !> converts an Option to a Result, and |> passes a value to a function. The output is 5, 4, 9, and 4. These meanings are standard-library definitions, not compiler-owned special cases.

Flat Sequencing with do

Use a do expression when later fallible operations need values from earlier ones. <- binds a successful payload, and the rest of the block becomes the callback passed to bind. With Result, an error skips that callback and becomes the block’s result.

positive: I64 -> Result I64, I64
positive = value ->
    if value < 0 then Result::Err 1 else Result::Ok value

sum_positive: I64 -> I64 -> Result I64, I64
sum_positive = left, right -> do
    first <- positive left
    second <- positive right
    total = first + second
    pure total

main = !->
    sum_positive 16, 6 .unwrap_or -1 .println!
    sum_positive -1, 6 .unwrap_or -1 .println!

This prints 22 and -1. An ordinary = statement keeps Rock’s usual binding or reassignment behavior. pure total constructs the successful result; pure is an ordinary prelude function whose carrier is inferred from the function signature. The last expression is returned unchanged, so you can also end with another fallible call or an explicit Result::Ok value.

The same sequencing works with Option:

main = !->
    answer = do
        first <- Option::Some 20
        _ <- Option::Some 0
        second <- Option::Some 22
        pure (first + second)
    answer.unwrap_or -1 .println!

This prints 42. _ <- action discards a successful payload. A nonfinal bare expression does the same: it is sequenced through bind, rather than treated as an ordinary statement. In particular, println! returns an I32, not a carrier; print the completed result outside the block, as above, or explicitly assign an eager side effect’s result to a local inside a continuation.

do is syntax sugar for calls to the bind function visible at each sequencing step. The prelude supplies the standard monadic implementation, but ordinary imports and explicit local shadowing can select another function. There is no implicit error conversion, automatic lifting of the final value, or new runtime effect system. Existing closure capture and ownership rules apply; the standard bind requires a callback that supports FnMut.

A block must be indented and end with an expression. Bind targets currently support an identifier or _. Use <- instead of ? directly in the block, and use the final expression instead of return; break and continue cannot leave the block. Explicit nested functions and loops retain their own control-flow rules.

Defining a Custom ? Carrier

? is not limited to the two standard enums. A custom carrier participates by implementing Try and FromResidual. Try::branch separates a continuing payload from a short-circuit residual; FromResidual::from_residual rebuilds the enclosing carrier when propagation stops.

enum MyFlow T
    Value T
    Stop I64

enum MyResidual
    Stop I64

impl Try for MyFlow T
    type Output = T
    type Residual = MyResidual

    ~@branch = ->
        match self
            MyFlow::Value value => ControlFlow::Continue value
            MyFlow::Stop code => ControlFlow::Break MyResidual::Stop code

impl FromResidual MyResidual for MyFlow T
    from_residual = residual ->
        match residual
            MyResidual::Stop code => MyFlow::Stop code

next: Bool -> MyFlow I64
next = should_continue ->
    if should_continue
    then MyFlow::Value 41
    else MyFlow::Stop 7

compute: Bool -> MyFlow I64
compute = should_continue ->
    value = next should_continue?
    MyFlow::Value value + 1

main = !->
    match compute true
        MyFlow::Value value => value.println!
        MyFlow::Stop code => code.println!
    match compute false
        MyFlow::Value value => value.println!
        MyFlow::Stop code => code.println!

The successful call unwraps Value 41, adds one, and prints 42. The failing call turns Stop 7 into ControlFlow::Break, reconstructs MyFlow::Stop 7, returns early from compute, and prints 7. Both implementations are ordinary public trait contracts; application code does not need compiler-only declarations.

Current Limitations

Option and Result remain the everyday carriers because their residual conversions are already provided by the standard library. A custom source carrier and the enclosing return carrier may differ, provided the return carrier implements FromResidual for the source’s exact residual type. The compiler selects that implementation; it does not invent a conversion between unrelated errors. Keep Result error parameters concrete and implement any missing conversion explicitly. Rock has no exception syntax; all failure propagation remains visible in a return type and ordinary control flow.