Reference

View Source

< How It Works | Up: Internals | Index | Performance Investigation >

Quick-reference material for Skuld. For narrative explanations, start with Why Effects? or Getting Started.

Glossary

Computation - A function (env, k) -> {result, env} that describes an effectful program. Computations are inert until run - they don't execute side effects when constructed, only when a handler interprets them. Created with comp do ... end, Comp.pure/1, or effect operations like State.get().

Effect - A named capability that a computation can request. Effects are defined by modules (e.g., Skuld.Effects.State) and identified by signatures. An effect operation returns a computation that, when executed, looks up the appropriate handler and delegates to it.

Handler - A function that interprets effect requests. Handlers are installed in the environment and looked up by effect signature. The same computation can run with different handlers for different behaviour (production vs test, real IO vs in-memory).

Evidence - The map of effect signatures to handler functions carried in the environment. "Evidence-passing" means handlers are threaded through computations as data, enabling O(1) lookup.

Continuation (k) - A function representing "what happens next" after an effect operation completes. Normal effects call k once. Throw discards k. Yield captures k for later resumption.

Bind (<-) - Sequences two computations. x <- comp runs comp, extracts the result into x, then continues with the rest. Desugars to Comp.bind/2.

Sentinel - A special return value (%Throw{}, %Suspend{}) that signals abnormal completion. The ISentinel protocol determines how each is finalised by Comp.run/1.

Suspend - A sentinel indicating the computation has paused (via Yield.yield/1). Contains a resume function to continue execution and optionally data decorated by handlers (e.g., EffectLogger state).

Scope - A handler installation boundary. Scoped handlers (Comp.scoped/2) guarantee cleanup runs in reverse installation order, whether the computation completes normally, throws, or suspends.

leave_scope - A composed cleanup chain in the environment. Each scoped handler adds its cleanup function, chaining to the previous one. Invoked on normal completion (via ISentinel) and on Throw (via leave_scope unwinding). Not invoked on Suspend (deferred until resume).

transform_suspend - A composed decoration chain in the environment. When a computation suspends, this function decorates the Suspend struct (e.g., attaching handler state for serialisation).

Auto-lifting - Plain values in comp blocks are automatically wrapped in Comp.pure/1. The final expression, bare if without else, and similar cases are lifted so you don't need explicit return calls.

Effect quick-reference

Foundational effects

State & Environment

EffectModuleKey OperationsHandlerTest Approach
StateSkuld.Effects.Stateget/0,1 put/1,2 modify/1,2 gets/1,2with_handler(comp, initial, opts) opts: tag:, output:, suspend:Use directly with initial value
ReaderSkuld.Effects.Readerask/0,1 asks/1,2 local/2,3with_handler(comp, value, opts) opts: tag:, output:, suspend:Use directly with test config
WriterSkuld.Effects.Writertell/1,2 peek/0,1 listen/1,2 pass/1,2 censor/2,3with_handler(comp, initial \\ [], opts) opts: tag:, output:, suspend:Use directly; output: &{&1, &2} to capture log

Error Handling & Resources

EffectModuleKey OperationsHandlerTest Approach
ThrowSkuld.Effects.Throwthrow/1 catch_error/2 try_catch/1with_handler(comp)Use directly
BracketSkuld.Effects.Bracketbracket/3 bracket_/3 finally/2None needed (combinator)Use directly

Value Generation

EffectModuleKey OperationsHandlerTest Approach
FreshSkuld.Effects.Freshfresh_uuid/0with_uuid7_handler(comp)with_test_handler(comp, opts) - deterministic UUID5 from namespace + counter
RandomSkuld.Effects.Randomrandom/0 random_int/2 random_element/1 shuffle/1with_handler(comp)with_seed_handler(comp, seed: ...) or with_fixed_handler(comp, values: [...])

Collections

EffectModuleKey OperationsHandlerNotes
FxListSkuld.Effects.FxListfx_map/2 fx_reduce/3 fx_each/2 fx_filter/2None (combinator)Full Yield/Suspend support; ~0.2 us/op
FxFasterListSkuld.Effects.FxFasterListfx_map/2 fx_reduce/3 fx_each/2 fx_filter/2None (combinator)Limited Yield support; ~0.1 us/op

Concurrency

EffectModuleKey OperationsHandlerTest Approach
ParallelSkuld.Effects.Parallelall/1 race/1 map/2with_handler(comp)with_sequential_handler(comp) for determinism
AtomicStateSkuld.Effects.AtomicStateget/0,1 put/1,2 modify/1,2 cas/2,3 atomic_state/1,2with_agent_handler(comp, initial, opts) opts: tag:with_state_handler(comp, initial, opts) - uses State internally
AsyncComputationSkuld.AsyncComputationstart/2 resume/3 cancel/1 + _sync variantsNot an effect; a runner moduleUse start_sync/resume_sync for testing

Persistence & Data

EffectModuleKey OperationsHandlerTest Approach
TransactionSkuld.Effects.Transactiontransact/1 rollback/1 try_transact/1Transaction.Ecto.with_handler(comp, repo, opts)Transaction.Noop.with_handler(comp, opts) - env state rollback without database
CommandSkuld.Effects.Commandexecute/1with_handler(comp, handler_fn) - fn receives command, returns computationProvide test handler function
EventAccumulatorSkuld.Effects.EventAccumulatoremit/1with_handler(comp, opts) opts: output:output: &{&1, &2} to capture events

External Integration

EffectModuleKey OperationsHandlerTest Approach
PortSkuld.Effects.Portrequest/3 request!/3with_handler(comp, registry, opts) registry: %{mod => resolver}with_test_handler(comp, responses) or with_fn_handler(comp, fn)
HexPort.ContractHexPort.Contractdefport name(p :: t) :: tUses Port handlerStub via Port test handlers
Port.FacadeSkuld.Effects.Port.Facadeuse Facade, hex_port_contract: MCombined effectful contract + facade (callers, bangs, keys)Same as plain
Port.EffectfulContractSkuld.Effects.Port.EffectfulContractuse EffectfulContract, hex_port_contract: MEffectful @callbacks (when separate from facade)Same as plain
Port.Adapter.EffectfulSkuld.Effects.Port.Adapter.Effectfuluse Port.Adapter.Effectful, contract: M, impl: I, stack: &f/1Bridges effectful to plain ElixirTest the effectful impl directly

Advanced effects

Coroutines

EffectModuleKey OperationsHandlerNotes
YieldSkuld.Effects.Yieldyield/0,1 respond/2 collect/2 feed/2 run_with_driver/2with_handler(comp)Returns %ExternalSuspend{value, resume} on yield

Cooperative Fibers

EffectModuleKey OperationsHandlerNotes
FiberPoolSkuld.Effects.FiberPoolfiber/1,2 await/1 await!/1 await_all/1 scope/1,2 fiber_await_all/1 map/2with_handler(comp, opts)Cooperative scheduling within one BEAM process
ChannelSkuld.Effects.Channelnew/1 put/2 take/1 put_async/2 take_async/1 close/1 stats/1with_handler(comp)Bounded, backpressure. Requires FiberPool
BrookSkuld.Effects.Brookfrom_enum/1,2 from_function/1,2 map/2,3 filter/2,3 each/2 to_list/1None (uses Channel + FiberPool)High-level streaming with chunking

Query Batching

EffectModuleKey OperationsHandlerNotes
query macroSkuld.Query.QueryBlockquery do ... end defquery defquerypRequires FiberPoolAuto-concurrent batching via dependency analysis
deffetchSkuld.Query.Contractdeffetch name(p :: t) :: t with_executor/2Executor behaviourFiber-based batching; executors handle [{ref, op}]
Query.CacheSkuld.Query.Cachewith_executors/2 with_executor/3Wraps executorsCross-batch caching + within-batch dedup

Serializable Coroutines

EffectModuleKey OperationsHandlerNotes
EffectLoggerSkuld.Effects.EffectLoggermark_loop/1with_logging(comp, opts) (fresh), with_logging(comp, log, opts) (replay), with_resume(comp, log, value, opts) (cold resume)Install before handlers it wraps (it intercepts their effects). Decorates Suspend with log.

Handler installation via catch clause

Effects that implement IInstall can be installed directly in comp blocks using the catch clause:

comp do
  x <- State.get()
  config <- Reader.ask()
  {x, config}
catch
  State -> 0                    # State.__handle__(comp, 0)
  Reader -> %{timeout: 5000}   # Reader.__handle__(comp, config)
end

The config value (right side of ->) is passed to the module's __handle__/2 function. First clause becomes innermost handler, last clause outermost.

Effects that implement IIntercept can be intercepted with tagged patterns:

comp do
  result <- risky_computation()
  result
catch
  {Throw, :not_found} -> :default_value
  {Yield, :checkpoint} -> :resume_value
end

Both forms can be mixed in a single catch block.

The output option

Most stateful handlers accept an output option that transforms the result when the computation completes:

# Without output: result is just the computation's return value
State.with_handler(comp, 0)

# With output: result includes final state
State.with_handler(comp, 0, output: fn result, state -> {result, state} end)

Common patterns:

PatternOutput function
Discard state (default)fn result, _state -> result end
Return bothfn result, state -> {result, state} end
Return state onlyfn _result, state -> state end
Transform resultfn result, state -> apply_state(result, state) end

Running computations

FunctionBehaviour
Comp.run(comp)Returns raw result. %Suspend{} and %Throw{} are returned as-is
Comp.run!(comp)Returns value for normal completion. Raises on Throw or Suspend

Comparison with alternatives

vs Freyja (Freer monads)

AspectFreyjaSkuld
Computation typesTwo (Freer + Hefty)One (computation)
Handler lookupLinear searchO(1) map
Macro systemcon + heftySingle comp
Performance~1 us/op~0.1-0.25 us/op
Control effectsHefty higher-orderDirect CPS

Skuld is simpler (one type, one macro) and faster (~4x). Freyja's dual type system made higher-order effects awkward.

vs Mox/Mimic (test doubles)

AspectMox/MimicSkuld effects
ScopePer-process expectationsPer-computation handlers
Simple stubsClean; stub/3 is order-independentClean; map or function handler
Stateful test doublesAd-hoc (Agent/closure per test)Reusable (Repo.InMemory with 3-stage dispatch, with_stateful_handler)
Property testingPossible but requires hand-rolled in-memory implsNatural fit with reusable handlers
Runtime behaviourSame code, mocked dependenciesSame code, different handlers
ConcurrencyPer-process isolation; allow/3 for multi-processHandlers in computation env

For simple cases (1-3 external calls), Mox and Skuld are comparable. Skuld's advantage appears with stateful call chains (reads-after-writes), deep orchestration (10+ calls), and property tests — where reusable in-memory handlers replace ad-hoc per-test stubs. See Mox vs Skuld for a detailed comparison.

vs GenStage/Flow (streaming)

AspectGenStage/FlowBrook/Channel
Concurrency modelBEAM processesCooperative fibers
BackpressureDemand-basedBounded channels
Effect integrationNoneFull effect stack available
TestabilityRequires process infrastructurePure handler swap
OverheadProcess per stageFibers within one process

Brook is lighter-weight for cases where you want streaming with full effect support. GenStage is better for long-running production pipelines that benefit from process isolation.

vs Temporal.io (durable workflows)

AspectTemporal.ioEffectLogger
InfrastructureSeparate server clusterIn-process, serialise to any store
LanguageSDK bindings (Go, Java, etc.)Native Elixir
Replay mechanismEvent sourcing on serverLog replay in computation
GranularityActivity-levelEffect-level
Loop supportWorkflow versioningLoop marking + pruning

EffectLogger is much lighter than Temporal but has narrower scope. It's suitable for durable conversations, multi-step wizards, and workflows that need to survive process restarts. It's not a distributed workflow orchestrator.

vs Haskell effect systems (Polysemy, Effectful, etc.)

AspectHaskell librariesSkuld
Type safetyFull effect row typesDynamic (Elixir)
PerformanceVaries; some use delimited continuationsEvidence-passing CPS
Handler lookupType-directedMap lookup by signature
CompositionType-level effect rowsRuntime handler stacking

Skuld takes the evidence-passing approach from Xie & Leijen (2021) and adapts it for a dynamic language. The single computation type with auto-lifting is what makes it practical without a type system - there's no Eff '[State, Reader, Throw] a to track.

Documentation map

LayerDocumentWhat you'll learn
0READMEOverview, quick example, installation
1Why Effects?The problem effects solve
2What Are Algebraic Effects?The concept, no code
3Getting StartedFirst computation, handlers, running
4Syntax In Depthcomp macro, else/catch, defcomp
5State & EnvironmentState, Reader, Writer
5Error HandlingThrow, Bracket
5Value GenerationFresh, Random
5CollectionsFxList, FxFasterList
5ConcurrencyParallel, AtomicState, AsyncComputation
5PersistenceTransaction, Command, EventAccumulator
5External IntegrationPort, Port.Contract, Port.Adapter.Effectful
6YieldCoroutines, suspend/resume
6Fibers & ConcurrencyFiberPool, Channel, Brook
6Query Batchingquery macro, deffetch, Cache
6EffectLoggerSerializable coroutines, replay
7TestingTesting patterns, property-based testing
7Hexagonal ArchitecturePort.Contract + Port.Adapter.Effectful
7Decider PatternCommand + EventAccumulator
7Handler StacksProduction vs test stacks
7LiveView IntegrationAsyncComputation in LiveView
7Durable WorkflowsEffectLogger persistence
7Data PipelinesBrook streaming
7Batch LoadingQuery contracts, FiberPool
8How It WorksImplementation, CPS, custom effects
9Reference (this page)Quick-reference tables, glossary

< How It Works | Up: Internals | Index | Performance Investigation >