Generics · Lesson 13.5

Generic outcome

Source
Write reusable helpers over T ! E and T? as generic functions, since optionals and fallibles cannot be extended.

Some questions get asked of outcomes again and again: "the value, or this fallback", "did it work?", "what went wrong, if anything?". For a struct you would answer them with methods. Optionals, fallibles and sums are different: they are built into the language rather than declared by a package, so there is no package to own methods on them, and extend refuses them. The answer is a generic function whose parameter spells the form out — T ! E or T? — and which therefore works for every success type and every error type at once.

A parameter that spells out the form

ValueOr takes any fallible and a fallback of its success type:

// The success, or `fallback` when there is none.
func ValueOr<T, E>(outcome: T ! E, fallback: T) -> T {
    return outcome catch { else => fallback };
}

Nothing is written at the call. Given Divide(12, 4), an int32 ! DivisionByZero, the compiler reads T = int32 and E = DivisionByZero straight out of the argument's type, the way it read T out of a Pair<T> in Generic type. The body is the catch fallback you already know; the only new thing is that it is written once for every fallible there will ever be.

Parameter writtenAcceptsHelpers in this lesson
T ! Eany fallible with a success valueValueOr, SuccessOf, ErrorOf
T?any optionalBoth
T?[..]a slice of optionals of one typeCountPresent

Turning one form into another

Two small helpers move between the forms. SuccessOf keeps the success and forgets why there might not be one; ErrorOf keeps the other channel:

func SuccessOf<T, E>(outcome: T ! E) -> T? {
    return match outcome {
        .Success(value) => .Some(value),
        .Failure(_) => none
    };
}

func ErrorOf<T, E>(outcome: T ! E) -> E? {
    return match outcome {
        .Success(_) => none,
        .Failure(error) => .Some(error)
    };
}
flowchart LR
    f(["T ! E"]) -- "SuccessOf" --> s(["T?"])
    f -- "ErrorOf" --> e(["E?"])
    f -- "ValueOr(fallback)" --> t(["T"])
    f -- "Succeeded / Failed" --> b(["bool"])

ErrorOf(broken) is a DivisionByZero?, so Main can open it with a presence pattern and read the error's own field:

match ErrorOf(broken) {
    error? => PrintLine("ErrorOf      cannot divide {} by zero", error.numerator),
    none => PrintLine("ErrorOf      nothing went wrong")
}

Core ships two of these

The two questions everybody asks are already in Core, written exactly this way. Succeeded(outcome) and Failed(outcome) answer which channel an outcome holds, without unwrapping it:

import Core::{ Failed, Succeeded };

Their signatures are Succeeded<T, E>(outcome: T ! E) -> bool and the same for Failed — generic functions over a fallible, like the ones in this lesson.

Optionals too

Both combines two optionals of possibly different types. The postfix ? from Optional propagate does the work: if either is none, the function returns none on the spot.

func Both<T, U>(first: T?, second: U?) -> (T, U)? {
    return (first?, second?);
}

And a helper can take a whole slice of optionals. T?[..] is a slice of T?, so an int32?[4] array is passed straight to it, and is asks whether each one holds a T:

func CountPresent<T>(values: T?[..]) -> uint {
    var count: uint = 0;
    for value in values {
        if value is T {
            count++;
        }
    }
    return count;
}

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// Optionals, fallibles and sums are built into the language rather than declared by a package,
// so there is no package to own methods on them. `extend int32 ! DivisionByZero { ... }` is
// rejected: "cannot extend native type 'int32 ! DivisionByZero'". A question you keep asking of
// an outcome is written instead as a generic function whose parameter spells the form out:
// `T ! E`, or `T?`.
//
// Such a helper works for every success type and every error type at once, and nothing needs
// to be written at the call: `T` and `E` are read straight out of the argument's type.
//
// `Core` ships the two everybody needs, written exactly this way: `Succeeded(outcome)` and
// `Failed(outcome)` answer which channel an outcome holds without unwrapping it.
import Core::{ Failed, Succeeded };
import Io::PrintLine;

struct DivisionByZero {
    numerator: int32;
}

func Divide(numerator: int32, denominator: int32) -> int32 ! DivisionByZero {
    if denominator == 0 {
        fail DivisionByZero { numerator: numerator };
    }
    return numerator / denominator;
}

// The success, or `fallback` when there is none.
func ValueOr<T, E>(outcome: T ! E, fallback: T) -> T {
    return outcome catch { else => fallback };
}

// Keeps the success and forgets why there might not be one.
func SuccessOf<T, E>(outcome: T ! E) -> T? {
    return match outcome {
        .Success(value) => .Some(value),
        .Failure(_) => none
    };
}

// The other channel: the error, if there is one.
func ErrorOf<T, E>(outcome: T ! E) -> E? {
    return match outcome {
        .Success(_) => none,
        .Failure(error) => .Some(error)
    };
}

// Optionals too: both values when both are present, and nothing otherwise.
func Both<T, U>(first: T?, second: U?) -> (T, U)? {
    return (first?, second?);
}

// And a whole array of them: an `int32?[4]` is passed straight to the `T?[..]` slice.
func CountPresent<T>(values: T?[..]) -> uint {
    var count: uint = 0;
    for value in values {
        if value is T {
            count++;
        }
    }
    return count;
}

func Main() -> int {
    let even = Divide(12, 4);
    let broken = Divide(7, 0);
    PrintLine("ValueOr      {} {}", ValueOr(even, -1), ValueOr(broken, -1));
    PrintLine("Succeeded    {} {}", Succeeded(even), Succeeded(broken));
    PrintLine("Failed       {} {}", Failed(even), Failed(broken));
    PrintLine("SuccessOf    {}", SuccessOf(even) ?? 0);

    match ErrorOf(broken) {
        error? => PrintLine("ErrorOf      cannot divide {} by zero", error.numerator),
        none => PrintLine("ErrorOf      nothing went wrong")
    }

    let width: int32? = 80;
    let height: int32? = 24;
    let missing: char8[..]? = none;
    match Both(width, height) {
        size? => PrintLine("Both         {} x {}", size.0, size.1),
        none => PrintLine("Both         incomplete")
    }
    PrintLine("Both         complete: {}", Both(width, missing) is (int32, char8[..]));

    let readings: int32?[4] = [3, none, 5, none];
    PrintLine("CountPresent {} of {}", CountPresent(readings), readings.length);
    return 0;
}

Besides Io, its Rux.toml lists Core under [Dependencies].

Run it

cd Examples/Generics/GenericOutcome
rux run
ValueOr      3 -1
Succeeded    true false
Failed       false true
SuccessOf    3
ErrorOf      cannot divide 7 by zero
Both         80 x 24
Both         complete: false
CountPresent 2 of 4

Common mistakes

Trying to extend an optional or a fallible.
extend int32 ! DivisionByZero { … } fails with error: cannot extend native type 'int32 ! DivisionByZero', and extend int32? { … } with cannot extend native type 'int32?'. The note explains why — a sum, optional, fallible, or unit type has no declaring package to own methods or interface implementations — and the help gives this lesson's answer: write a generic function that takes the native type as a parameter.
Passing a plain value where a fallible is expected.
ValueOr(12, -1) fails with error: function 'ValueOr' requires 2 type arguments, but 0 were provided. A plain int has no error type, so there is nothing to read E from. Only a real T ! E fills in both parameters.

Try it yourself

  1. Write OrElse<T>(value: T?, fallback: T) -> T with ??, and call it with an int32? and a char8[..]?.
  2. Write IsPresent<T>(value: T?) -> bool, then rewrite CountPresent to use it.
  3. Write Either<T, E>(first: T ! E, second: T ! E) -> T ! E that returns first if it succeeded and second otherwise.
  4. Use ValueOr on the result of a function from an earlier lesson that returns a different fallible. Did you have to change ValueOr?

Learn more