Why Effects? >

Test Hex.pm Documentation

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 pointWhat Skuld doesMore
Orchestration code is untestableSwap handlers: same code runs in-memory with no DB, no networkTesting
Stateful test doubles for complex flowsReusable in-memory handlers with read-after-write consistencyStateful testing
Non-deterministic UUIDs / randomnessFresh and Random have deterministic test handlers - same test, same valuesDeterminism
N+1 queriesdeffetch + query batch independent loads automaticallyBatching
Long-running workflows across restartsEffectLogger serialises progress; resume from where you left offDurable workflows
LiveView multi-step operationsAsyncComputation bridges effects into LiveView's process modelLiveView
Hexagonal architecture plumbingPort.Contract / Port.Adapter.Effectful - typed boundaries, no parameter threadingHex 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 operationEffectful equivalent
Configuration / environmentReader
Process dictionary / stateState, Writer
Random valuesRandom
Generating IDs (UUIDs)Fresh
TransactionsTransaction
Blocking calls to external codePort, Port.Contract
Effectful code from plain codePort.Adapter.Effectful
Mutation dispatchCommand
Domain event collectionEventAccumulator
Fork-join concurrencyParallel
Thread-safe stateAtomicState
Effects from LiveViewAsyncComputation
Raising exceptionsThrow
Resource cleanup (try/finally)Bracket
Effectful list operationsFxList, 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.

PatternEffectful equivalent
Coroutines / suspend-resumeYield
Cooperative fibersFiberPool
Bounded channelsChannel
Streaming with backpressureBrook
Automatic N+1 query batchingquery, deffetch, Query.Cache
Serializable coroutinesEffectLogger

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

LayerTopicDescription
1Why Effects?The problem effects solve
2The ConceptHow algebraic effects work
What Skuld SolvesConcrete problems, worked examples
3Getting StartedYour first computation
4Syntax In Depthcomp, else, catch, defcomp
5Foundational Effects
State & EnvironmentState, Reader, Writer
Error HandlingThrow, Bracket
Value GenerationFresh, Random
CollectionsFxList, FxFasterList
ConcurrencyParallel, AtomicState, AsyncComputation
PersistenceTransaction, Command, EventAccumulator
External IntegrationPort, Port.Contract, Port.Adapter.Effectful
6Advanced Effects
YieldCoroutines
Fibers & ConcurrencyFiberPool, Channel, Brook
Query & BatchingAutomatic N+1 prevention
EffectLoggerSerializable coroutines
7Recipes
TestingProperty-based testing with effects
Hexagonal ArchitecturePort.Contract + Port.Adapter.Effectful
Decider PatternEvent-sourced domain logic
Handler StacksComposing production & test stacks
LiveViewMulti-step wizards
Durable WorkflowsPersist-and-resume with EffectLogger
Data PipelinesStreaming with Brook
Batch LoadingN+1-free data access
8Internals
InternalsImplementation details
ReferenceAPI 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? >