Silk

silk/effect

Builds lazy computations by transforming success, recovering typed failure, supplying services, and controlling sequencing and cleanup.

When to use

An Effect<A ! E ? R> describes a computation with three visible channels: it can succeed with A, fail with typed value E, and require providers R. Use map and flatMap to continue success, mapError, catch, or catchAll for typed failures, provide or provideMut for lexical services, and ensuring for typed-outcome cleanup. Direct run remains clearest for straightforward sequential code.

Details

Combinators are lazy: passing an Effect does not run it. Most accept a once Effect, so that particular value can execute at most once; retry explicitly accepts a reusable Effect. Sequential combinators stop at the first typed failure unless a recovery operation handles it. Their signatures show how failure and requirement rows combine, so composing two steps normally produces the unions ! E | F and ? R | S.

A provider operation removes one exact capability, role, and access entry from the requirement row. When one provider could satisfy multiple entries, select the intended entry explicitly as the first generic argument, for example provideMut<Logger at Audit>. Shared, exclusive, and owned provider bindings have distinct borrowing and capture behavior.

Gotchas

Typed failures are outcomes that combinators can reify and recover. Traps are not: they bypass catchAll, ensuring, and Drop hooks. suspend crosses the stack-safe execution boundary while preserving all three channels exactly; frame exhaustion is fatal.

Examples

Transform and continue a successful computation

import silk.effect { Effect }

struct Problem {
  code: i32
}

effect fn read(value: i32) -> i32
! Problem {
  if value < 0 {
    fail Problem {code: 0}
  }
  return value
}

fn double(value: i32) -> i32 {
  return value * 2
}

effect fn addTwo(value: i32) -> i32
! Problem {
  return value + 2
}

effect fn recover(error: Problem) -> i32 {
  return error.code
}

pub fn main() -> i32 {
  let computation = read(20)
    |> Effect.map(double)
    |> Effect.flatMap(addTwo)
  return run Effect.catchAll(computation, recover)
}

Supply a custom service for one lexical computation

Operation is declared inline below.

import silk.effect { Effect }

service Clock {
  effect fn value() -> i32 ? &Clock
}

struct FixedClock {
  value: i32
}

impl Clock for FixedClock {
  effect fn value(self: &Self) -> i32 {
    return self.value
  }
}

effect fn readClock() -> i32
? &Clock {
  return run Clock.value()
}

pub fn main() -> i32 {
  let clock = FixedClock {value: 42}
  return run Effect.provide(readClock(), &clock)
}

Recover a typed failure into success

import silk.effect { Effect }

struct Problem {
  answer: i32
}

effect fn load() -> i32
! Problem {
  fail Problem {answer: 42}
}

effect fn recover(error: Problem) -> i32 {
  return error.answer
}

pub fn main() -> i32 {
  return run Effect.catchAll(load(), recover)
}

Import as Effect with import silk.effect.

Public declarations: 32.

Effect

pub struct Effect

The importable name of the silk.effect module scope.

Details

This struct carries no data and is never constructed by the library. Importing it as import silk.effect { Effect } names the module scope, so Effect.map(...) and every other combinator resolve through it exactly as through a module alias. It is unrelated to the builtin Effect<A ! E ? R> type, which needs no import.

log

pub effect fn log(message: string) -> () ! LogError ? &mut Logger

Sends one complete message at LogLevel.Info through the required mutable Logger.

Details

The logger decides where the message goes. Logging may fail with LogError, and this wrapper neither buffers nor recovers that failure. Use logAt when the level is not Info.

logAt

pub effect fn logAt(level: LogLevel, message: string) -> () ! LogError ? &mut Logger

Sends one complete message at level through the required mutable Logger.

Details

The message is one logging event rather than a fragment. The provider controls formatting and destination; its LogError propagates unchanged.

logTrace

pub effect fn logTrace(message: string) -> () ! LogError ? &mut Logger

Sends one complete message at LogLevel.Trace through the required mutable Logger.

logDebug

pub effect fn logDebug(message: string) -> () ! LogError ? &mut Logger

Sends one complete message at LogLevel.Debug through the required mutable Logger.

logInfo

pub effect fn logInfo(message: string) -> () ! LogError ? &mut Logger

Sends one complete message at LogLevel.Info through the required mutable Logger.

logWarning

pub effect fn logWarning(message: string) -> () ! LogError ? &mut Logger

Sends one complete message at LogLevel.Warning through the required mutable Logger.

logError

pub effect fn logError(message: string) -> () ! LogError ? &mut Logger

Sends one complete message at LogLevel.Error through the required mutable Logger.

result

pub effect fn result<A, E, ?R>(protected: once Effect<A ! E ? R>) -> silk/result.Result<A, E> ? R

Executes protected once and converts its success or typed failure into ordinary Result data.

Details

The returned Effect still requires R, because reification does not provide services. Its typed failure row is empty: an E becomes Failure data instead of propagating. Traps are not typed failures and therefore are not captured.

Examples

Inspect a failure as ordinary data

import silk.effect { Effect }

import silk.result as Result

struct Problem {
  answer: i32
}

effect fn load() -> i32
! Problem {
  fail Problem {answer: 42}
}

pub fn main() -> i32 {
  let completed = run Effect.result(load())
  return match move completed {
    Result.Result<i32, Problem> {value: outcome} => match move outcome {
      Result.Success<i32> {value} => value
      Result.Failure<Problem> {error} => error.answer
    }
  }
}

mapBoth

pub effect fn mapBoth<A, B, E, F, ?R>(self: once Effect<A ! E ? R>, onSuccess: once fn(A) -> B, onFailure: once fn(E) -> F) -> B ! F ? R

Transforms both possible typed outcomes with pure callbacks.

Details

Exactly one callback runs after self: onSuccess changes A to B, while onFailure changes E to F and re-raises it. Requirements are preserved, and traps bypass both callbacks.

map

pub effect fn map<A, B, E, ?R>(self: once Effect<A ! E ? R>, onSuccess: once fn(A) -> B) -> B ! E ? R

Applies a pure callback to success while preserving typed failure and requirements.

Details

onSuccess runs once only after self succeeds. A typed failure propagates without invoking the callback. Use flatMap when the callback itself needs an Effect.

mapError

pub effect fn mapError<A, E, F, ?R>(self: once Effect<A ! E ? R>, onFailure: once fn(E) -> F) -> A ! F ? R

Applies a pure callback to typed failure while preserving success and requirements.

Details

onFailure runs once only when self fails, and its returned F becomes the new typed failure. Success bypasses the callback. This changes an error value; use catchAll to recover to success.

flatMap

pub effect fn flatMap<A, B, E, F, ?R, ?S>(self: once Effect<A ! E ? R>, onSuccess: once fn(A) -> Effect<B ! F ? S>) -> B ! E | F ? R | S

Runs self, then continues its success with an effectful callback.

Details

The callback is not invoked when self fails. Its failure and requirement rows join those of self, and its success becomes the overall success. This is the general sequencing combinator; use direct run statements when named intermediate values are clearer.

flatten

pub effect fn flatten<A, E, F, ?R, ?S>(self: once Effect<Effect<A ! F ? S> ! E ? R>) -> A ! E | F ? R | S

Runs an outer Effect and then the inner Effect it produces.

Details

If the outer Effect fails, no inner Effect is available or run. The two failure rows and the two requirement rows are joined. flatten(nested) is the nested-Effect form of flatMap.

Pair

pub struct Pair<A, B>

Two success values collected in operand order by zip.

Field first

pub first: A

The first Effect's success value.

Field second

pub second: B

The second Effect's success value.

Triple

pub struct Triple<A, B, C>

Three success values collected in operand order by zip3.

Field first

pub first: A

The first Effect's success value.

Field second

pub second: B

The second Effect's success value.

Field third

pub third: C

The third Effect's success value.

zip

pub effect fn zip<A, B, E, F, ?R, ?S>(self: once Effect<A ! E ? R>, other: once Effect<B ! F ? S>) -> silk/effect.Pair<A, B> ! E | F ? R | S

Runs two Effects in declaration order and collects both success values.

Details

self runs first. Only after it succeeds does other run, so a first-step typed failure skips the second step. Both failure and requirement rows are joined. Use the public Pair.first and Pair.second fields to read the results; this is sequencing, not parallel execution.

zip3

pub effect fn zip3<A, B, C, E, F, G, ?R, ?S, ?T>(self: once Effect<A ! E ? R>, second: once Effect<B ! F ? S>, third: once Effect<C ! G ? T>) -> silk/effect.Triple<A, B, C> ! E | F | G ? R | S | T

Runs three Effects in declaration order and collects all three success values.

Details

The operands run from left to right. Each later operand is skipped if an earlier one fails, and all three failure and requirement rows are joined. Use this fixed-arity operation when all three successful values are needed together; it does not run them concurrently.

tap

pub effect fn tap<A, E, F, ?R, ?S>(self: once Effect<A ! E ? R>, callback: once fn(A) -> Effect<A ! F ? S>) -> A ! E | F ? R | S

Continues success with a callback that returns the value to expose as the overall success.

Details

The callback receives and consumes the original A, then must produce an A of its own. This is useful for effectful observation followed by returning the observed value, but it does not automatically preserve the original value. A failure from either step propagates, and the callback is skipped when self fails.

catchAll

pub effect fn catchAll<A, B, E, F, ?R, ?S>(self: once Effect<A ! E ? R>, onFailure: once fn(E) -> Effect<B ! F ? S>) -> A | B ! F ? R | S

Recovers every typed failure in the protected row with another Effect.

Details

The handler receives the complete failure value and runs only on typed failure. The protected failure row is removed in full; only the handler's own F can fail afterwards. Success bypasses the handler, requirements from both paths remain, and traps are not recovered. Use catch to handle one selected member while leaving the other failures in the row.

catch

pub effect fn catch<S, A, B, E, F, ?R, ?Q>(self: once Effect<A ! E ? R>, onFailure: once fn(S) -> Effect<B ! F ? Q>) -> A | B ! Without<E, S> | F ? R | Q where S in E

Recovers one selected typed failure.

Details

Effect.catch<E>(protected, handler) names one member of the protected row. The handler runs only for that member, its own failures join the result row, and every nonmatching member of the protected row propagates unchanged as the residual. Success bypasses the handler.

A success bypasses the handler. A matching S invokes it once; nonmatching typed failures propagate in Without<E, S>, and the handler's failures join as F. Requirements from either path remain. Traps are not selected or recovered. Use catchAll when the handler should receive the entire failure value regardless of its union member.

ensuring

pub effect fn ensuring<A, E, ?R, ?S>(self: once Effect<A ! E ? R>, finalizer: once Effect<() ? S>) -> A ! E ? R | S

Runs a finalizer after the Effect completes, whatever its outcome, and preserves that outcome.

Details

The protected Effect is reified into Result data before the finalizer runs, which is what fixes the order: a typed failure reaches this body as data rather than as a propagation, so the protected Effect's own frame — and every local it cleans up — is already gone by the time the finalizer starts. The finalizer therefore exits last, in reverse acquisition order against the cleanup it wraps. The original success value or the original typed failure is only handed on afterwards, so a recovering caller never observes the outcome before the finalizer has run.

The finalizer is typed ! never: it cannot fail, so there is no second outcome to reconcile with the one being preserved. A caller with fallible cleanup recovers it into ! never first — for example with Effect.catch — and decides there what a failed release means.

A trap is not an outcome. It bypasses the finalizer exactly as it bypasses Effect.catch and every Drop hook.

ifThenElse

pub effect fn ifThenElse<A, E, F, ?R, ?S>(condition: bool, onTrue: once fn() -> Effect<A ! E ? R>, onFalse: once fn() -> Effect<A ! F ? S>) -> A ! E | F ? R | S

Runs exactly one of two suspended branches, selected by a condition.

Details

The arms are suspended rather than pre-built: each is a once fn() that produces its branch's Effect, and only the selected arm is invoked. The branch not taken is therefore never constructed, which is a stronger guarantee than merely not being run — construction-time work inside an arm never happens, and an arm whose body is only well-defined under the condition is safe to write. Two pre-built Effect arguments would instead be evaluated at the call site, before either was chosen.

The unselected arm is released here with an explicit drop move, so the affine obligation for the arm that is never invoked is discharged in this source rather than left to a generated release.

The result's failure and requirement rows are the union of the two arms', so the caller discharges whatever either branch could need without knowing which one will be selected. Both arms must agree on the success type.

The name is ifThenElse rather than if because if is a keyword and Silk has no raw-identifier form, so the declaration itself could not be spelled if.

retry

pub effect fn retry<A, E, ?R>(self: mut Effect<A ! E ? R>, retries: usize) -> A ! E ? R

Runs a reusable Effect once, then repeats it after typed failure up to retries more times.

Details

Success stops the loop immediately. If every attempt fails, the final typed failure propagates. retries == 0 means one initial attempt. Traps are not retried, and self must be reusable (mut Effect) because the same computation may execute more than once.

bindRequirement

pub effect fn bindRequirement<?S, A, P, E, ?R>(self: once Effect<A ! E ? R>, provider: &P) -> A ! E ? Without<R, S> where &P provides S from R

Satisfies one exact shared service requirement with a provider borrowed for this execution.

Details

The selected row S is the first generic argument. Selection may use exact capability identity or one unique service-conformance witness, but a shared provider selects only a stored shared requirement. Subtraction removes that exact stored capability-role-access member. The borrow is lexical: the provider remains owned by the caller after the Effect completes.

bindRequirementMut

pub effect fn bindRequirementMut<?S, A, P, E, ?R>(self: once Effect<A ! E ? R>, provider: &mut P) -> A ! E ? Without<R, S> where &mut P provides S from R

Satisfies one service requirement with a provider borrowed exclusively for this execution.

Details

An exclusive provider may satisfy a stored shared or exclusive requirement. The selected row is still the exact stored member, so providing &mut P for a shared &Logger removes &Logger, not a synthesized &mut Logger. The caller regains exclusive access after the Effect completes.

bindRequirementOwned

pub effect fn bindRequirementOwned<?S, A, P, E, ?R>(self: once Effect<A ! E ? R>, provider: P) -> A ! E ? Without<R, S> where P provides S from R

Satisfies one typed service requirement by taking ownership of its provider.

Details

Owned selection accepts shared or exclusive stored requirements. Moving an affine provider makes the resulting Effect take-once; an ordinary Copy provider is captured by snapshot and remains repeatable. The provider is released with the Effect's lexical scope; it is not returned.

provide

pub effect fn provide<?S, A, P, E, ?R>(self: once Effect<A ! E ? R>, provider: &P) -> A ! E ? Without<R, S> where &P provides S from R

Provides a shared service for one lexical Effect execution.

Details

This is the user-facing alias of bindRequirement. The provider is borrowed, the exact selected shared row member is removed, and every unrelated requirement remains visible in the return type.

provideMut

pub effect fn provideMut<?S, A, P, E, ?R>(self: once Effect<A ! E ? R>, provider: &mut P) -> A ! E ? Without<R, S> where &mut P provides S from R

Provides a service from an exclusive borrow for one lexical Effect execution.

Details

Selection scans the whole input row and subtracts the exact stored member selected by provider identity or one unique conformance witness. Canonical row order is never selection evidence. Supply the selected row first when one provider could satisfy multiple entries. The provider is not moved and becomes exclusively available to the caller again after execution.

Examples

Mutate a custom service for one computation

import silk.effect { Effect }

service Counter {
  effect fn next() -> i32 ? &mut Counter
}

struct Counting {
  value: i32
}

effect fn next(self: &mut Counting) -> i32 {
  self.value = self.value + 1
  return self.value
}

impl Counter for Counting {
  next: Counting.next
}

effect fn read() -> i32
? &mut Counter {
  return run Counter.next()
}

pub fn main() -> i32 {
  let mut counter = Counting {value: 41}
  return run Effect.provideMut<Counter>(read(), &mut counter)
}

provideEffect

pub effect fn provideEffect<?S, A, P, E, F, ?R, ?Q>(self: once Effect<A ! E ? R>, acquire: Effect<P ! F ? Q>) -> A ! E | F ? Without<R, S> | Q where &mut P provides S from R

Acquires and lexically provides one typed service requirement.

Details

acquire runs on every execution, and its F failures propagate before self begins. A successful provider is borrowed exclusively while self runs and is released before either self's success or typed failure becomes observable to the caller. Retrying the returned Effect therefore acquires a fresh provider for each attempt. The result keeps acquisition requirements Q and every requirement in R except the selected entry S.

suspend

pub effect fn suspend<A, E, ?R>(deferred: once Effect<A ! E ? R>) -> A ! E ? R

Defers one Effect through stack-safe execution while preserving its channels exactly.

Details

Suspension adds no allocator requirement or recoverable allocation failure. Coroutine frames belong to the compiler-owned execution stack, whose exhaustion is a fatal trap. Use this at a recursive or deeply chained boundary that must yield through the stack-safe Effect executor; ordinary laziness alone does not require suspension.

of

pub effect fn of<A>(value: A) -> A

Constructs an Effect that succeeds with the captured value when run.

Details

Calling of evaluates and transfers value immediately as an ordinary function argument, but the returned Effect does not produce that value until execution. The Effect has no typed failure or requirement channels. For an affine value, constructing the Effect transfers ownership into it, so that Effect can be consumed only once.

On this page