silk/effect
Profiles: aarch64-apple-darwin, aarch64-unknown-linux-gnu, aarch64-unknown-linux-gnu-no-libc, wasm32-unknown-unknown, x86_64-unknown-linux-gnu, x86_64-unknown-linux-gnu-no-libc.
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 Result.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 Effect.provideMut<Logger at Audit>. Shared, exclusive, and
owned provider bindings have distinct borrowing and capture behavior.
Gotchas
Typed failures are outcomes that combinators can materialize 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 { Effect }.
Public declarations: 3.
Effect
pub struct EffectThe owner of the Effect combinators.
Details
This struct carries no data and is never constructed by the library. Every combinator is an
inherent member declared in impl Effect, so import silk.effect { Effect } is the one import
that reaches Effect.map(...), Effect.catchAll(...), and the rest. It is unrelated to the
builtin Effect<A ! E ? R> type, which needs no import.
Associated function Effect.log
pub effect<'env> fn log<Args: 'env, 'life1: 'env, 'env>(static template: string<'static>, args: &'life1 Args) -> () ! LogError ? &mut LoggerSends 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.
Associated function Effect.logAt
pub effect<'env> fn logAt<Args: 'env, 'life1: 'env, 'env>(level: LogLevel, static template: string<'static>, args: &'life1 Args) -> () ! LogError ? &mut LoggerSends 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.
Associated function Effect.logTrace
pub effect<'env> fn logTrace<Args: 'env, 'life1: 'env, 'env>(static template: string<'static>, args: &'life1 Args) -> () ! LogError ? &mut LoggerSends one complete message at LogLevel.Trace through the required mutable Logger.
Associated function Effect.logDebug
pub effect<'env> fn logDebug<Args: 'env, 'life1: 'env, 'env>(static template: string<'static>, args: &'life1 Args) -> () ! LogError ? &mut LoggerSends one complete message at LogLevel.Debug through the required mutable Logger.
Associated function Effect.logInfo
pub effect<'env> fn logInfo<Args: 'env, 'life1: 'env, 'env>(static template: string<'static>, args: &'life1 Args) -> () ! LogError ? &mut LoggerSends one complete message at LogLevel.Info through the required mutable Logger.
Associated function Effect.logWarning
pub effect<'env> fn logWarning<Args: 'env, 'life1: 'env, 'env>(static template: string<'static>, args: &'life1 Args) -> () ! LogError ? &mut LoggerSends one complete message at LogLevel.Warning through the required mutable Logger.
Associated function Effect.logError
pub effect<'env> fn logError<Args: 'env, 'life1: 'env, 'env>(static template: string<'static>, args: &'life1 Args) -> () ! LogError ? &mut LoggerSends one complete message at LogLevel.Error through the required mutable Logger.
Associated function Effect.result
pub effect<'env> fn result<'env, A, E, ?R>(protected: once Effect<'env; A ! E ? R>) -> silk/result.Result<A, E> ? RExecutes protected once and converts its success or typed failure into ordinary Result data.
Details
The returned Effect still requires R, because conversion 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 { 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<i32, Problem>.Success {value} => value
Result<i32, Problem>.Failure {error} => error.answer
}
}Associated function Effect.mapBoth
pub effect<'env> fn mapBoth<'env, A, B, E, F, ?R>(self: once Effect<'env; A ! E ? R>, onSuccess: once fn<'env>(A) -> B, onFailure: once fn<'env>(E) -> F) -> B ! F ? RTransforms 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.
When diagnostic observation is active, the mapped failure retains the original selected cause.
Associated function Effect.map
pub effect<'env> fn map<'env, A, B, E, ?R>(self: once Effect<'env; A ! E ? R>, onSuccess: once fn<'env>(A) -> B) -> B ! E ? RApplies 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.
Associated function Effect.mapError
pub effect<'env> fn mapError<'env, A, E, F, ?R>(self: once Effect<'env; A ! E ? R>, onFailure: once fn<'env>(E) -> F) -> A ! F ? RApplies 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.
Mapping runs inside selected recovery, preserving the original failure as its diagnostic cause.
Associated function Effect.flatMap
pub effect<'env> fn flatMap<'env, A, B, E, F, ?R, ?S>(self: once Effect<'env; A ! E ? R>, onSuccess: once fn<'env>(A) -> Effect<'env; B ! F ? S>) -> B ! E | F ? R | SRuns 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.
Associated function Effect.flatten
pub effect<'env> fn flatten<'env, A, E, F, ?R, ?S>(self: once Effect<'env; Effect<'static; A ! F ? S> ! E ? R>) -> A ! E | F ? R | SRuns 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. Effect.flatten(nested) is the nested-Effect form of flatMap.
Associated function Effect.zip
pub effect<'env> fn zip<'env, A, B, E, F, ?R, ?S>(self: once Effect<'env; A ! E ? R>, other: once Effect<'env; B ! F ? S>) -> silk/effect.Pair<A, B> ! E | F ? R | SRuns 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.
Associated function Effect.zip3
pub effect<'env> fn zip3<'env, A, B, C, E, F, G, ?R, ?S, ?T>(self: once Effect<'env; A ! E ? R>, second: once Effect<'env; B ! F ? S>, third: once Effect<'env; C ! G ? T>) -> silk/effect.Triple<A, B, C> ! E | F | G ? R | S | TRuns 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.
Associated function Effect.tap
pub effect<'env> fn tap<'env, A, E, F, ?R, ?S>(self: once Effect<'env; A ! E ? R>, callback: once fn<'env>(A) -> Effect<'env; A ! F ? S>) -> A ! E | F ? R | SContinues 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.
Associated function Effect.catchAll
pub effect<'env> fn catchAll<'env, A, B, E, F, ?R, ?S>(self: once Effect<'env; A ! E ? R>, onFailure: once fn<'env>(E) -> once Effect<'env; B ! F ? S>) -> A | B ! F ? R | SRecovers 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.
The handler's returned Effect runs once and may consume captured values.
Associated function Effect.catch
pub effect<'env> fn catch<'env, S, A, B, E, F, ?R, ?Q>(self: once Effect<'env; A ! E ? R>, onFailure: once fn<'env>(S) -> once Effect<'env; B ! F ? Q>) -> A | B ! Without<E, S> | F ? R | Q where S in ERecovers 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.
Associated function Effect.ensuring
pub effect<'env> fn ensuring<'env, A, E, ?R, ?S>(self: once Effect<'env; A ! E ? R>, finalizer: once Effect<'env; () ? S>) -> A ! E ? R | SRuns a finalizer after the Effect completes, whatever its outcome, and preserves that outcome.
Details
The protected outcome remains owned while the finalizer runs. 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. A typed failure retains its original diagnostic origin and context; it is not raised again. The finalizer runs in the surrounding observation scope. The pending failure does not become a selected recovery cause for failures handled inside the finalizer.
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.
Both inputs may suspend. Destroying the parked composition drops its held outcome and remaining captures exactly once; it does not resume or finish the finalizer.
A trap is not an outcome. It bypasses the finalizer exactly as it bypasses Effect.catch and
every Drop hook.
Associated function Effect.ensuringNonParking
pub effect<'env> fn ensuringNonParking<'env, A, E, ?R, ?S, F>(self: once Effect<'env; A ! E ? R>, finalizer: F) -> A ! E ? R | SRuns a nonparking finalizer on every structured Effect exit and preserves the original outcome.
When to use
Use this operation when structured cancellation must release a scoped resource before it
destroys the protected execution. Use ensuring when the finalizer can suspend.
Details
The finalizer runs exactly once after success or typed failure. It also runs when an owner
cancels a parked Execution. Required providers remain valid until the finalizer completes.
Nested scopes finalize in reverse order. The original outcome remains unchanged.
Gotchas
The finalizer must satisfy Intrinsic.NonParking. Fatal traps bypass this finalizer and all
Drop hooks. Recover a fallible release before you pass it to this operation.
Associated function Effect.useReleaseNonParking
pub effect<'env> fn useReleaseNonParking<'env, Resource: 'env, A, E, ?R, ?S, Release>(resource: Resource, use: for<'scope> once fn<'env>(&'scope mut Resource) -> once Effect<'scope & 'env; A ! E ? R>, release: Release) -> A ! E ? R | SUses one owned resource and releases it after every structured Effect exit.
When to use
Use this operation when suspended work needs an exclusive resource borrow and cancellation must release the owned resource before its frame is destroyed.
Details
use receives one temporary exclusive borrow. That borrow ends before release receives a
fresh exclusive borrow after success, typed failure, or structured cancellation. Release runs
exactly once, nested brackets release in reverse order, and the protected outcome is
preserved. The release Effect must satisfy Intrinsic.NonParking.
Gotchas
Fatal traps bypass release and all Drop hooks. Recover a fallible release to ! never before
returning it from the callback.
Associated function Effect.ifThenElse
pub effect<'env> fn ifThenElse<'env, A, E, F, ?R, ?S>(condition: bool, onTrue: once fn<'env>() -> Effect<'env; A ! E ? R>, onFalse: once fn<'env>() -> Effect<'env; A ! F ? S>) -> A ! E | F ? R | SRuns 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.
Associated function Effect.retry
pub effect<'env> fn retry<'env, A, E, ?R>(self: mut Effect<'env; A ! E ? R>, retries: usize) -> A ! E ? RRuns 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.
Earlier failure payloads are released before the next attempt; the last failure keeps its origin.
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.
Associated function Effect.bindRequirement
pub effect<'env1> fn bindRequirement<'env: 'env1, ?S, A, P: 'env1, E, ?R, 'env1>(self: once Effect<'env; A ! E ? R>, provider: &'env P) -> A ! E ? Without<R, S> where &P provides S from RSatisfies 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.
Associated function Effect.bindRequirementMut
pub effect<'env1> fn bindRequirementMut<'env: 'env1, ?S, A, P: 'env1, E, ?R, 'env1>(self: once Effect<'env; A ! E ? R>, provider: &'env mut P) -> A ! E ? Without<R, S> where &mut P provides S from RSatisfies 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.
Associated function Effect.bindRequirementOwned
pub effect<'env1> fn bindRequirementOwned<'env: 'env1, ?S, A, P: 'env1, E, ?R, 'env1>(self: once Effect<'env; A ! E ? R>, provider: P) -> A ! E ? Without<R, S> where P provides S from RSatisfies 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.
Associated function Effect.provide
pub effect<'env1> fn provide<'env: 'env1, ?S, A, P: 'env1, E, ?R, 'env1>(self: once Effect<'env; A ! E ? R>, provider: &'env P) -> A ! E ? Without<R, S> where &P provides S from RProvides 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.
Associated function Effect.provideMut
pub effect<'env1> fn provideMut<'env: 'env1, ?S, A, P: 'env1, E, ?R, 'env1>(self: once Effect<'env; A ! E ? R>, provider: &'env mut P) -> A ! E ? Without<R, S> where &mut P provides S from RProvides 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)
}Associated function Effect.provideEffect
pub effect<'env> fn provideEffect<'env, ?S, A, P, E, F, ?R, ?Q>(self: once Effect<'env; A ! E ? R>, acquire: Effect<'env; P ! F ? Q>) -> A ! E | F ? Without<R, S> | Q where &mut P provides S from RAcquires 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.
Associated function Effect.suspend
pub effect<'env> fn suspend<'env, A, E, ?R>(deferred: once Effect<'env; A ! E ? R>) -> A ! E ? RDefers 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.
Associated function Effect.of
pub effect<'env> fn of<A: 'env, 'env>(value: A) -> AConstructs 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.
Associated function Effect.sleep
pub effect<'static> fn sleep(howLong: u64) -> () ? &mut MonotonicClockWaits for howLong nanoseconds on the active MonotonicClock provider's logical timeline.
Details
The provider decides whether the wait blocks a host thread or advances virtual time.
Gotchas
A zero duration needs no positive timeline advance. An unrepresentable absolute deadline traps.
Pair
pub struct Pair<A, B>Two success values collected in operand order by zip.
Field first
pub first: AThe first Effect's success value.
Field second
pub second: BThe 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: AThe first Effect's success value.
Field second
pub second: BThe second Effect's success value.
Field third
pub third: CThe third Effect's success value.