Packages
skuld
0.1.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
[](https://github.com/mccraigmccraig/skuld/actions/workflows/test.yml)
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, coroutines via Yield, and composable effect stacks.
## Features
- **Evidence-passing style**: Handlers are looked up directly from the environment, avoiding pattern matching on effect signatures
- **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
- **No Freer/Hefty split**: Single unified `comp` macro for all effects
## Installation
Add `skuld` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:skuld, "~> 0.1.0"}
]
end
```
## Quick Start
```elixir
import Skuld.Syntax
alias Skuld.Comp
alias Skuld.Effects.{State, Reader, Writer, Throw, Yield}
# Define a computation using the comp macro
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}")
return({config, count})
end
# Run with handlers installed
{result, _env} =
example()
|> Reader.with_handler(:my_config)
|> State.with_handler(0)
|> Writer.with_handler()
|> Comp.run()
```
## Effects
### State
Mutable state within a computation:
```elixir
defcomp counter() do
n <- State.get()
_ <- State.put(n + 1)
return(n)
end
{result, _} =
counter()
|> State.with_handler(0)
|> Comp.run()
# result = 0
```
### Reader
Read-only environment:
```elixir
defcomp greet() do
name <- Reader.ask()
return("Hello, #{name}!")
end
{result, _} =
greet()
|> Reader.with_handler("World")
|> Comp.run()
# result = "Hello, World!"
```
### Writer
Accumulating output:
```elixir
defcomp logging() do
_ <- Writer.tell("step 1")
_ <- Writer.tell("step 2")
return(:done)
end
{{result, log}, _} =
logging()
|> Writer.with_handler()
|> Comp.run()
# result = :done
# log = ["step 1", "step 2"]
```
### Throw
Error handling:
```elixir
defcomp might_fail(x) do
if x < 0 do
Throw.throw({:error, "negative value"})
else
return(x * 2)
end
end
defcomp with_recovery() do
result <- Throw.catch_error(
might_fail(-1),
fn error -> return({:recovered, error}) end
)
return(result)
end
{result, _} = with_recovery() |> Comp.run()
# result = {:recovered, {:error, "negative value"}}
```
### Yield
Coroutine-style suspension and resumption:
```elixir
defcomp generator() do
_ <- Yield.yield(1)
_ <- Yield.yield(2)
_ <- Yield.yield(3)
return(:done)
end
# Collect all yielded values
{:done, result, yields, _} =
generator()
|> Yield.with_handler()
|> Yield.collect()
# result = :done
# yields = [1, 2, 3]
# Or drive with a custom function
{:done, _, _} =
generator()
|> Yield.with_handler()
|> Yield.run_with_driver(fn yielded ->
IO.puts("Got: #{yielded}")
{:continue, :ok}
end)
```
### FxList
Effectful list operations:
```elixir
defcomp process_all(items) do
results <- FxList.fx_map(items, fn item ->
comp do
count <- State.get()
_ <- State.put(count + 1)
return(item * 2)
end
end)
return(results)
end
{result, _} =
process_all([1, 2, 3])
|> State.with_handler(0)
|> Comp.run()
# result = [2, 4, 6]
```
> **Note**: For large iteration counts (10,000+), use `Yield`-based coroutines instead
> of `FxList` for better performance. See the FxList module docs for details.
## Architecture
Skuld uses evidence-passing style where:
1. **Handlers** are stored in the environment as functions
2. **Effects** look up their handler and call it directly
3. **CPS** enables control effects (Yield, Throw) to manipulate continuations
4. **Scoped handlers** automatically manage handler installation/cleanup
This avoids the performance overhead of pattern matching on effect signatures
and enables efficient composition of multiple effects.
## Comparison with Freyja
Skuld is a cleaner alternative to Freyja:
| Aspect | Freyja | Skuld |
|--------|--------|-------|
| Effect representation | Freer monad + Hefty algebras | Evidence-passing CPS |
| Control effects | Hefty (higher-order) | Direct CPS |
| Handler dispatch | Pattern matching on signatures | Direct map lookup |
| Macro system | `con` + `hefty` | Single `comp` |
## License
MIT License - see [LICENSE](LICENSE) for details.