aleff
Algebraic effects for Python — deep and shallow, stateful, composable, multi-shot handlers.
from aleff import effect, create_handler
choose = effect("choose")
h = create_handler(choose)
@h.on(choose)
def _(k, *values):
return sum((k(v) for v in values), [])
print(h(lambda: [choose("A", "B") + choose("C", "D")]))
# ['AC', 'AD', 'BC', 'BD']
Features
Deep handlers — effects propagate through nested function calls without annotation
Shallow handlers — handle an effect once, then delegate re-installation to the handler function; enables state machines, shift0/reset0, and strategy changes between invocations
Stateful handlers — handlers can maintain and update state across multiple effect invocations, either via mutable variables (deep) or re-installation with new state (shallow); enables get/put state, transactions, and reverse-mode AD
Multi-shot continuations —
resumecan be called multiple times in a single handler, enabling backtracking search, non-determinism, and other advanced patternsSync and async — both synchronous (
Handler) and asynchronous (AsyncHandler) handlers are supported, with transparent bridging between the twoEffect composition — handler functions can perform effects themselves, dispatched to enclosing handlers; enables layered architectures and modular effect stacking
Dynamic wind —
windcontext manager establishes before/after guards that are re-invoked on multi-shot re-entry, with optional auto-management of context managers returned bybeforeEffect annotation —
@effect(step1, step2)collects effect sets transitively from decorated functionsIntrospection —
effects(fn)andunhandled_effects(fn, h)for querying and validating effect coverageTyped — effect parameters and return types are checked by type checkers (pyright)
No macros, no code generation — pure Python library built on greenlet and a small CPython C extension
Requirements
CPython >=3.12
Tested versions:
3.12.13
3.13.12
3.14.3
3.14.3t (free-threaded)
greenlet >= 3.3.2
Linux / macOS / Windows
Installation
# uv
uv add aleff
# pip
pip install aleff
Quick start
from aleff import effect, Effect, Resume, create_handler
# Define effects
read: Effect[[], str] = effect("read")
write: Effect[[str], int] = effect("write")
# Write business logic using effects
def run():
s = read()
return write(s)
# Provide handler implementations
h = create_handler(read, write)
@h.on(read)
def _read(k: Resume[str, int]):
return k("file contents")
@h.on(write)
def _write(k: Resume[int, int], contents: str):
print(f"writing: {contents}")
return k(len(contents))
result = h(run)
print(result) # 13
Multi-shot example
from aleff import effect, Effect, Handler, Resume, create_handler
choose: Effect[[int], int] = effect("choose")
h: Handler[list[int]] = create_handler(choose)
@h.on(choose)
def _choose(k: Resume[int, list[int]], n: int):
# Resume once for each choice and collect all results
results: list[int] = []
for i in range(n):
results += k(i)
return results
def computation():
x = choose(3) # 0, 1, or 2
y = choose(2) # 0 or 1
return [x * 10 + y]
result = h(computation)
print(result) # [0, 1, 10, 11, 20, 21]
Effect composition
Handler functions can perform effects that are handled by enclosing handlers:
from aleff import effect, Effect, Resume, create_handler
log: Effect[[str], None] = effect("log")
parse: Effect[[str], int] = effect("parse")
# Outer handler: logging
h_log = create_handler(log)
@h_log.on(log)
def _log(k: Resume[None, int], msg: str):
print(f"[LOG] {msg}")
return k(None)
# Inner handler: parsing with logging
h_parse = create_handler(parse)
@h_parse.on(parse)
def _parse(k: Resume[int, int], s: str):
log(f"parsing: {s}") # handled by the outer handler
return k(int(s))
result = h_log(lambda: h_parse(lambda: parse("42") + 1))
# prints: [LOG] parsing: 42
print(result) # 43
Dynamic wind
The wind context manager establishes before/after guards around a dynamic extent. When a multi-shot continuation captured inside the with block is resumed, the before thunk is called again; when it exits, the after thunk runs.
from aleff import effect, Effect, Resume, Handler, create_handler, wind
choose: Effect[[], int] = effect("choose")
h: Handler[list[int]] = create_handler(choose)
@h.on(choose)
def _choose(k: Resume[int, list[int]]):
return k(1) + k(2)
log: list[str] = []
def run() -> list[int]:
with wind(lambda: log.append("before"), lambda: log.append("after")):
return [choose() * 10]
result = h(run)
print(result) # [10, 20]
print(log) # ['before', 'after', 'before', 'after']
If before returns a context manager and auto_exit=True (the default), __enter__ and __exit__ are called automatically:
with wind(lambda: open("data.txt")) as ref:
ref.unwrap().read()
# file is closed on exit
wind_range is a multi-shot-safe replacement for range() in for loops. Python’s range() iterator is shared across shots and exhausted after the first; wind_range saves and restores the iterator position automatically:
with wind_range(n) as r:
for i in r:
v = choose() # multi-shot safe
How it works
Effects are declared as typed values and invoked like regular function calls. A Handler intercepts these calls via greenlet-based context switching:
Business logic runs in a greenlet
When an effect is invoked, control switches to the handler
The handler processes the effect and calls
resume(value)to return a valueIf
resumeis called multiple times, each call restores a snapshot of the continuation’s frames (multi-shot)If the handler returns without calling
resume, the computation is aborted (early exit)
Because handlers use greenlets (not exceptions), the control flow is:
Transparent — no
yield,await, or special syntax in business logicNon-stack-cutting — code after
resumein the handler runs after the continuation completes, enabling reverse-order execution (useful for backpropagation, transactions, etc.)
Multi-shot continuations are implemented via a CPython C extension (aleff._multishot.v1._aleff) that snapshots and restores interpreter frame chains.
Package structure
Package |
Description |
|---|---|
|
Default: re-exports |
|
Multi-shot handlers with frame snapshot/restore |
|
One-shot handlers (no C extension required) |
Examples
See examples/ for demonstrations:
N-Queens — backtracking search via multi-shot continuations
Amb / Logic puzzle — Scheme-style
amboperator and constraint solving (SICP Exercise 4.42)Probability — exact discrete probability distributions via weighted multi-shot
Dependency injection — swap DB/email/logging implementations
Record/Replay — record effect results, replay without side effects
Transactions — buffer writes, commit on success, rollback on failure
Automatic differentiation — forward-mode (dual numbers) and reverse-mode (backpropagation) with the same math expressions
Shallow state machine — mutable state (get/put) and traffic light controller via shallow handler re-installation
shift/reset, shift0/reset0 — delimited continuations: deep = shift/reset, shallow = shift0/reset0, with generator example
API reference
Function / Class |
Description |
|---|---|
|
Create a new |
|
Decorate a function to declare its effects |
|
Create a synchronous handler |
|
Create an asynchronous handler |
|
Register a handler function (decorator) |
|
Run caller with the handler active |
|
Get the declared effect set of a function |
|
Get effects not covered by the given handlers |
|
Effect protocol (parameters |
|
Sync handler protocol |
|
Async handler protocol |
|
Sync continuation ( |
|
Async continuation ( |
|
Dynamic wind context manager |
|
Multi-shot-safe |
|
Reference wrapper returned by |
Development
From source:
git clone https://github.com/hnmr293/aleff.git
cd aleff
uv sync
# Run tests
uv run pytest
# Run tests on all supported Python versions
./run_tests.sh
# Format
uv run ruff format
# Lint
uv run pyright
License
Apache-2.0