# `Skuld.Comp`
[🔗](https://github.com/mccraigmccraig/skuld/blob/main/lib/skuld/comp.ex#L1)

Core computation engine and foundational effects for the Skuld ecosystem.

`Skuld.Comp` is the runtime that powers algebraic effects in Elixir. It
executes effectful computations — pure descriptions of side effects — against
pluggable handlers. The same computation runs with production handlers or
deterministic test handlers, with no stubs or mocks.

This package (`skuld`) provides the engine plus foundational effects:

  * **State** — mutable state scoped to a handler
  * **Reader** — read-only environment with scoped overrides
  * **Writer** — append-only accumulated output
  * **Throw** — early-exit errors with handler recovery
  * **Bracket** — resource acquisition and guaranteed cleanup
  * **Fresh** — unique identifier generation (UUID v7)
  * **Random** — deterministic random generation for tests
  * **FxList** / **FxFasterList** — effectful iteration over collections
  * **Yield** — suspend a computation and wait for external input
  * **Command** — dispatch commands through a handler pipeline
  * **Transaction** — env state rollback with optional database transactions

For coroutines, concurrency, query batching, port/adapter boundaries, durable
execution, and database integration, see the sibling packages:
`skuld_concurrency`, `skuld_query`, `skuld_port`, `skuld_durable`,
`skuld_repo`, and `skuld_process`.

## Auto-Lifting

Non-computation values are automatically lifted to `pure(value)` — you almost
never need to call `Comp.pure/1` explicitly. This enables ergonomic patterns
where bare values and expressions Just Work:

    comp do
      x <- State.get()
      _ <- if x > 5, do: Writer.tell(:big)  # nil auto-lifted when false
      x * 2  # final expression auto-lifted (no `return` needed)
    end

Auto-lifting is implemented by the catch-all clause of `call/3`, which treats
any value that isn't a 2-arity function as `pure(value)` and passes it
directly to the continuation.

## Core Concepts

- **Computation**: `(env, k -> {result, env})` - a suspended computation
- **Result**: Opaque value - framework doesn't impose shape
- **Leave-scope**: Continuation chain for scope cleanup/control
- **ISentinel**: Protocol that dispatches terminal handling at the run boundary.
  Each sentinel type (Throw, ExternalSuspend, InternalSuspend, Cancelled)
  has its own `ISentinel.run/2` implementation. Comp.run is a clean
  `call + ISentinel.run` pipeline with no sentinel-specific logic.

## Architecture

Skuld uses decentralised evidence-passing. Run acts as a **control authority** -
it recognizes the ExternalSuspend sentinel and invokes the leave-scope chain -
but treats results as opaque.

Scoped effects (Reader.local, Catch) install leave-scope handlers
that can clean up env or redirect control flow.

# `bind`

```elixir
@spec bind(Skuld.Comp.Types.computation(), (term() -&gt; Skuld.Comp.Types.computation())) ::
  Skuld.Comp.Types.computation()
```

Sequence computations

# `call`

```elixir
@spec call(
  Skuld.Comp.Types.computation(),
  Skuld.Comp.Types.env(),
  Skuld.Comp.Types.k()
) ::
  {Skuld.Comp.Types.result(), Skuld.Comp.Types.env()}
```

Call a computation with validation, exception handling, and auto-lifting.

If the value is a 2-arity function, it's called as `comp.(env, k)`.

If the value is not a computation (not a 2-arity function), it is automatically
lifted — treated as `pure(value)` and passed directly to the continuation.
This enables ergonomic patterns without explicit wrapping:

    _ <- if condition, do: Writer.tell(x)  # nil auto-lifted when false
    x + 1  # final expression auto-lifted (no return needed)

Elixir exceptions (raise/throw/exit) are caught and converted to Throw effects,
allowing them to be handled uniformly with effect-based errors via `catch_error`.

Note: `InvalidComputation` errors (validation failures) are re-raised rather than
converted to Throws, since they represent programming bugs that should fail fast.

# `call_handler`

```elixir
@spec call_handler(
  Skuld.Comp.Types.handler(),
  term(),
  Skuld.Comp.Types.env(),
  Skuld.Comp.Types.k()
) ::
  {Skuld.Comp.Types.result(), Skuld.Comp.Types.env()}
```

Call an effect handler with exception handling.

Supports both 2-arity total+linear handlers and 3-arity general handlers.
Exceptions in handler code are caught and converted to Throw effects.

# `call_k`

```elixir
@spec call_k(Skuld.Comp.Types.k(), term(), Skuld.Comp.Types.env()) ::
  {Skuld.Comp.Types.result(), Skuld.Comp.Types.env()}
```

Call a continuation (k or leave_scope) with exception handling.

Continuations have signature `(value, env) -> {value, env}`. Unlike `call/3`
which handles computations, this handles the simpler continuation case where
we just need to catch Elixir exceptions and convert them to Throw effects.

Used in `scoped/2` to wrap calls to finally_k.

# `cancel`

```elixir
@spec cancel(Skuld.Comp.ExternalSuspend.t(), Skuld.Comp.Types.env(), term()) ::
  {Skuld.Comp.Cancelled.t(), Skuld.Comp.Types.env()}
```

Cancel a suspended computation, invoking the leave_scope chain for cleanup.

When a computation yields (returns `%ExternalSuspend{}`), the caller can either:
- Resume it with `suspend.resume.(input)`
- Cancel it with `Comp.cancel(suspend, env, reason)`

Cancellation creates a `%Cancelled{reason: reason}` result and invokes the
leave_scope chain, allowing effects to clean up resources.

## Example

    # Run until suspension
    {%ExternalSuspend{} = suspend, env} = Comp.run(my_yielding_comp)

    # Decide to cancel instead of resume
    {%Cancelled{reason: :timeout}, final_env} =
      Comp.cancel(suspend, env, :timeout)

## Effect Cleanup

Effects can detect cancellation in their leave_scope handlers:

    my_leave_scope = fn result, env ->
      case result do
        %Cancelled{} -> cleanup_my_resources(env)
        _ -> :ok
      end
      {result, env}
    end

# `computation?`
*macro* 

Check whether a value is a computation.

A computation in Skuld is a 2-arity function `(env, k) -> {result, env}`.
This is a runtime heuristic — any 2-arity function will return true. In
contexts where precision matters (e.g., stream combinators), prefer an
explicit tagged return value.

# `each`

```elixir
@spec each(list(), (term() -&gt; Skuld.Comp.Types.computation())) ::
  Skuld.Comp.Types.computation()
```

Apply f to each element for side effects, discarding results.

Like `traverse/2` but returns `:ok` instead of collecting results.
Useful when you only care about effects (e.g., `Writer.tell`), not values.

## Example

    comp do
      _ <- Comp.each(items, &Writer.tell/1)
      :done
    end

# `effect`

```elixir
@spec effect(Skuld.Comp.Types.sig(), term()) :: Skuld.Comp.Types.computation()
```

Invoke an effect operation

# `flatten`

```elixir
@spec flatten(Skuld.Comp.Types.computation()) :: Skuld.Comp.Types.computation()
```

Flatten nested computations

# `identity_k`

identity continuation - for initial continuation & default leave-scope

# `map`

```elixir
@spec map(Skuld.Comp.Types.computation(), (term() -&gt; term())) ::
  Skuld.Comp.Types.computation()
```

Map over a computation's result

# `pure`

```elixir
@spec pure(term()) :: Skuld.Comp.Types.computation()
```

Lift a pure value into a computation.

You almost never need this — bare values are automatically lifted by `call/3`.
Prefer returning values directly inside `comp` blocks rather than wrapping
them with `pure/1`.

`pure/1` is still useful when you need an explicit computation value for
combinators like `map/2`, `sequence/1`, or when passing computations as
arguments.

# `return`

```elixir
@spec return(term()) :: Skuld.Comp.Types.computation()
```

Lift a pure value into a computation. Alias for `pure/1`.

Note: auto-lifting makes this unnecessary in almost all contexts.
Prefer bare values — they're automatically lifted via `call/3`.

# `run`

```elixir
@spec run(Skuld.Comp.Types.computation()) ::
  {Skuld.Comp.Types.result(), Skuld.Comp.Types.env()}
```

Run a computation to completion.

Creates a fresh environment internally — all handler installation should
be done via `with_handler` on the computation.

Uses ISentinel protocol to determine completion behavior:
- ExternalSuspend: bypasses leave-scope chain
- Other values: invoke leave-scope chain

## Example

    {result, _env} =
      my_comp
      |> State.with_handler(0)
      |> Reader.with_handler(:config)
      |> Comp.run()

# `run!`

```elixir
@spec run!(Skuld.Comp.Types.computation()) :: term()
```

Run a computation, extracting just the value (raises on ExternalSuspend/Throw)

# `scoped`

```elixir
@spec scoped(
  Skuld.Comp.Types.computation(),
  (Skuld.Comp.Types.env() -&gt;
     {Skuld.Comp.Types.env(), Skuld.Comp.Types.leave_scope()})
) :: Skuld.Comp.Types.computation()
```

Create a scoped computation with a final continuation for cleanup and result transformation.

The `setup` function receives the current env and must return
`{modified_env, finally_k}` where `finally_k :: (value, env) -> {value, env}`
is a continuation that runs when the scope exits.

This enables Koka-style `with` semantics where handlers can transform
computation results (e.g., wrapping with collected state, logs, etc.).

The `finally_k` continuation is called on both:
- **Normal exit**: before continuing to outer computation
- **Abnormal exit**: during leave-scope unwinding (e.g., throw)

The previous leave-scope is automatically restored in both paths.

The argument order is pipe-friendly (computation first).

## Example - Environment restoration only

    def local(modify, comp) do
      comp
      |> Skuld.Comp.scoped(fn env ->
        current = Env.get_state(env, @sig)
        modified_env = Env.put_state(env, @sig, modify.(current))
        finally_k = fn value, e -> {value, Env.put_state(e, @sig, current)} end
        {modified_env, finally_k}
      end)
    end

## Example - Result transformation (like EffectLogger)

    def with_logging(comp) do
      comp
      |> Skuld.Comp.scoped(fn env ->
        env_with_log = Env.put_state(env, :log, [])

        finally_k = fn value, e ->
          log = Env.get_state(e, :log)
          cleaned = Map.delete(e.state, :log)
          {{value, Enum.reverse(log)}, %{e | state: cleaned}}
        end

        {env_with_log, finally_k}
      end)
    end

# `sequence`

```elixir
@spec sequence([Skuld.Comp.Types.computation()]) :: Skuld.Comp.Types.computation()
```

Sequence a list of computations.

Runs each computation in order, collecting results into a list.
Uses a tail-recursive accumulator to avoid stack overflow on large lists.

# `then_do`

```elixir
@spec then_do(Skuld.Comp.Types.computation(), Skuld.Comp.Types.computation()) ::
  Skuld.Comp.Types.computation()
```

Sequence computations, ignoring first result

# `traverse`

```elixir
@spec traverse(list(), (term() -&gt; Skuld.Comp.Types.computation())) ::
  Skuld.Comp.Types.computation()
```

Apply f to each element, sequence the resulting computations.

Uses a tail-recursive accumulator to avoid stack overflow on large lists.

# `with_handler`

```elixir
@spec with_handler(
  Skuld.Comp.Types.computation(),
  Skuld.Comp.Types.sig(),
  Skuld.Comp.Types.handler()
) ::
  Skuld.Comp.Types.computation()
```

Install a scoped handler for an effect.

The handler is installed for the duration of `comp` and then restored
to its previous state (or removed if there was no previous handler).

This allows "shadowing" handlers - an inner computation can have its
own handler for an effect while an outer handler exists.

The argument order is pipe-friendly (computation first).

## Example

    # Create a computation with its own State handler
    inner =
      comp do
        x <- State.get()
        _ <- State.put(x + 1)
        x
      end
      |> Comp.with_handler(State, &State.handle/3)

    # Use it - inner State is independent of outer State
    outer = comp do
      _ <- State.put(100)
      result <- inner        # uses inner's handler
      y <- State.get()       # uses outer's handler, still 100
      {result, y}
    end

# `with_new_handler`

```elixir
@spec with_new_handler(
  Skuld.Comp.Types.computation(),
  Skuld.Comp.Types.sig(),
  Skuld.Comp.Types.handler()
) :: Skuld.Comp.Types.computation()
```

Install a scoped handler for an effect, but only if no handler is already
installed for that signature. If a handler exists, this is a no-op —
the computation runs unchanged.

This is useful when a module wants a default handler but shouldn't shadow
an explicitly-installed one from an outer scope.

## Example

    # FiberPool auto-installs a test Fresh handler. If production code has
    # already installed Fresh.with_uuid7_handler, this is a no-op.
    comp
    |> Comp.with_new_handler(Fresh, Fresh.Test.handle/2)

# `with_scoped_state`

```elixir
@spec with_scoped_state(Skuld.Comp.Types.computation(), term(), term(), keyword()) ::
  Skuld.Comp.Types.computation()
```

Install scoped state for a computation with automatic save/restore.

This is a common pattern used by effect handlers to manage state that should
be isolated to a computation scope. On entry, saves previous state (if any)
and sets initial state. On exit (normal or throw), restores previous state
or removes it if there was none.

## Options

- `:output` - optional function `(result, final_state) -> new_result` to
  transform the result using the final state value before returning.
- `:suspend` - optional function `(ExternalSuspend.t(), env) -> {ExternalSuspend.t(), env}` to
  decorate ExternalSuspend values when yielding. Allows attaching scoped state to suspends.
- `:default` - default value when reading final state (default: nil)

## Example

    # Simple usage - state is saved/restored automatically
    comp
    |> Comp.with_scoped_state(state_key, initial_value)
    |> Comp.with_handler(sig, handler)

    # With output transformation - include final state in result
    comp
    |> Comp.with_scoped_state(state_key, initial, output: fn result, final -> {result, final} end)
    |> Comp.with_handler(sig, handler)

    # With suspend decoration - attach state to ExternalSuspend.data when yielding
    comp
    |> Comp.with_scoped_state(state_key, initial,
      suspend: fn s, env ->
        state = Env.get_state(env, state_key)
        data = s.data || %{}
        {%{s | data: Map.put(data, :my_state, state)}, env}
      end
    )

---

*Consult [api-reference.md](api-reference.md) for complete listing*
