Packages
skuld
0.7.0
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/)
Evidence-passing Algebraic Effects for Elixir.
## The problem
Between your pure business logic and your side-effecting infrastructure
sits the orchestration layer: "fetch the user, check permissions, load
their subscription, compute the price, write the 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 impossible to property-test.
## The insight
Skuld adds a layer between pure and side-effecting code: **effectful**
code. Domain logic *requests* effects (database access, randomness, error
handling) without performing them. Handlers decide what those requests
mean. The same orchestration code runs with real handlers in production
and pure in-memory handlers in tests - fully deterministic, trivially
testable.
## What Skuld solves
| Pain point | What Skuld does | More |
|----------------------------------------|----------------------------------------------------------------------------|----------------------------------------------------------------------------|
| Orchestration code is untestable | Swap handlers: same code runs in-memory with no DB, no network | [Testing](docs/pain-points.md#testing-orchestration-code) |
| Mox boilerplate for multiple stubs | Handler scoping replaces Mox - five stubs compose as cleanly as one | [No more Mox](docs/pain-points.md#no-more-mox-boilerplate) |
| Non-deterministic UUIDs / randomness | Fresh and Random have deterministic test handlers - same test, same values | [Determinism](docs/pain-points.md#deterministic-uuids-randomness-and-time) |
| N+1 queries | `deffetch` + `query` batch independent loads automatically | [Batching](docs/pain-points.md#automatic-query-batching) |
| Long-running workflows across restarts | EffectLogger serialises progress; resume from where you left off | [Durable workflows](docs/pain-points.md#long-running-computations) |
| LiveView multi-step operations | AsyncComputation bridges effects into LiveView's process model | [LiveView](docs/pain-points.md#liveview-multi-step-operations) |
| Hexagonal architecture plumbing | Port.Contract / Port.Provider - typed boundaries, no parameter threading | [Hex arch](docs/pain-points.md#clean-architecture-boundaries) |
See [What Skuld Solves](docs/pain-points.md) for worked examples of each.
## Quick example
```elixir
defmodule Onboarding do
use Skuld.Syntax
defcomp register(params) do
# Read configuration from the environment
config <- Reader.ask()
# Generate a deterministic ID
id <- Fresh.fresh_uuid()
# Use a port for the database call
user <- UserRepo.create_user!(%{id: id, name: params.name, tier: config.default_tier})
# Accumulate domain events
_ <- EventAccumulator.emit(%UserRegistered{user_id: id})
{:ok, user}
end
end
```
Run with production handlers:
```elixir
Onboarding.register(%{name: "Alice"})
|> Reader.with_handler(%{default_tier: :free})
|> Fresh.with_uuid7_handler()
|> Port.with_handler(%{UserRepo => UserRepo.Ecto})
|> EventAccumulator.with_handler(output: fn r, events ->
MyApp.EventBus.publish(events)
r
end)
|> Throw.with_handler()
|> Comp.run!()
```
Run with test handlers - same code, no database, fully deterministic:
```elixir
Onboarding.register(%{name: "Alice"})
|> Reader.with_handler(%{default_tier: :free})
|> Fresh.with_test_handler()
|> Port.with_test_handler(%{
UserRepo.key(:create_user, %{id: _, name: "Alice", tier: :free}) =>
{:ok, %User{id: "test-uuid", name: "Alice", tier: :free}}
})
|> EventAccumulator.with_handler(output: fn r, events -> {r, events} end)
|> Throw.with_handler()
|> Comp.run!()
```
## Installation
Add `skuld` to your dependencies in `mix.exs` (see [Hex](https://hex.pm/packages/skuld) for the current version):
```elixir
def deps do
[
{:skuld, "~> 0.3"}
]
end
```
## What can it do?
### Foundational effects
Effects that solve problems every Elixir developer recognises. They feel
like well-structured Elixir code with better testability.
| Side-effecting operation | Effectful equivalent |
|---------------------------------|------------------------------|
| Configuration / environment | Reader |
| Process dictionary / state | State, Writer |
| Random values | Random |
| Generating IDs (UUIDs) | Fresh |
| Transactions | Transaction |
| Blocking calls to external code | Port, Port.Contract |
| Effectful code from plain code | Port.Provider |
| Mutation dispatch | Command |
| Domain event collection | EventAccumulator |
| Fork-join concurrency | Parallel |
| Thread-safe state | AtomicState |
| Effects from LiveView | AsyncComputation |
| Raising exceptions | Throw |
| Resource cleanup (try/finally) | Bracket |
| Effectful list operations | FxList, FxFasterList |
### Advanced effects
Effects that use cooperative fibers and continuations to do things that
aren't possible with standard BEAM patterns. You don't need these to
get value from Skuld - the foundational effects stand on their own.
| Pattern | Effectful equivalent |
|---------------------------------|------------------------------|
| Coroutines / suspend-resume | Yield |
| Cooperative fibers | FiberPool |
| Bounded channels | Channel |
| Streaming with backpressure | Brook |
| Automatic N+1 query batching | query, deffetch, Query.Cache |
| Serializable coroutines | EffectLogger |
## Documentation
**New to algebraic effects?** Start with
[Why Effects?](docs/why.md) - the problem, explained without jargon.
**Ready to code?** Jump to
[Getting Started](docs/getting-started.md) for your first computation.
### Full documentation
| Layer | Topic | Description |
|-------|-------|-------------|
| 1 | [Why Effects?](docs/why.md) | The problem effects solve |
| 2 | [The Concept](docs/what.md) | How algebraic effects work |
| | [What Skuld Solves](docs/pain-points.md) | Concrete problems, worked examples |
| 3 | [Getting Started](docs/getting-started.md) | Your first computation |
| 4 | [Syntax In Depth](docs/syntax.md) | `comp`, `else`, `catch`, `defcomp` |
| 5 | **Foundational Effects** | |
| | [State & Environment](docs/effects/state-environment.md) | State, Reader, Writer |
| | [Error Handling](docs/effects/error-handling.md) | Throw, Bracket |
| | [Value Generation](docs/effects/value-generation.md) | Fresh, Random |
| | [Collections](docs/effects/collections.md) | FxList, FxFasterList |
| | [Concurrency](docs/effects/concurrency.md) | Parallel, AtomicState, AsyncComputation |
| | [Persistence](docs/effects/persistence.md) | Transaction, Command, EventAccumulator |
| | [External Integration](docs/effects/external-integration.md) | Port, Port.Contract, Port.Provider |
| 6 | **Advanced Effects** | |
| | [Yield](docs/advanced/yield.md) | Coroutines |
| | [Fibers & Concurrency](docs/advanced/fibers-concurrency.md) | FiberPool, Channel, Brook |
| | [Query & Batching](docs/advanced/query-batching.md) | Automatic N+1 prevention |
| | [EffectLogger](docs/advanced/effect-logger.md) | Serializable coroutines |
| 7 | **Recipes** | |
| | [Testing](docs/recipes/testing.md) | Property-based testing with effects |
| | [Hexagonal Architecture](docs/recipes/hexagonal-architecture.md) | Port.Contract + Port.Provider |
| | [Decider Pattern](docs/recipes/decider-pattern.md) | Event-sourced domain logic |
| | [Handler Stacks](docs/recipes/handler-stacks.md) | Composing production & test stacks |
| | [LiveView](docs/recipes/liveview.md) | Multi-step wizards |
| | [Durable Workflows](docs/recipes/durable-workflows.md) | Persist-and-resume with EffectLogger |
| | [Data Pipelines](docs/recipes/data-pipelines.md) | Streaming with Brook |
| | [Batch Loading](docs/recipes/batch-loading.md) | N+1-free data access |
## Demo Application
See [TodosMcp](https://github.com/mccraigmccraig/todos_mcp) - a
voice-controllable todo application built with Skuld. It demonstrates
command/query structs with algebraic effects for LLM integration and
property-based testing. Try it live at
https://todos-mcp-lu6h.onrender.com/
## License
MIT License - see [LICENSE](LICENSE) for details.
<!-- nav:footer:start -->
---
[Why Effects? >](docs/why.md)
<!-- nav:footer:end -->