Effects and rows
Khora functions can carry two orthogonal row-polymorphic dimensions in addition to parameters and return values:
with capabilities required by the computationraises recoverable failures that may leave the computationThey are part of the function type and are checked by the compiler.
Effect declaration
Section titled “Effect declaration”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.
Capability requirement on a function
Section titled “Capability requirement on a function”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.
Several capabilities
Section titled “Several capabilities”fn build_report(id: Id) -> Report with { store: Store, clock: Clock } raises StoreError{ // ...}Open capability row
Section titled “Open capability row”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.
Failure row
Section titled “Failure row”Single type:
raises StoreErrorSeveral failure types:
raises StoreError + ValidationError + HttpErrorOpen failure row:
raises 'erA helper that waits needs no row
Section titled “A helper that waits needs no row”A raises row says how a function can fail, and nothing else. A helper that
waits on a clock, a channel or a fiber and cannot fail declares no row, and it
is still a place its fiber can be canceled — every blocking operation is,
whatever the row:
fn nap(ms: Int) -> () with { clock: Clock } { clock.sleep(ms)}It can be called from a function with any row, fallible or not, without a !
and without widening the caller’s row. A row variable (raises 'er) belongs
where a failure passes through the function — most often a closure the caller
supplies — not on a helper that has no failure of its own.
Effects on function types
Section titled “Effects on function types”Request -> Response with { db: Db }Request -> Response raises HttpErrorRequest -> Response with { db: Db } raises DbError + HttpErrorClauses belong to the function arrow they follow.
Handler expression
Section titled “Handler expression”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.
Install handlers
Section titled “Install handlers”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.
Effect-polymorphic higher-order functions
Section titled “Effect-polymorphic higher-order functions”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.
Failure propagation and handling
Section titled “Failure propagation and handling”operation()!operation()! catch { Error::Case(value) => recover(value),}See Failures for raise, !, catch, and attempt semantics.
Capabilities and failures are independent
Section titled “Capabilities and failures are independent”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.4.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource