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 EffectThe 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 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.
logAt
pub effect fn logAt(level: LogLevel, message: string) -> () ! 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.
logTrace
pub effect fn logTrace(message: string) -> () ! LogError ? &mut LoggerSends one complete message at LogLevel.Trace through the required mutable Logger.
logDebug
pub effect fn logDebug(message: string) -> () ! LogError ? &mut LoggerSends one complete message at LogLevel.Debug through the required mutable Logger.
logInfo
pub effect fn logInfo(message: string) -> () ! LogError ? &mut LoggerSends one complete message at LogLevel.Info through the required mutable Logger.
logWarning
pub effect fn logWarning(message: string) -> () ! LogError ? &mut LoggerSends one complete message at LogLevel.Warning through the required mutable Logger.
logError
pub effect fn logError(message: string) -> () ! LogError ? &mut LoggerSends 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> ? RExecutes 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 ? 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.
map
pub effect fn map<A, B, E, ?R>(self: once Effect<A ! E ? R>, onSuccess: once fn(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.
mapError
pub effect fn mapError<A, E, F, ?R>(self: once Effect<A ! E ? R>, onFailure: once fn(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.
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 | 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.
flatten
pub effect fn flatten<A, E, F, ?R, ?S>(self: once Effect<Effect<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. 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: 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.
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 | 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.
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 | 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.
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 | 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.
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 | 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.
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 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.
ensuring
pub effect fn ensuring<A, E, ?R, ?S>(self: once Effect<A ! E ? R>, finalizer: once Effect<() ? S>) -> A ! E ? R | SRuns 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 | 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.
retry
pub effect fn retry<A, E, ?R>(self: mut Effect<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.
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 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.
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 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.
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 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.
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 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.
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 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)
}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 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.
suspend
pub effect fn suspend<A, E, ?R>(deferred: once Effect<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.
of
pub effect fn of<A>(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.