Skuld

Why Effects? >

TestHex.pmDocumentation

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
Stateful test doubles for complex flows Reusable in-memory handlers with read-after-write consistency Stateful testing
Non-deterministic UUIDs / randomness Fresh and Random have deterministic test handlers - same test, same values Determinism
N+1 queries deffetch + query batch independent loads automatically Batching
Long-running workflows across restarts EffectLogger serialises progress; resume from where you left off Durable workflows
LiveView multi-step operations AsyncComputation bridges effects into LiveView's process model LiveView
Hexagonal architecture plumbing Port.Contract / Port.Adapter.Effectful - typed boundaries, no parameter threading Hex arch

See What Skuld Solves for worked examples of each.

Quick example

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.EffectPort.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:

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:

Onboarding.register(%{name: "Alice"})
|> Reader.with_handler(%{default_tier: :free})
|> Fresh.with_test_handler()
|> Port.with_test_handler(%{
   UserRepo.EffectPort.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 for the current version):

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.Adapter.Effectful
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? - the problem, explained without jargon.

Ready to code? Jump to Getting Started for your first computation.

Full documentation

Layer Topic Description
1 Why Effects? The problem effects solve
2 The Concept How algebraic effects work
What Skuld Solves Concrete problems, worked examples
3 Getting Started Your first computation
4 Syntax In Depthcomp, else, catch, defcomp
5 Foundational Effects
State & Environment State, Reader, Writer
Error Handling Throw, Bracket
Value Generation Fresh, Random
Collections FxList, FxFasterList
Concurrency Parallel, AtomicState, AsyncComputation
Persistence Transaction, Command, EventAccumulator
External Integration Port, Port.Contract, Port.Adapter.Effectful
6 Advanced Effects
Yield Coroutines
Fibers & Concurrency FiberPool, Channel, Brook
Query & Batching Automatic N+1 prevention
EffectLogger Serializable coroutines
7 Recipes
Testing Property-based testing with effects
Hexagonal Architecture Port.Contract + Port.Adapter.Effectful
Decider Pattern Event-sourced domain logic
Handler Stacks Composing production & test stacks
LiveView Multi-step wizards
Durable Workflows Persist-and-resume with EffectLogger
Data Pipelines Streaming with Brook
Batch Loading N+1-free data access
8 Internals
Internals Implementation details
Reference API reference and comparisons

Demo Application

See TodosMcp - 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 for details.


Why Effects? >