Skuld
Evidence-passing Algebraic Effects for Elixir.
Skuld is a clean, efficient implementation of Algebraic Effects using evidence-passing style with CPS (continuation-passing style) for control effects. It provides scoped handlers, composable effect stacks, and a library of useful effects.
Algebraic effects add an architectural layer between pure and side-effecting code: instead of just pure functions and side-effecting functions, you have pure functions, effectful functions, and side-effecting handlers. Domain code is written with effects but remains pure - the same code runs with test handlers (pure, in-memory) or production handlers (real I/O). This enables clean separation of concerns, property-based testing, and serializable coroutines for durable computation.
Skuld's library of effects aims to provide primitives broad enough that most domain computations can use effectful operations instead of side-effecting ones. Here are some common side-effecting operations and their effectful equivalents:
| Side-effecting operation | Effectful equivalent |
|---|---|
| Configuration / environment | Reader |
| Process dictionary | State, Writer |
| Random values | Random |
| Generating IDs (UUIDs) | Fresh |
| Concurrent fibers / streaming | FiberPool, Channel, Brook |
| Run effects from LiveView | AsyncComputation |
| DB writes & transactions | DB |
| Typed batchable queries | query, deffetch, Query.Cache |
| Blocking calls to external code | Port, Port.Contract |
| Effectful code from plain code | Port.Provider |
| Decider pattern | Command, EventAccumulator |
| Serializable coroutines | EffectLogger |
| Raising exceptions | Throw |
| Resource cleanup (try/finally) | Bracket |
| Control flow | Yield |
| Lists of effectful computations | FxList, FxFasterList |
Documentation
- Syntax - Computations, the
compblock, binds, auto-lifting,else/catchclauses,defcomp - Debugging - Stacktraces, exception handling,
try_catch,IThrowable - Effects
- State & Environment - State, Reader, Writer, tagged usage, scoped transforms
- Control Flow - Throw, Bracket, Yield, Yield.respond
- Collections - FxList, FxFasterList
- Value Generation - Fresh, Random
- Basic Concurrency - AtomicState, Parallel, AsyncComputation
- Persistence & Data - DB, Command, EventAccumulator
- Port - Port, Port.Contract, Port.Provider
- Slightly insane effects
- Concurrency - FiberPool, Channel, Brook
- Query - query macro, deffetch contracts, batchable queries via FiberPool
- Serializable Coroutines - EffectLogger
- Property-Based Testing - Testing effectful code with pure handlers
- Architecture - Design, comparison with Freyja
- Performance - Benchmarks and analysis
Features
- Evidence-passing style: Handlers are looked up directly from a map in the dynamic environment
- CPS for control effects: Enables proper support for control flow effects like Yield and Throw
- Scoped handlers: Handlers are automatically installed/restored with proper cleanup
- Composable: Multiple effects can be stacked and composed naturally
- Single type: Single unified
computationtype andcompmacro for all effectful code - ideal for dynamic languages - Auto-lifting: Plain values are automatically lifted to computations,
enabling ergonomic patterns like
ifwithoutelseand implicit final returns
Installation
Add skuld to your list of dependencies in mix.exs (see the Hex package for the current version):
def deps do
[
{:skuld, "~> x.y"}
]
endDemo Application
See TodosMcp - a voice-controllable todo application built with Skuld. It demonstrates how command/query structs combined with algebraic effects enable trivial LLM integration and property-based testing. Try it live at https://todos-mcp-lu6h.onrender.com/
Quick Start
use Skuld.Syntax
alias Skuld.Comp
alias Skuld.Effects.{State, Reader, Writer, Throw, Yield}
# Define a computation using the comp macro
defmodule Example do
defcomp example() do
# Read from Reader effect
config <- Reader.ask()
# Get and update State
count <- State.get()
_ <- State.put(count + 1)
# Write to Writer effect
_ <- Writer.tell("processed item #{count}")
{config, count} # final expression auto-lifted (no return needed)
end
end
# Run with handlers installed
Example.example()
|> Reader.with_handler(:my_config)
|> State.with_handler(0, output: fn r, st -> {r, {:final_state, st}} end)
|> Writer.with_handler([], output: fn r, w -> {r, {:log, w}} end)
|> Comp.run!()
#=> {{{:my_config, 0}, {:final_state, 1}}, {:log, ["processed item 0"]}}
See the Syntax guide for full details on the comp block,
pattern matching, error handling, and more.
License
MIT License - see LICENSE for details.