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

Effects

An effect is a set of operations a computation may perform. A hook names them. Performing an operation suspends the computation and asks the nearest handler that answers that operation. do installs the handler around one expression.

// An exception that replaces the computation, and a reader that resumes.

hook Exn { func throw(message: String) -> i64; }
hook Reader { func config() -> i64; }

func checked_div(a: i64, b: i64) -> i64 / {Exn} {
    of (<(b, 0) | eq) {
        True => <"division by zero" | throw,
        False => <(a, b) | div,
    }
}

func scaled(x: i64) -> i64 / {Reader} {
    <(x, config()) | mul
}

proc main | (exit: i32) / {IO} {
    let safe = do (<(10, 0) | checked_div) hn { throw(m) => -1 };
    <safe | println;
    let ok = do (<(10, 2) | checked_div) hn { throw(m) => -1 };
    <ok | println;
    let reading = do (<7 | scaled) hn { config(): resume => <10 | resume };
    <reading | println;
    <0 | exit>
}
-1
5
70

Declaring and performing

hook Exn { func throw(message: String) -> i64; }

func checked_div(a: i64, b: i64) -> i64 / {Exn} {
    of (<(b, 0) | eq) {
        True => <"division by zero" | throw,
        False => <(a, b) | div,
    }
}

throw is an ordinary function in the scope of the hook. checked_div names {Exn} on its arrow, so a caller can see the effect. A handler discharges it:

let safe = do (<(10, 0) | checked_div) hn { throw(m) => -1 };

The clause binds the message and does not resume, so the handler's answer is -1 in place of the division. The clause for a successful division resumes nowhere, because throw never ran, and the body's 5 is the answer.

config is a nullary operation. Its clause names the resumption:

let reading = do (<7 | scaled) hn {
    config(): resume => <10 | resume
};

<10 | resume supplies the operation's result. scaled multiplies 7 by that 10, and the handler answers 70. A clause may resume once, more than once, or not at all. The name resume is conventional; any name works in that position.

The body's answer type is the handler's answer type when there is no return clause. return(x) => e maps the body's value when the body finishes through the normal result. Every operation clause produces that same answer type, or leaves through a command.

Forwarding

A handler that names any operation of an effect covers every operation of that effect. A handler that means to leave some of them to an outer handler ends with _ => forward. The forwarding clause is last, and it has no binder.

// A complete handler, then one that forwards the operation it does not answer.

hook Config {
    func first() -> i64;
    func second() -> i64;
}

func total() -> i64 / {Config} {
    <(first(), second()) | add
}

proc main | (exit: i32) / {IO} {
    let n = do total() hn {
        first(): resume => <1 | resume,
        second(): resume => <2 | resume,
    };
    <n | println;
    let m = do (do total() hn {
        first(): resume => <10 | resume,
        _ => forward,
    }) hn {
        first(): resume => <0 | resume,
        second(): resume => <3 | resume,
    };
    <m | println;
    <0 | exit>
}
3
13

The first do answers both operations: 1 + 2 is 3. The inner do answers first with 10 and forwards second. The outer handler answers second with 3, so the sum is 13. Forwarding keeps the effect in the outward row. The outer do is what discharges it.

Handlers as values

hn Reader { … } builds a handler and installs nothing. do expr reader installs a handler that a let stored. The type of a stored handler is (A hn B / {E} / {F}): the body produces A, the answer is B, the handler discharges the concrete effects E, and the clauses may still perform F. An empty residual row is left off the type.

let reader: (i64 hn String / {Reader}) = hn Reader {
    config(): resume => <10 | resume,
    return(value) => <value | to_string
};

IO is the effect the runtime handles, declared in the prelude as write and write_line. println performs it. A program may install its own IO handler nearer the operation. main may leave {IO} undischarged; anything else is handled before the runtime sees it.

fs::real, args::real, and clock::real are hands. A hand is inlined at each do, as in do expr fs::real. Using the hand's name as a value is an error that tells you to install it with do.

Bare reset expr is a delimiter. It bounds a captured continuation and discharges no effect. It is distinct from the library hand control::reset, which answers control::shift.

The worked effects programs start at examples/effects/effects.sl. The reference page is Effects and handlers.