LeanEff is a small Lean 4 extensible-effects library inspired by “Freer Monads, More Extensible Effects,” and shaped for practical Lean programming.
The intended user-facing style is concrete effect rows:
import LeanEff
open LeanEff
def program : Eff [Reader Nat, Writer String] Nat := do
let n ← ask
tell s!"n = {n}"
pure (n + 1)
#eval program
|> runReader 41
|> runWriter
|> runEffect rows are treated as set-like lists for handler lookup. A handler removes its effect wherever it appears and preserves the remaining row order; the order in which handlers are called controls effect interaction.
Reader ρ:ask,runReaderWriter ω:tell,runWriterState σ:get,put,modify,runState,evalState,execStateExceptE ε:throw,tryCatch,runExceptRandom:randNat,randBool,runRandom,evalRandomLiftIO:liftIO,runLiftIOClock τ:now,runClockAtConsole:printLine,readLine,runConsoleIODisplay frame:drawFrame,runDisplayIOInput ι:pollInputSleep:sleepMs,runSleepIO
recordSnapshot records request/response effect interactions into a
Snapshot. A snapshot can later replay the same responses with
replaySnapshot, or be compared against another run with compareSnapshots.
Use Snapshot.toJsonString and Snapshot.fromJsonString to persist traces as
JSON.
def recorded :=
program
|> recordSnapshot
|> runReader 41
|> runWriter (ω := String)
|> runWriter (ω := SnapshotEvent)
|> run
def replayed :=
program
|> replaySnapshot recorded.2
def divergence :=
compareSnapshots recorded.2 anotherSnapshotEffects opt into snapshots through SnapshotCodec, which encodes effect
requests and responses as structured Lean.Json values. LeanEff includes
codecs for the built-in resumable effects.
Snapshots are request/response traces, so an effect operation that never resumes
does not produce an event through this middleware.
Examples.AsciiTetris: an animated terminal Tetris clone with nonblocking controls, ANSI colors, and a next-piece preview. It usesReaderfor configuration,Statefor the board,Writerfor game events,ExceptEfor quit/game-over exits,Randomfor piece generation,Display Stringfor drawing,Input Commandfor controls, andSleepfor frame pacing.
Run it with:
lake exe ascii_tetrisRecord a JSON snapshot for deterministic debugging with:
lake exe ascii_tetris --record trace.jsonAnimate a JSON snapshot by replaying the recorded keys with:
lake exe ascii_tetris --replay trace.jsonStrictly verify a JSON snapshot without animation with:
lake exe ascii_tetris --check trace.jsonRun it in a real terminal because it temporarily switches terminal input mode.
Examples.VideoRental: a small "Palladin's Video Rental" shop. The business logic usesClockfor the current rental day,RentalRepofor customers, movies, and rentals, andWriter Stringas a notification outbox. The demo interpreter backs the repository with an initialized in-memory SQLite database throughleanprover/leansqlite, then runs an interactive menu until quit.
Run it with:
lake exe video_rental_shopExamples.AgentSnake: a concurrent multi-agent snake arena. The arena uses anAgentHosteffect to spawn AI agents, publish world snapshots, and gather agent moves. Each AI snake is an independentAgentEffprogram that observes snapshots and chooses moves.Randomdrives agent jitter and food placement, snakes render as colored*cells, and the same effects can run with either a threaded host interpreter or a single-threaded cooperative interpreter.
Run it with:
lake exe agent_snake_arenaUse the single-threaded cooperative interpreter with:
lake exe agent_snake_arena --cooperativeRecord a JSON snapshot of the arena with:
lake exe agent_snake_arena --record trace.jsonReplay the recorded snake animation with:
lake exe agent_snake_arena --replay trace.jsonStrictly verify the snapshot without animation with:
lake exe agent_snake_arena --check trace.json- Duplicate effect labels are unsupported. Wrap labels in distinct types if two effects have the same payload type.
- The core uses a Lean-friendly mutual
Eff/Arrscontinuation queue with aViewLoperation, so continuations are consumed from the front instead of repeatedly walking a left spine. - Recursive handlers are executable
partial defs; v1 handlers therefore require inhabited result types.