Browse the docs

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#

silk
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#

silk
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#

silk
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#

silk
pub struct Effect

The 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#

silk
impl Effect

Associated function log#

silk
pub effect fn log<Args>(
    static template: string<'static>,
    args: &Args
  ) -> () ! LogError ? &mut Logger

Sends 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#

  • LogError if the Logger provider fails. This function does not buffer or retry.

Associated function logAt#

silk
pub effect fn logAt<Args>(
    level: LogLevel,
    static template: string<'static>,
    args: &Args
  ) -> () ! LogError ? &mut Logger

Sends 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#

  • LogError if the Logger provider fails. This function does not buffer or retry.

Associated function logTrace#

silk
pub effect fn logTrace<Args>(
    static template: string<'static>,
    args: &Args
  ) -> () ! LogError ? &mut Logger

Sends one log message at LogLevel.Trace to the active Logger.

Details#

This function has the same behavior as logAt with LogLevel.Trace.

Errors#

  • LogError if the Logger provider fails. This function does not retry.

Associated function logDebug#

silk
pub effect fn logDebug<Args>(
    static template: string<'static>,
    args: &Args
  ) -> () ! LogError ? &mut Logger

Sends one log message at LogLevel.Debug to the active Logger.

Details#

This function has the same behavior as logAt with LogLevel.Debug.

Errors#

  • LogError if the Logger provider fails. This function does not retry.

Associated function logInfo#

silk
pub effect fn logInfo<Args>(
    static template: string<'static>,
    args: &Args
  ) -> () ! LogError ? &mut Logger

Sends one log message at LogLevel.Info to the active Logger.

Details#

This function has the same behavior as logAt with LogLevel.Info.

Errors#

  • LogError if the Logger provider fails. This function does not retry.

Associated function logWarning#

silk
pub effect fn logWarning<Args>(
    static template: string<'static>,
    args: &Args
  ) -> () ! LogError ? &mut Logger

Sends one log message at LogLevel.Warning to the active Logger.

Details#

This function has the same behavior as logAt with LogLevel.Warning.

Errors#

  • LogError if the Logger provider fails. This function does not retry.

Associated function logError#

silk
pub effect fn logError<Args>(
    static template: string<'static>,
    args: &Args
  ) -> () ! LogError ? &mut Logger

Sends one log message at LogLevel.Error to the active Logger.

Details#

This function has the same behavior as logAt with LogLevel.Error.

Errors#

  • LogError if the Logger provider fails. This function does not retry.

Associated function result#

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

Runs 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#

silk
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#

silk
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 ? R

Runs 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#

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

Runs 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#

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

Runs 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#

silk
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 | S

Runs 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#

silk
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 | S

Runs 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#

silk
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 | S

Runs 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#

silk
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 | T

Runs 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#

silk
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 | S

Runs 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#

silk
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 | S

Runs 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#

silk
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 E

Runs 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#

silk
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 | S

Runs 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:

  1. self runs and cleans up its own local values.

  2. finalizer runs.

  3. 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#

silk
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 | S

Runs 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#

silk
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 | S

Gives 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#

silk
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 | S

Runs 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#

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

Runs 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#

silk
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 R

Satisfies 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#

silk
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 R

Satisfies 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#

silk
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 R

Satisfies 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#

silk
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 R

Satisfies 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#

silk
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 R

Satisfies 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#

silk
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#

silk
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 R

Runs 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#

  • F if acquire fails. Then self does not run.

  • E if self fails.

Associated function suspend#

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

Runs 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#

silk
pub effect<'env> fn of<A: 'env, 'env>(value: A) -> A

Returns 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#

silk
pub effect fn sleep(howLong: u64) -> () ? &mut MonotonicClock

Waits 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#

silk
pub struct Pair<A, B>

The two success values that zip returns, in operand order.

Field first#

silk
pub first: A

The success value of the first effect.

Field second#

silk
pub second: B

The success value of the second effect.

Triple#

silk
pub struct Triple<A, B, C>

The three success values that zip3 returns, in operand order.

Field first#

silk
pub first: A

The success value of the first effect.

Field second#

silk
pub second: B

The success value of the second effect.

Field third#

silk
pub third: C

The 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.

View the source of this page