Skuld

Why Effects? >

TestHex.pmDocumentation

An effectful programming framework for Elixir.

Bundled with a library of effects and components spanning state management, cooperative concurrency, streaming, efficient query execution, component architecture, and durable serialisable workflows:

                          Comp
                     (lazy computation,
                      evidence-passing,
                      scoped handlers)
                            │
      ┌─────────────────────┼───────────────────────────┐
      │                     │                           │
 //Foundational       //Coroutines &               //Boundaries
 //Effects            //Concurrency                     │
      │                     │                           │
      │                 Coroutine                ┌──────┼────┐
      │                     │                    │      │    │
 State, Reader,    ┌────────┼─────────┐          │      │  Port
 Writer, Throw,    │        │         │          │      │  Port.EffectfulFacade
 Bracket, Fresh,   │  Serializable-   │          │      │  Repo
 Random, FxList,   │   Coroutine      │          │      │
 Yield,            │                  │          │      │
 EffectLogger,  AsyncCoroutine     FiberPool     │  Adapter
 Parallel,                            │          │  Adapter.EffectfulContract
 AtomicState,                         ├─────────┐│
 Transaction,                         │         ││
 Command                              │         ││
                                 ┌────┴────┐    ││
                              Channel    Task   ││
                                 │              ││
                               Brook            ││
                                                ││
                                           Query.Contract
                                           QueryBlock
                                           (Haxl-like: auto-batches fetches
                                           via Coroutine fibers)

The old problem

Between pure business logic and side-effecting infrastructure sits the orchestration layer — "fetch the user, check permissions, load their subscription, hit some APIs, compute a price, write an invoice." This code encodes your most important business rules, but it's tangled with databases, APIs, and randomness — making it hard to test, hard to refactor, and often — impossible to property-test.

Another way

Skuld lets you write pure orchestration code that describes side effects without performing them — then handlers decide what those descriptions mean. The exact same "effectful" code runs with side-effecting handlers in production and pure in-memory handlers in tests — fully deterministic, fully pure, and straightforwardly property-testable.

The effect advantage

Effectful computations condense domain logic to its essence. Handlers provide context — production vs test, concurrency, batching — without touching the computation. Effects are first-class data: inspect them, serialise them, replay them. The same mechanism enables all three examples below.

Composability

Effects compose with zero ceremony. This query function reads like straightforward sequential code, but when it runs, concurrency happens at two levels: within the defquery block (fetch_user and fetch_orders run together via dependency analysis), and across all streamed invocations — Brook.map runs 4 transforms concurrently, and FiberPool batches their deffetch calls into single round-trips:

defquery build_account_summary(user_id, month) do
  user <- AccountQueries.fetch_user(user_id)
  orders <- AccountQueries.fetch_orders(user_id, month)
  details <- Query.map(Enum.map(orders, & &1.id), &AccountQueries.fetch_order_details/1)
  build_account_summary(user, orders, details)
end

# Feed a stream of users through — 4 concurrent transforms, all deffetch
# calls batched together by FiberPool
comp do
  source <- Brook.from_enum(user_ids)
  summaries <- Brook.map(source, &build_account_summary(&1, "2026-01"), concurrency: 4)
  Brook.to_list(summaries)
end
|> Skuld.Query.with_executor(AccountQueries, AccountExecutor)
|> Channel.with_handler()
|> FiberPool.with_handler()
|> Comp.run!()

build_account_summary knows nothing about batch sizes, concurrency limits, database round-trips, or the other account summaries which also need to be built - it's pure domain logic. Everything else is handler wiring — swappable, testable, composable.

Full batch loading recipe →

Suspension & resumption

A pausable state machine. The computation pauses at each Yield, waits for external input, then resumes with full effect context preserved:

defmodule Checkout do
  use Skuld.Syntax

  alias Skuld.Effects.Yield

  defcomp run do
    cart <- Yield.yield(:get_cart)
    {:ok, inventory} <- Inventory.check_stock(cart.items)
    payment <- Yield.yield(:get_payment)
    {:ok, order} <- Orders.place(cart, payment)
    _ <- Emailer.send_confirmation(order)
    {:ok, order}
  end
end

Run it from a LiveView with AsyncCoroutine:

# mount
{:ok, runner} = AsyncCoroutine.run(Checkout.run(), tag: :checkout)

# handle_info — the state machine pauses at each yield
def handle_info({AsyncCoroutine, :checkout, %ExternalSuspend{value: :get_cart}}, socket) do
  cart = ShoppingCart.get_cart(socket.assigns.user)
  AsyncCoroutine.run(socket.assigns.runner, cart)   # resume with cart
  {:noreply, socket}
end

def handle_info({AsyncCoroutine, :checkout, %ExternalSuspend{value: :get_payment}}, socket) do
  payment = socket.assigns.payment_form |> to_payment_method()
  AsyncCoroutine.run(socket.assigns.runner, payment) # resume with payment
  {:noreply, socket}
end

def handle_info({AsyncCoroutine, :checkout, {:ok, order}}, socket) do
  {:noreply, assign(socket, order: order, step: :done)}
end

Test it — drive the whole machine in a few lines:

comp =
  Checkout.run()
  |> Port.with_handler(%{Inventory => Inventory.Test, Orders => Orders.Test})
  |> Port.with_test_handler(%{Port.key(Emailer, :send_confirmation, [_]) => :ok})
  |> Yield.with_handler()
  |> Throw.with_handler()

fiber = comp |> Coroutine.new(Env.new()) |> Coroutine.run()    # pauses at :get_cart
fiber = Coroutine.run(fiber, %{items: [...]})                  # pauses at :get_payment
%Coroutine.Completed{result: {:ok, order}} =
  Coroutine.run(fiber, %{card: "4242..."})                     # completes

Same code. Production pauses at each step for user input. Tests drive the entire state machine in a single function — deterministic, no processes, no stubs.

Full LiveView integration recipe →

Durability

The same Checkout state machine, above, but now serialised: pause it mid-flight, save its entire execution history as JSON, and resume it later — after a restart, on a different machine. Every effect invocation is captured in a serialisable log:

sc =
  SerializableCoroutine.new(Checkout.run(), fn comp ->
    comp |> Yield.with_handler() |> Throw.with_handler()
  end)

# Run until suspension, serialize the effect log
suspended = SerializableCoroutine.run(sc)
json = SerializableCoroutine.serialize(SerializableCoroutine.get_log(suspended))
# => EffectLogEntry{data: :get_cart, value: nil, state: :started}

# Later — cold resume from JSON, no manual deserialisation needed
suspended2 = SerializableCoroutine.run(json, sc, %{items: [...]})
# => EffectLogEntry{data: :get_cart, value: %{items: [...]}, state: :executed}
# => EffectLogEntry{data: :get_payment, value: nil, state: :started}

Every effect invocation — yields, state changes, writer events — is captured in the log. run replays recorded effects and resumes at the suspension point. The same mechanism that enables batching in the composability example enables durability here.

Full durable computation recipe →

Installation

Hex.pm

def deps do
  [
    {:skuld, "~> 0.28"}
  ]
end

Where next?

If you want to... Read
Understand the problem effects solve Why Effects?
See how effects and handlers work How It Works
Write your first computation Getting Started
State, Reader, Writer, Throw, Fresh, Random Foundational Effects
Yield, Coroutines, FiberPool, Channels, Async Coroutines & Concurrency
Port, Repo, Hexagonal Architecture Boundaries
Eliminate N+1 queries Batch Loading
Handler-swapping for deterministic testing Testing
Full effect and API reference Reference
Peek under the hood — CPS, evidence-passing, custom effects How It Really Works

License

MIT License — see LICENSE for details.


Why Effects? >