Skip to content
You are reading v0.3, which is not the current release. Go to v0.4.

Effects and rows

Khora functions can carry two orthogonal row-polymorphic dimensions in addition to parameters and return values:

with capabilities required by the computation
raises recoverable failures that may leave the computation

They are part of the function type and are checked by the compiler.

pub effect Clock {
now: () -> Instant,
}

An effect with fallible operations:

pub effect Store {
load: Id -> User raises StoreError,
save: User -> () raises StoreError,
}

General form:

pub? effect Name<TypeParams>? {
operation: FunctionType,
...
}

An effect name is the type of handlers implementing that effect.

The Clock above is a stand-in cut down to one operation. The real one is std::clock::Clock: it lives in its own module rather than in std::env, and it has four operations, sleep among them. Waiting is an operation on the capability on purpose — a fake clock is an ordinary handler, so a test that exercises a retry loop finishes instantly:

handler for Clock {
sleep: fn _ms => (),
unix_seconds: fn () => 0,
unix_millis: fn () => 0,
monotonic_millis: fn () => 0,
}

A handler gives every operation of its effect. There is no .. for the rest, and leaving one out is an error naming the operation that is missing, so the fake above writes all four even though only sleep is what the test is for.

An operation may be generic in a row, but not in a type

Section titled “An operation may be generic in a row, but not in a type”

An operation’s raises row may mention a row variable the operation itself binds. It is quantified per call, not per handler, so one handler serves callers whose failures are unrelated:

pub effect Nursery {
adopt: (Fiber<(), 'er>) -> (),
}

An operation cannot introduce a type parameter the same way. The asymmetry follows from representation. A capability crosses as evidence and an error as a tag, so the handler’s closure is the same machine code for every row — nothing in it depends on which failures the caller can raise. A type parameter decides a layout and must be monomorphized, and a handler’s fields are closures, which have nowhere to put a per-layout instantiation.

An effect may still take type parameters of its own on the effect declaration, as the general form above shows. Those are fixed when the handler is written; a row variable on an operation is not.

fn load_user(id: Id) -> User
with { store: Store }
raises StoreError
{
store.load(id)!
}

with { store: Store } introduces the label store into the body with the operations of Store.

fn build_report(id: Id) -> Report
with { store: Store, clock: Clock }
raises StoreError
{
// ...
}
fn use_clock<A, 'ef>(body: () -> A with { clock: Clock | 'ef }) -> A
with { clock: Clock | 'ef }
{
body()
}

A row variable preserves additional requirements not interpreted by the generic function.

Single type:

raises StoreError

Several failure types:

raises StoreError + ValidationError + HttpError

Open failure row:

raises 'er

raises 'er is usually seen on a higher-order signature, where the row belongs to a closure the caller supplies. It belongs on an ordinary function too, and there it means something worth having a name for:

fn nap<'er>(ms: Int) -> () with { clock: Clock } raises 'er {
clock.sleep(ms)!
}

A helper with no failure of its own that is still a cancellation point. The ! inside is what makes it one; the 'er is what lets it sit inside a caller that raises anything at all without widening that caller’s row. Written raises Never it would be a helper nobody could call from a fallible function without a discharge; written with a concrete error it would invent a failure it does not have.

Any helper that waits — on a clock, a channel, a lock — wants this signature.

Request -> Response with { db: Db }
Request -> Response raises HttpError
Request -> Response
with { db: Db }
raises DbError + HttpError

Clauses belong to the function arrow they follow.

handler for Clock {
now: fn () => fixed_instant,
}

Fallible operation implementation:

handler for Store {
load: fn id => load_from_disk(id)!,
save: fn user => save_to_disk(user)!,
}

The implementation must satisfy the effect’s operation types.

One expression:

job() with {
clock: fixed_clock,
}

Lexical block:

with {
clock: fixed_clock,
store: memory_store,
} {
job()!
}

Named contexts and overrides are specified in Capabilities.

fn map<A, B, 'ef, 'er>(
values: List<A>,
f: A -> B with 'ef raises 'er,
) -> List<B>
with 'ef
raises 'er
{
// ...
}

The higher-order function preserves the capabilities and failures of its callback rather than wrapping it in a separate effect value.

operation()!
operation()! catch {
Error::Case(value) => recover(value),
}

See Failures for raise, !, catch, and attempt semantics.

A capability operation may be infallible, and a pure function may still raise a typed domain failure. with records authority; raises records recoverable non-local control flow. Neither implies the other.

Khora 0.3.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource