Effect
Effects are interceptable side effects: effect declares a set of operations (ops), function bodies trigger them with perform, and callers intercept with handle and continue with resume.
Unlike the fail channel, effects propagate strongly along the call chain: whoever calls is responsible, checked by the compiler all the way to the entry point.
Declaration and the Basic Cycle
effect Ask
{
func Prompt(msg: string) -> string;
func Get() -> int { return 42; }
}
func Greet() uses Ask -> string
{
return perform Ask.Prompt("name");
}
func Answer() uses Ask -> int
{
return perform Ask.Get();
}In this example’s Ask, Prompt is a contract op (no method body; whoever triggers it must be intercepted), and Get has a default implementation (if nobody intercepts, the default body runs).
A function signature declares “which effects I will trigger” with uses Ask; the clause sits in the same position as fail (uses, fail, where, return type).
handle and resume
func Caller() uses Ask -> string
{
return Greet();
}
func main() -> int
{
handle
{
print(Caller());
print(Answer());
}
with
{
Ask.Prompt(msg) => resume($"hello, {msg}"),
Ask.Get() => resume(7),
}
return 0;
}hello, name
7handle { block } with { arms } intercepts effects triggered inside the block: Ask.Prompt(msg) deconstructs the op’s arguments, and resume(value) hands the value back to the perform site, where execution continues.
Answer() runs even without a handle of its own: Ask.Get has a default implementation, so with nobody intercepting it returns the default 42; here an arm intercepts it and returns 7 instead.
Strong Propagation via uses
Calling a function that uses E, the caller must either uses E itself (passing it further up) or sit inside a handle covering every op. With neither, the compiler reports:
error MS4011: Function calls a function that uses 'Log'; it must itself declare 'uses Log'.Inheritance and Dispatch by Written Path
effects single-inherit (effect Fs : IO); along the inheritance chain, virtual ops can be overridden.
Dispatch takes the most derived implementation along the path as written in the perform: with an arm, the arm handles it; without one, dispatch follows the written path:
effect Log
{
func fire() -> void;
virtual func Level() -> int { return 1; }
}
effect FileLog : Log
{
override func Level() -> int { return 2; }
}
func Quiet() -> int => perform FileLog.Level();
func BasePath() -> int => perform Log.Level();
print(Quiet(), BasePath());2 1Writing FileLog.Level() takes the override and gives 2; writing Log.Level() takes the base default and gives 1. uses IO covers perform Fs.op(), and the reverse holds as well (judged by the op surface).
The effects Union
effects merges several effects under one name, shaped like the errors union. Members must be same-scope effect declarations or other effects unions (expanded recursively), and uses App expands to cover all (transitive) members:
effect Log
{
func fire(msg: string) -> void;
}
effect Tick
{
func now() -> int;
}
effects App = Log | Tick;
func Work() uses App -> int
{
perform Log.fire("go");
return perform Tick.now();
}
func main() -> int
{
handle
{
print(Work());
}
with
{
Log.fire(msg) => print("log", msg),
Tick.now() => resume(7),
}
return 0;
}log go
7Generic members must be explicitly instantiated (effects App = Cache<int> | Log; is legal). A bare-name perform / on infers to the sole instantiation through the uses declaration. The union appears only in the uses clause position; it is not a value type.
discard: Giving Up the Continuation
discard; means “produce no value and skip what follows”: every statement after the perform in the handle block body is skipped, and control flow lands directly after the handle block:
handle
{
print(Middle());
}
with
{
Log.fire() => discard,
}
print("control reaches here after discard");Inside Middle(), perform Log.fire() jumps out the moment it fires, landing directly on the print after the block.
Notes
- Dispatch order: exact arm → wildcard arm
_(void ops only) → default implementation → defensive crash; - A default implementation’s body must not perform contract ops and must not resume; an overridden op’s uses/fail contracts may narrow but not widen, the same rule as protocol implementations;
handleis a statement, not an expression.
