Skip to content
You are reading v0.1, 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 handler for Clock { sleep: fn _ms => (), .. } and nothing else, so a test that exercises a retry loop finishes instantly.

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.1.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource