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
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.
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 row variable on an ordinary function
Section titled “A row variable on an ordinary function”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.
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.2.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource