Skip to main content

Pragmatic do-notation and effects system for Python

Project description

doeff - Algebraic Effects for Python

doeff is an algebraic effects runtime for Python with one-shot continuations and a Rust VM. Programs are written with generator-based @do notation and executed through explicit handler stacks.

Installation

pip install doeff

Quick Start

from doeff import do, run, WithHandler
from doeff_core_effects import Ask, Get, Put, Tell
from doeff_core_effects.handlers import reader, state, writer
from doeff_core_effects.scheduler import scheduled

@do
def counter_program():
    yield Put("counter", 0)
    yield Tell("Starting computation")
    count = yield Get("counter")
    yield Put("counter", count + 1)
    return count + 1

# Compose handlers explicitly with WithHandler
prog = counter_program()
prog = WithHandler(writer(), prog)
prog = WithHandler(state(), prog)
prog = WithHandler(reader(env={"greeting": "hello"}), prog)
result = run(scheduled(prog))
print(result)  # 1

Runtime API

Entrypoint Signature Use case
run run(doexpr) Execute a DoExpr program to completion

run() takes a single DoExpr (program node) and returns the result value directly. Handlers are composed explicitly using WithHandler(handler, body). Use scheduled(prog) to wrap with the scheduler for concurrency effects.

Handler Composition

Handlers are installed with WithHandler(handler, body). Stack multiple handlers by nesting:

from doeff import do, run, WithHandler, Resume, Pass
from doeff_core_effects import Ask

@do
def my_handler(effect, k):
    if isinstance(effect, Ask):
        return (yield Resume(k, "hello"))
    yield Pass(effect, k)

@do
def prog():
    return (yield Ask("greeting"))

result = run(WithHandler(my_handler, prog()))
print(result)  # hello

Handlers are @do functions that receive (effect, k). They can:

  • yield Resume(k, value) — resume the continuation with a value
  • yield Pass(effect, k) — forward the effect to outer handlers
  • yield Transfer(k, value) — tail-resume (handler done after this)

Effect Surface

Core effects (from doeff_core_effects):

  • Reader: Ask(key), Local(env, program)
  • State: Get(key), Put(key, value)
  • Writer: Tell(message), slog(msg, **kwargs)
  • Error: Try(program) — returns Ok(value) or Err(error)
  • Observe: Listen(program, types=...) — collect effects during execution

Scheduler effects (from doeff_core_effects.scheduler):

  • Concurrency: Spawn(program), Wait(task), Gather(*tasks), Race(*tasks)
  • Promises: CreatePromise(), CompletePromise(p, v), FailPromise(p, e)
  • External: CreateExternalPromise() — bridge external threads
  • Semaphores: CreateSemaphore(n), AcquireSemaphore(s), ReleaseSemaphore(s)

Handlers

Built-in handlers (from doeff_core_effects.handlers):

Handler Factory Effects handled
Reader reader(env={...}) Ask
Lazy Ask lazy_ask(env={...}) Ask, Local (with caching)
State state(initial={...}) Get, Put
Writer writer() Tell / WriterTellEffect
Try try_handler Try
Slog slog_handler() Slog
Local local_handler Local
Listen listen_handler Listen
Await await_handler() Await
Scheduler scheduled(prog) Spawn, Wait, Gather, Race, etc.

Scheduler and Concurrency

from doeff import do, run, WithHandler
from doeff_core_effects import Tell
from doeff_core_effects.handlers import writer
from doeff_core_effects.scheduler import scheduled, Spawn, Wait, Gather

@do
def worker(label):
    yield Tell(f"working: {label}")
    return label

@do
def main():
    t1 = yield Spawn(worker("a"))
    t2 = yield Spawn(worker("b"))
    results = yield Gather(t1, t2)
    return results

result = run(scheduled(WithHandler(writer(), main())))
print(result)  # ['a', 'b']

CLI

# Run a program with auto-discovered interpreter
doeff run --program myapp.module.program

# With explicit interpreter
doeff run --program myapp.program --interpreter myapp.interpreter

# With environment
doeff run --program myapp.program --env myapp.default_env

# Inline code
doeff run -c 'return 42'

# Apply transform (T -> Program[U])
doeff run --program myapp.program --apply myapp.transforms.wrap

# JSON output
doeff run --program myapp.program --format json

The CLI supports automatic interpreter/environment discovery via doeff-indexer. See docs/14-cli-auto-discovery.md for marker syntax and hierarchy rules.

Development

uv sync --reinstall  # rebuild Rust VM
uv run pytest

License

MIT License. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

doeff-0.4.0.tar.gz (105.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

doeff-0.4.0-py3-none-any.whl (21.2 kB view details)

Uploaded Python 3

File details

Details for the file doeff-0.4.0.tar.gz.

File metadata

  • Download URL: doeff-0.4.0.tar.gz
  • Upload date:
  • Size: 105.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for doeff-0.4.0.tar.gz
Algorithm Hash digest
SHA256 95f1dabc752ebeb9f38a163494a3c920b1339fcefa882fcc51332c4b2739e7fa
MD5 b3583a5d0a7eb9ded7c391118e9d57fb
BLAKE2b-256 1988e7b791de2ebfb3590f64e4a4616a0ce228d41e8f2a57c184ad7345e4a4d5

See more details on using hashes here.

File details

Details for the file doeff-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: doeff-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 21.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for doeff-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e511d231ddcb91330cb5514c916bcc6f2ac8078ab3d3043f7deaf578a20aa0fa
MD5 109b033776778af8d713dfe9b1620cb0
BLAKE2b-256 0dfcacff069e48387a1abdf1f6dce8f53e02e1cacea7f56497e928726a1c5f22

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page