silk.core.effect.
Combinators for lazy effect values: transform success, recover typed failures, supply services, and run cleanup.
When to use#
An Effect<A ! E ? R> is a computation that can succeed with A, fail with a typed value E,
and needs the providers R. Use map and flatMap to continue after success. Use
mapError, catch, or catchAll for typed failures. Use provide or provideMut
to supply a service, and ensuring for cleanup. For plain sequential code, direct run
statements are often clearer than combinators.
Details#
Combinators are lazy. When code passes an effect to a combinator, the effect does not run. Most
combinators take a once Effect, so that value runs at most one time. retry takes a
reusable effect because it can run the effect more than one time.
Sequential combinators stop at the first typed failure, unless a recovery operation handles it.
The signatures show how the channels combine. Two steps usually give the unions ! E | F and
? R | S.
A provider operation removes one exact requirement from R. If one provider can satisfy more
than one requirement, write the requirement as the first type argument, as in
Effect.provideMut<Logger at Audit>. Shared, exclusive, and owned providers have different
borrow and capture rules.
Gotchas#
A typed failure is an outcome that combinators can catch. A trap is not an outcome. A trap
bypasses catchAll, ensuring, and Drop hooks. If the execution stack is exhausted, the
program traps.
Examples#
Transform and continue a successful computation#
import silk.core.effect { Effect }
struct Problem {
code: i32,
}
effect fn read(value: i32) -> i32
! Problem {
if value < 0 {
fail Problem { code: -1 }
}
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)
let answer = run Effect.catchAll(computation, recover)
if answer != 42 {
return 1
}
return 0
}Supply a custom service for one computation#
import silk.core.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 }
let value = run Effect.provide(readClock(), &clock)
if value != 42 {
return 1
}
return 0
}Recover a typed failure into success#
import silk.core.effect { Effect }
struct Problem {
fallback: i32,
}
effect fn load() -> i32
! Problem {
fail Problem { fallback: 42 }
}
effect fn recover(error: Problem) -> i32 {
return error.fallback
}
pub fn main() -> i32 {
let value = run Effect.catchAll(load(), recover)
if value != 42 {
return 1
}
return 0
}Import as Effect with import silk.core.effect { Effect }.
Public declarations: 3.
Effect#
pub struct EffectThe owner type of the effect combinators. It has no fields and is never constructed.
Details#
import silk.core.effect { Effect } gives access to Effect.map, Effect.catchAll, and the other
combinators. This struct is not the built-in type Effect<A ! E ? R>, which needs no import.
Implementation Effect#
impl EffectAssociated function log#
pub effect fn log<Args>(
static template: string<'static>,
args: &Args
) -> () ! LogError ? &mut LoggerSends one log message at LogLevel.Info to the active Logger.
When to use#
Use log for an ordinary progress message. Use logAt or a level function, such as
logWarning, for a different level.
Details#
template is a static format template. The compiler checks it against args at compile time.
The Logger provider selects the output format and destination. The message is one complete
log event.
Errors#
LogErrorif theLoggerprovider fails. This function does not buffer or retry.
Associated function logAt#
pub effect fn logAt<Args>(
level: LogLevel,
static template: string<'static>,
args: &Args
) -> () ! LogError ? &mut LoggerSends one log message at level to the active Logger.
When to use#
Use logAt when the level is a value at runtime. Use logTrace, logDebug, logInfo,
logWarning, or logError when the level is fixed.
Details#
template is a static format template. The compiler checks it against args at compile time.
The Logger provider selects the output format and destination.
Errors#
LogErrorif theLoggerprovider fails. This function does not buffer or retry.
Associated function logTrace#
pub effect fn logTrace<Args>(
static template: string<'static>,
args: &Args
) -> () ! LogError ? &mut LoggerSends one log message at LogLevel.Trace to the active Logger.
Details#
This function has the same behavior as logAt with LogLevel.Trace.
Errors#
LogErrorif theLoggerprovider fails. This function does not retry.
Associated function logDebug#
pub effect fn logDebug<Args>(
static template: string<'static>,
args: &Args
) -> () ! LogError ? &mut LoggerSends one log message at LogLevel.Debug to the active Logger.
Details#
This function has the same behavior as logAt with LogLevel.Debug.
Errors#
LogErrorif theLoggerprovider fails. This function does not retry.
Associated function logInfo#
pub effect fn logInfo<Args>(
static template: string<'static>,
args: &Args
) -> () ! LogError ? &mut LoggerSends one log message at LogLevel.Info to the active Logger.
Details#
This function has the same behavior as logAt with LogLevel.Info.
Errors#
LogErrorif theLoggerprovider fails. This function does not retry.
Associated function logWarning#
pub effect fn logWarning<Args>(
static template: string<'static>,
args: &Args
) -> () ! LogError ? &mut LoggerSends one log message at LogLevel.Warning to the active Logger.
Details#
This function has the same behavior as logAt with LogLevel.Warning.
Errors#
LogErrorif theLoggerprovider fails. This function does not retry.
Associated function logError#
pub effect fn logError<Args>(
static template: string<'static>,
args: &Args
) -> () ! LogError ? &mut LoggerSends one log message at LogLevel.Error to the active Logger.
Details#
This function has the same behavior as logAt with LogLevel.Error.
Errors#
LogErrorif theLoggerprovider fails. This function does not retry.
Associated function result#
pub effect fn result<'env, A, E, ?R>(
protected: once Effect<'env; A ! E ? R>
) -> Result<A, E> ? RRuns protected one time and returns its outcome as a Result value.
When to use#
Use result when the caller must keep, inspect, or store a failure as data, instead of
letting it propagate. Use catchAll when the caller only needs to recover to a success
value.
Details#
The output effect has no typed failures. A success becomes Result.Success and a typed
failure becomes Result.Failure. The output effect still needs the providers R.
Gotchas#
A trap is not a typed failure. result does not catch a trap.
Examples#
Inspect a failure as a value#
import silk.core.effect { Effect }
import silk.core.result { Result }
struct Problem {
code: i32,
}
effect fn load() -> i32
! Problem {
fail Problem { code: 7 }
}
pub fn main() -> i32 {
let completed = run Effect.result(load())
let code = match move completed {
Result<i32, Problem>.Success { value } => 0
Result<i32, Problem>.Failure { error } => error.code
}
if code != 7 {
return 1
}
return 0
}Method mapBoth#
pub effect 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 ? RRuns self and transforms its success with onSuccess or its typed failure with onFailure.
Details#
One callback runs, after self completes. onSuccess changes A to B. onFailure changes
E to F, and the output effect fails with that F. The providers R stay required. A trap
bypasses both callbacks.
When diagnostic observation is active, the new failure keeps the original failure as its cause.
Method map#
pub effect fn map<'env, A, B, E, ?R>(
self: once Effect<'env; A ! E ? R>,
onSuccess: once fn<'env>(A) -> B
) -> B ! E ? RRuns self and returns onSuccess(value) for its success value.
When to use#
Use map when the next step is a plain function that cannot fail. Use flatMap when the
next step is an effect.
Details#
onSuccess runs one time, after self succeeds. If self fails, the failure propagates and
onSuccess does not run. The failure type and providers do not change.
Method mapError#
pub effect fn mapError<'env, A, E, F, ?R>(
self: once Effect<'env; A ! E ? R>,
onFailure: once fn<'env>(E) -> F
) -> A ! F ? RRuns self and, if it fails, changes the typed failure with onFailure.
When to use#
Use mapError to convert a failure into the failure type of the caller, or to add context to
a failure. The output still fails. Use catchAll to recover to a success.
Details#
onFailure runs one time, only if self fails. Its result is the new failure. A success does
not change. The new failure keeps the original failure as its diagnostic cause.
Method flatMap#
pub effect fn flatMap<'env, A, B, E, F, ?R, ?S>(
self: once Effect<'env; A ! E ? R>,
onSuccess: for<use 'call> once fn<'env>(A) -> once Effect<'call & 'env; B ! F ? S>
) -> B ! E | F ? R | SRuns self and then runs the effect that onSuccess returns for the success value.
When to use#
Use flatMap to chain effects in a pipeline. Use direct run statements when named
intermediate values make the code clearer.
Details#
If self fails, onSuccess does not run. The output has the failures of both steps (E | F)
and the providers of both steps (R | S). The success of the second effect is the output
success.
The success value stays valid while the second effect runs and while it cleans up, also when it suspends or is cancelled. The output effect cannot escape the call scope.
Method flatten#
pub effect 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 runs the inner effect that it returns.
Details#
If the outer effect fails, there is no inner effect and nothing more runs. The output has the
failures and providers of both effects. Effect.flatten(nested) has the same behavior as
flatMap with a function that returns its argument.
Method zip#
pub effect fn zip<'env, A, B, E, F, ?R, ?S>(
self: once Effect<'env; A ! E ? R>,
other: once Effect<'env; B ! F ? S>
) -> Pair<A, B> ! E | F ? R | SRuns self and then other, and returns both success values as a Pair.
Details#
The effects run in order, not in parallel. If self fails, other does not run. The output
has the failures and providers of both effects. Read the values from Pair.first and
Pair.second.
Method zip3#
pub effect 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>
) -> Triple<A, B, C> ! E | F | G ? R | S | TRuns three effects in order and returns the three success values as a Triple.
Details#
The effects run from left to right, not in parallel. If one effect fails, the later effects do not run. The output has the failures and providers of all three effects.
Method tap#
pub effect fn tap<'env, A, E, F, ?R, ?S>(
self: once Effect<'env; A ! E ? R>,
callback: for<use 'call> once fn<'env>(A) -> once Effect<'call & 'env; A ! F ? S>
) -> A ! E | F ? R | SRuns self and then runs the effect that callback returns. The success of that effect is
the output success.
When to use#
Use tap to do an effectful action with a value, such as a log message, and then continue
with the value. The callback must return the value again.
Details#
callback receives the success value by move. It must produce a value of the same type A.
If self fails, callback does not run. A failure of either step propagates.
Method catchAll#
pub effect fn catchAll<'env, A, B, E, F, ?R, ?S>(
self: once Effect<'env; A ! E ? R>,
onFailure: for<use 'call> once fn<'env>(E) -> once Effect<'call & 'env; B ! F ? S>
) -> A | B ! F ? R | SRuns self and, if it fails, runs the effect that onFailure returns for the failure.
When to use#
Use catchAll to recover from every typed failure of self. Use catch to recover from
one failure type and let the other failure types propagate.
Details#
onFailure runs one time, only if self fails, and receives the complete failure value. The
output has no failures of self. It can fail only with the failures F of the recovery
effect. A success of self bypasses onFailure. The output needs the providers of both
paths.
The failure value stays valid while the recovery effect runs and cleans up, also when it suspends or is cancelled.
Gotchas#
A trap is not a typed failure. catchAll does not catch a trap.
Method catch#
pub effect fn catch<'env, S, A, B, E, F, ?R, ?Q>(
self: once Effect<'env; A ! E ? R>,
onFailure: for<use 'call> once fn<'env>(S) -> once Effect<'call & 'env; B ! F ? Q>
) -> A | B ! Without<E, S> | F ? R | Q
where S in ERuns self and, if it fails with the failure type S, runs the effect that onFailure
returns.
When to use#
Use catch to recover from one failure type and let the other failures propagate. Use
catchAll to recover from each failure.
Details#
Write the selected type as the first type argument, as in Effect.catch<NotFound>(load(),
handler). S must be one member of the failure union E. onFailure runs one time, only
for a failure of type S. Other failures propagate unchanged, as Without<E, S>. The
failures F of the recovery effect join the output. A success bypasses onFailure.
Gotchas#
A trap is not a typed failure. catch does not catch a trap.
Method ensuring#
pub effect fn ensuring<'env, A, E, ?R, ?S>(
self: once Effect<'env; A ! E ? R>,
finalizer: once Effect<'env; () ! never ? S>
) -> A ! E ? R | SRuns self and then runs finalizer, for a success and for a typed failure. The outcome of
self does not change.
When to use#
Use ensuring to release a resource or to record the end of an operation, whatever the
outcome. Use ensuringNonParking when cancellation must also run the finalizer.
Details#
The order is:
selfruns and cleans up its own local values.finalizerruns.The caller receives the success value or the typed failure.
A caller that recovers the failure does not see it before the finalizer completes. The failure keeps its original origin and context. Both effects can suspend. If a suspended composition is destroyed, it drops the held outcome one time and does not run the rest of the finalizer.
Gotchas#
finalizer cannot fail (! never). To use a cleanup step that can fail, recover its failure
first, for example with catchAll. A trap bypasses the finalizer.
Method ensuringNonParking#
pub effect fn ensuringNonParking<
'env,
A,
E,
?R,
?S,
F: once Effect<'env; () ! never ? S> + Intrinsic.NonParking,
>(
self: once Effect<'env; A ! E ? R>,
finalizer: F
) -> A ! E ? R | SRuns self and then runs finalizer on each exit: success, typed failure, and cancellation.
The outcome of self does not change.
When to use#
Use this function when cancellation of a parked execution must release a scoped resource. Use
ensuring when the finalizer must suspend.
Details#
The finalizer runs one time after success or typed failure, and also when an owner cancels a
parked Execution. The providers stay valid until the finalizer completes. Nested finalizers
run in reverse order.
Gotchas#
finalizer must be Intrinsic.NonParking, so it cannot suspend. It cannot fail (! never).
A trap bypasses the finalizer and all Drop hooks.
Associated function useReleaseNonParking#
pub effect<'env> fn useReleaseNonParking<
'env,
Resource: 'env,
A,
E,
?R,
?S,
Release: for<'scope> once fn<'env>(
&'scope mut Resource
) -> once Effect<'scope & 'env; () ! never ? S> + Intrinsic.NonParking,
>(
resource: Resource,
use: for<'scope> once fn<'env>(
&'scope mut Resource
) -> once Effect<'scope & 'env; A ! E ? R>,
release: Release
) -> A ! E ? R | SGives an exclusive borrow of resource to use, and then gives a new exclusive borrow to
release on each exit: success, typed failure, and cancellation.
When to use#
Use this function when work that can suspend needs exclusive access to a resource, and cancellation must release the resource before the frame is destroyed.
Details#
The borrow for use ends before release starts. release runs one time. Nested calls
release in reverse order. The outcome of use does not change.
Gotchas#
The effect that release returns must be Intrinsic.NonParking and cannot fail (! never).
Recover a cleanup failure before you return the effect. A trap bypasses release and all
Drop hooks.
Associated function ifThenElse#
pub effect 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 the effect of onTrue if condition is true, or the effect of onFalse if it is
false.
When to use#
Use ifThenElse to select one of two effects in a pipeline. Use an if statement with run
in plain sequential code.
Details#
Each branch is a function that makes its effect. Only the selected function runs, so the other effect is never made. Code that is valid only under the condition is safe in its branch. The output has the failures and providers of both branches. Both branches must have the same success type.
The name is not if because if is a keyword.
Method retry#
pub effect fn retry<'env, A, E, ?R>(
mut self: mut Effect<'env; A ! E ? R>,
retries: usize
) -> A ! E ? RRuns self and, after each typed failure, runs it again, at most retries more times.
Details#
A success stops the attempts. If each attempt fails, the last failure propagates. Earlier
failure values are dropped. A retries of 0 gives one attempt. self must be a reusable
effect (mut Effect).
Gotchas#
There is no delay between attempts. A trap stops the attempts at once and is not retried.
Method bindRequirement#
pub effect fn bindRequirement<'env, ?S, A, P, E, ?R>(
self: once Effect<'env; A ! E ? R>,
provider: &'env P
) -> A ! E ? Without<R, S>
where &'env P provides S from RSatisfies one shared requirement of self with a shared borrow of provider.
When to use#
Use provide, which has the same behavior. This name shows the relation to
bindRequirementMut and bindRequirementOwned.
Details#
Write the selected requirement as the first type argument when the selection is not unique. A
shared provider can satisfy only a shared requirement. The output does not need that
requirement. The borrow ends when the effect completes, and the caller keeps ownership of
provider.
Method bindRequirementMut#
pub effect fn bindRequirementMut<'env, ?S, A, P, E, ?R>(
self: once Effect<'env; A ! E ? R>,
provider: &'env mut P
) -> A ! E ? Without<R, S>
where &'env mut P provides S from RSatisfies one requirement of self with an exclusive borrow of provider.
When to use#
Use provideMut in ordinary code. It has the same behavior.
Details#
An exclusive provider can satisfy a shared or an exclusive requirement. The output does not
need the exact requirement that was selected. For example, &mut P for a shared &Logger
removes &Logger. The caller gets exclusive access back when the effect completes.
Method bindRequirementOwned#
pub effect<'env> fn bindRequirementOwned<'env, ?S, A, P: 'env, E, ?R>(
self: once Effect<'env; A ! E ? R>,
provider: P
) -> A ! E ? Without<R, S>
where P provides S from RSatisfies one requirement of self with provider, which moves into the output effect.
When to use#
Use this function when the provider must live as long as the effect, for example when the caller returns the effect.
Details#
An owned provider can satisfy a shared or an exclusive requirement. If provider is affine,
the output effect can run one time. If provider is Copy, the effect keeps a copy and can
run again. The provider is dropped with the effect. It is not returned.
Method provide#
pub effect fn provide<'env, ?S, A, P, E, ?R>(
self: once Effect<'env; A ! E ? R>,
provider: &'env P
) -> A ! E ? Without<R, S>
where &'env P provides S from RSatisfies one shared requirement of self with a shared borrow of provider.
When to use#
Use provide for a service that the computation only reads, such as a clock or a
configuration. Use provideMut when the service changes state.
Details#
The output does not need the selected requirement. Each other requirement stays in the output
type. The caller keeps ownership of provider. This function has the same behavior as
bindRequirement.
Gotchas#
If provider can satisfy more than one requirement, write the requirement as the first type
argument, as in Effect.provide<Clock>(...).
Method provideMut#
pub effect fn provideMut<'env, ?S, A, P, E, ?R>(
self: once Effect<'env; A ! E ? R>,
provider: &'env mut P
) -> A ! E ? Without<R, S>
where &'env mut P provides S from RSatisfies one requirement of self with an exclusive borrow of provider.
When to use#
Use provideMut for a service that changes state, such as an allocator, a logger, or a
counter. Use provide for a read-only service.
Details#
The compiler selects the requirement by provider identity or by one unique service
conformance. The order of requirements in the type is never used for selection. The output
does not need the selected requirement. provider does not move, and the caller gets
exclusive access back when the effect completes.
Gotchas#
If provider can satisfy more than one requirement, write the requirement as the first type
argument, as in Effect.provideMut<Allocator>(...).
Examples#
Change a custom service in one computation#
import silk.core.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 }
let value = run Effect.provideMut<Counter>(read(), &mut counter)
if value != 42 || counter.value != 42 {
return 1
}
return 0
}Method provideEffect#
pub effect 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 &'env mut P provides S from RRuns acquire to make a provider, and then runs self with an exclusive borrow of that
provider.
When to use#
Use provideEffect when the provider itself must be made by an effect, such as a connection
that needs an allocator.
Details#
acquire runs each time the output effect runs. Thus a retry of the output makes a new
provider for each attempt. The provider is dropped before the caller sees the outcome of
self. The output needs the providers Q of acquire, and each requirement of self except
the selected one.
Errors#
Fifacquirefails. Thenselfdoes not run.Eifselffails.
Associated function suspend#
pub effect fn suspend<'env, A, E, ?R>(
deferred: once Effect<'env; A ! E ? R>
) -> A ! E ? RRuns deferred through the stack-safe effect executor. The channels do not change.
When to use#
Use suspend at a recursive or deeply chained boundary, so that deep recursion does not use
the native call stack. Laziness alone does not need suspend.
Details#
suspend adds no allocator requirement and no allocation failure. The frames are on the
execution stack of the compiler.
Gotchas#
If the execution stack is exhausted, the program traps.
Associated function of#
pub effect<'env> fn of<A: 'env, 'env>(value: A) -> AReturns an effect that succeeds with value when it runs.
Details#
The call evaluates value immediately and moves it into the effect. The effect has no
failures and needs no providers. If value is affine, the effect can run one time.
Associated function sleep#
pub effect fn sleep(howLong: u64) -> () ? &mut MonotonicClockWaits howLong nanoseconds on the timeline of the active MonotonicClock provider.
Details#
The provider decides how to wait. A native provider blocks the thread. A test provider can advance virtual time.
Gotchas#
A duration of 0 returns without a wait. If the deadline is more than the clock can represent, the program traps.
Pair#
pub struct Pair<A, B>The two success values that zip returns, in operand order.
Field first#
pub first: AThe success value of the first effect.
Field second#
pub second: BThe success value of the second effect.
Triple#
pub struct Triple<A, B, C>The three success values that zip3 returns, in operand order.
Field first#
pub first: AThe success value of the first effect.
Field second#
pub second: BThe success value of the second effect.
Field third#
pub third: CThe success value of the third effect.
Category: Core.
Profiles: aarch64-apple-darwin, aarch64-unknown-linux-gnu, aarch64-unknown-linux-gnu-no-libc, x86_64-unknown-linux-gnu, x86_64-unknown-linux-gnu-no-libc, wasm32-unknown-unknown, wasm32-wasip1, wasm32-wasip2.