Effects
Letlang provides the ability to delegate a computation to the caller. This is
done by first declaring the signature of an effect (see
this page), then using the perform
keyword, the
callee can "call" an effect handler defined by the caller using a do
expression.
Syntax
Show source
effect_call_expression
= "perform" effect_ref "(" expression ("," expression)* ","? ")"
Show source
effect_ref
= symbol_path ("<" type_expression ("," type_expression)* ","? ">")?
Example
Consider this effect:
let log: effect[(string) -> @ok];
@ok := perform log("hello world");
Or:
let compare<t>: effect[(t, t) -> @lesser | @equal | @greater]
@lesser := perform compare<int>(1, 2);
Semantics
When calling an effect, the arguments MUST be evaluated in order.
Calling an effect MUST interrupt the function's execution. If a matching effect
handler is found in an outer do
expression, the handler MUST be executed.
The function MUST resume with the handler's return value.
If the handler throws an exception, that exception MUST be thrown from the effect call-site:
let will_throw: effect[() -> @ok];
@error_caught := do {
do {
perform will_throw();
}
catch {
(@oops) -> @error_caught,
};
}
intercept will_throw {
() -> throw @oops,
};
NB: This can be shortened to:
@error_caught := do {
perform will_throw();
}
intercept will_throw {
() -> throw @oops,
}
catch {
(@oops) -> @error_caught,
};
If no handler is found, the effect call MUST be handled by the runtime.
The runtime provides effect handlers for builtin effects. Those effects will be handled according to the runtime specification. However, if the effect is a user-defined effect, the runtime MUST throw an exception instead. The value of that exception is defined by the implementation.