tripon[github]

opt, result and ?

Two things cause most of the incidents in software written by people and by agents alike: a value that turns out to be missing, and an operation that turns out to have failed. Tripon makes both of them values with two variants each, so the language has no need for null and no need for exceptions.

typevariantsmeans
opt<T>some(v), nonea value that may be missing
result<T, E>ok(v), err(e)an operation that may fail

No null, and it is not a convention

null is not a keyword to be avoided, it is not in the grammar. Writing it is a parse error with the replacement named in the message:

ralt check null.tp
null.tp:4:13: error: null and undefined do not exist; use opt (T) for may-be-missing values
  [E0204 no-null] null and undefined do not exist; use opt<T>

So a lookup that may find nothing returns opt<T>, and every reader of that value has to say what happens when it is none:

tripon
use tier core;

// T3: opt<i32> - constructors, match, ?-propagation inside an opt-returning fn.

fn find(x: i32) -> opt<i32> {
    if x > 0 { return some(x); }
    return none;
}

fn bump(x: i32) -> opt<i32> {
    let v = find(x)?;      // none propagates out of the opt-returning fn
    return some(v + 1);
}

fn show(o: opt<i32>) {
    match o {
        some(v) => { print_i32(v); }
        none => { print_str("none"); }
    }
}

fn main() -> i32 {
    show(bump(5));         // some path: 6
    show(bump(-3));        // none path
    show(find(-1));        // none, no spread
    let d: opt<i32> = none;
    show(d);               // typed bare none
    let e: opt<i32> = some(9);
    show(e);               // typed some
    return 0;
}
examples/opt_some.tp
ralt run examples/opt_some.tp
6
none
none
none
9

Note the two bindings near the end: let d: opt<i32> = none; needs the type, because a bare none carries no type of its own — nothing is guessed. some(9) infers its T from the argument and the annotation agrees with it.

? is propagation, not unwrapping

? does not mean "unwrap, and fail at runtime if there is nothing". It means if this is none (or err), return it from the function I am in right now. That is why bump can be written without a match: its own return type is opt<i32>, so the propagation has somewhere to go.

The caller of a ? must therefore return the same shape. A ? in a function returning i32 is not a runtime hazard, it is a compile error.

Failures are values too

result<T, E> is the same idea with an error payload you choose — here a str:

tripon
use tier core;

fn parse(s: str) -> result<i32, str> {
    if s == "" { return err("empty input"); }
    return ok(42);
}

fn validate(n: i32) -> result<i32, str> {
    if n < 10 { return err("too small"); }
    return ok(n * 2);
}

// ?-propagation across two fns returning result<i32, str>
fn run() -> result<i32, str> {
    let n = parse("hi")?;
    let m = validate(n)?;
    return ok(m);
}

fn run_bad() -> result<i32, str> {
    let n = parse("")?;    // err propagates: parse gives err, run gives it on
    return ok(n);
}

fn handle(r: result<i32, str>) -> i32 {
    match r {
        ok(v) => { return v; }
        err(e) => { print_str(e); return -1; }
    }
}

fn main() -> i32 {
    let good = handle(run());
    print_i32(good);
    let bad = handle(run_bad());
    print_i32(bad);
    return 0;
}
examples/result_ok.tp
ralt run examples/result_ok.tp
84
empty input
-1

run spreads twice; the first err to appear leaves the function immediately with that error. handle is the only place that decides what a failure means, and it is explicit about it.

A dropped result will not build

The mistake this design exists to prevent is the ignored failure — the return value nobody checks. In Tripon, evaluating a result and discarding it is a compile error:

ralt check unhandled.tp
unhandled.tp:8:5: error: result<i32, str> value evaluated and dropped
  [E0205 unhandled-result] Result value ignored; spread with ? or match

The two repairs are the two spellings: ? if the failure belongs to your caller, match if it belongs to you. There is no third option where the failure quietly evaporates.

Propagation has to have somewhere to go

Because ? is a return, the function containing it must be able to return what it propagates. Both halves of that are checked:

ralt check spread.tp
spread.tp:9:13: error: '?' inside a fn returning i32 cannot propagate; match it or return result<i32, str>
  [E0205 unhandled-result] Result value ignored; spread with ? or match
ralt check errmix.tp
errmix.tp:8:13: error: cannot spread result<i32, str> out of a fn returning result<i32, i32>
  [E0202 type-mismatch] types must match exactly; no implicit conversion exists; use cast<T>(x)

The error type is part of the type: a function that can fail with a str cannot quietly become a function that fails with an i32, in the same way that an i32 cannot become an i64.

Where this shows up in the runtime

opt<T> and result<T, E> are ordinary enums with two variants each, and the optimizer packs them into a niche where the types prove one exists, so a result<i32, str> is not a struct wrapping a flag in a box. The variants are checked at compile time and matched exhaustively like any other enum — a match that drops a variant is E0206, the same as it would be for an enum you wrote.

Next: arrays and strings, or the trap contract if you want to know what the runtime does when arithmetic goes wrong.