Packages
skuld
0.31.2
0.33.1
0.33.0
0.32.1
0.32.0
0.31.2
0.31.1
0.31.0
0.30.0
0.28.0
0.27.3
0.27.2
0.27.1
0.26.0
0.25.0
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.0
0.9.0
0.8.3
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.0
0.5.0
0.4.0
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.26
0.1.25
0.1.24
0.1.23
0.1.22
0.1.21
0.1.20
0.1.19
0.1.18
0.1.17
0.1.16
0.1.15
0.1.14
0.1.13
0.1.12
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Core effect system for Elixir: write business logic as pure effect descriptions, swap handlers for testing. Provides the Comp engine and foundational effects (State, Reader, Writer, Throw, Yield).
Current section
Files
Jump to
Current section
Files
README.md
# Skuld
<!-- nav:header:start -->
[Why Effects? >](docs/why.md)
<!-- nav:header:end -->
[](https://github.com/mccraigmccraig/skuld/actions/workflows/test.yml)
[](https://hex.pm/packages/skuld)
[](https://hexdocs.pm/skuld/)
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:
```elixir
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 →](docs/recipes/batch-loading.md)
### Suspension & resumption
A pausable state machine. The computation *pauses* at each `Yield`,
waits for external input, then resumes with full effect context
preserved:
```elixir
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`:
```elixir
# 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:
```elixir
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 →](docs/recipes/liveview.md)
### 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:
```elixir
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 →](docs/recipes/durable-computation.md)
## Installation
[](https://hex.pm/packages/skuld)
```elixir
def deps do
[
{:skuld, "~> 0.28"}
]
end
```
## Where next?
| If you want to... | Read |
|---|---|
| Understand the problem effects solve | [Why Effects?](docs/why.md) |
| See how effects and handlers work | [How It Works](docs/what.md) |
| Write your first computation | [Getting Started](docs/getting-started.md) |
| State, Reader, Writer, Throw, Fresh, Random | [Foundational Effects](docs/effects/state-reader-writer.md) |
| Yield, Coroutines, FiberPool, Channels, Async | [Coroutines & Concurrency](docs/effects/yield.md) |
| Port, Repo, Hexagonal Architecture | [Boundaries](docs/effects/port.md) |
| Eliminate N+1 queries | [Batch Loading](docs/recipes/batch-loading.md) |
| Handler-swapping for deterministic testing | [Testing](docs/testing.md) |
| Full effect and API reference | [Reference](docs/reference.md) |
| Peek under the hood — CPS, evidence-passing, custom effects | [How It Really Works](docs/internals.md) |
## License
MIT License — see [LICENSE](LICENSE) for details.
<!-- nav:footer:start -->
---
[Why Effects? >](docs/why.md)
<!-- nav:footer:end -->