Skip to content

Commit 8d3feb4

Browse files
Technologicatclaude
andcommitted
D13: Monads subpackage + monadic_do macro
Port seven classical monads from the python-opetus-2017 teaching code into `unpythonic.monads`: Identity, Maybe, Either (new - Left/Right), List, Writer, State, Reader. Plus `Monad`/`LiftableMonad` base classes (modeled on `unpythonic.slicing.Sliced`) and `liftm`/`liftm2`/`liftm3` helpers. Subpackage is not re-exported at top level; import directly. `monadic_do` block macro: `with monadic_do[M] as result:` over any monad, with body `[bindings] in result << final_expr`. Supports `:=` (primary) and `<<` (legacy) for bindings; `_ := mexpr` for sequencing; empty bindings allowed. Parsed via `letdoutil.canonize_bindings`, expands to a nested lambda-bind chain. Always the innermost `with` (body shape constraint), composes correctly with lazify / continuations / tco / autocurry et al. between their two passes. State.join properly implemented (teaching code had NotImplementedError): run the outer state function, then run the inner with the threaded state — standard "thread the state" pattern. MonadicList was moved to `unpythonic.monads.List` with a varargs constructor (so `List(x)` is the monadic unit). `amb.MonadicList` remains as a silent alias with a TODO(3.0.0) for removal. Added a `_make` classmethod on `List` so `mogrify` reconstructs containers elementwise (matching the namedtuple convention), which is what `forall` needs under Pytkell's `lazify`. Brief: briefs/monads-implementation.md (already committed as 1249f9c). Tests: 115 pure-Python + 17 macro + 14 integration (including the critical lazify short-circuit preservation test). All green on 3.14. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1249f9c commit 8d3feb4

26 files changed

Lines changed: 2053 additions & 175 deletions

AUTHORS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
- Juha Jeronen (@Technologicat) - original author
44
- @aisha-w - documentation improvements
5-
- @Technologicat with Claude (Anthropic) as AI pair programmer - CI modernization, Python 3.13–3.14 and mcpyrate 4.0.0 adaptation (2.0.0)
5+
- @Technologicat with Claude (Anthropic) as AI pair programmer - CI modernization, Python 3.13–3.14 and mcpyrate 4.0.0 adaptation (2.0.0); monads subpackage and `monadic_do` macro (2.1.0)
66

77
**Design inspiration from the internet**:
88

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
**New**:
66

7+
- `unpythonic.monads`: subpackage of classical monads — `Identity`, `Maybe`, `Either` (with `Left`/`Right`), `List`, `Writer`, `State`, `Reader`. Plus `Monad`/`LiftableMonad` base classes and `liftm`/`liftm2`/`liftm3` helpers. Not re-exported at the top level; import as `from unpythonic.monads import Maybe`.
8+
- `monadic_do`: do-notation macro over any monad. `with monadic_do[M] as result:` with body `[bindings] in result << final_expr`; supports `:=` (primary) and `<<` (legacy) for bindings; empty bindings allowed; `_ := mexpr` for sequencing. Always used as the innermost `with` (body shape constraint), composes correctly with `lazify`/`continuations`/`tco`/`autocurry`/etc.
79
- `environ_override`: context manager to temporarily override OS environment variables within a `with` block, restoring the previous state on exit.
810
- Thread-safe (serialises concurrent overrides via `RLock`); same-thread nesting supported.
911
- New module `unpythonic.environ`; the function is named `override` at the module level and re-exported as `environ_override` at the top level.
@@ -25,6 +27,7 @@
2527

2628
**Changed**:
2729

30+
- `unpythonic.amb.MonadicList`: the implementation was moved to `unpythonic.monads.List` and renamed. `MonadicList` remains as a silent alias of `List` for name compatibility, scheduled for removal in 3.0.0. The constructor uses varargs (`List(1, 2, 3)`) — the class itself is the monadic unit (`List(x)` is a singleton list). Iterable-constructor use-cases go through `List.from_iterable(iterable)`.
2831
- `unpythonic.net` (REPL server and client) now runs on MS Windows.
2932
- Previously the whole subsystem was POSIX-only because `unpythonic.net.ptyproxy` required `termios`, `tty`, and `os.openpty`.
3033
- A new `socket.socketpair()`-based backend stands in for the pty master/slave endpoints on Windows, plugged in via a platform dispatch in `PTYSocketProxy`.

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,31 @@ The condition system is the clean, general solution to this problem. It automati
335335

336336
If this sounds a lot like an exception system, that's because conditions are the supercharged sister of exceptions. The condition model cleanly separates mechanism from policy, while otherwise remaining similar to the exception model.
337337
</details>
338+
<details><summary>Monads: Identity, Maybe, Either, List, Writer, State, Reader.</summary>
339+
340+
[[docs](doc/features.md#monads)] [[`monadic_do` macro](doc/macros.md#monadic_do-do-notation-for-any-monad)]
341+
342+
```python
343+
from unpythonic.llist import nil
344+
from unpythonic.monads import Maybe, List
345+
346+
# Maybe — short-circuits on `nil`; the lambda is never called
347+
assert Maybe(nil) >> (lambda x: Maybe(x + 1)) == Maybe(nil)
348+
349+
# List — flatMap. Pythagorean triples by three nested binds.
350+
def r(lo, hi):
351+
return List.from_iterable(range(lo, hi))
352+
pt = r(1, 21) >> (lambda z:
353+
r(1, z + 1) >> (lambda x:
354+
r(x, z + 1) >> (lambda y:
355+
List.guard(x*x + y*y == z*z).then(
356+
List((x, y, z))))))
357+
assert tuple(sorted(pt)) == ((3, 4, 5), (5, 12, 13), (6, 8, 10),
358+
(8, 15, 17), (9, 12, 15), (12, 16, 20))
359+
```
360+
361+
For do-notation syntax (`with monadic_do[M] as result:`), see the macro documentation. Bind is `>>` (Python's `>>=` is in-place and doesn't chain), sequence is `.then(other)`, the class itself is `unit`.
362+
</details>
338363
<details><summary>Lispy symbol type.</summary>
339364

340365
[[docs](doc/features.md#sym-gensym-Singleton-symbols-and-singletons)]
@@ -698,6 +723,34 @@ with lazify:
698723
assert my_if(False, 1/0, 42) == 42
699724
```
700725
</details>
726+
<details><summary>Monadic do-notation for any monad.</summary>
727+
728+
[[docs](doc/macros.md#monadic_do-do-notation-for-any-monad)]
729+
730+
```python
731+
from unpythonic.syntax import macros, monadic_do
732+
from unpythonic.monads import Maybe, List
733+
734+
# Maybe — do-notation threads Just-values (denoted `Maybe(value)`); any Nothing (`unpythonic.nil`) short-circuits.
735+
with monadic_do[Maybe] as result:
736+
[x := Maybe(10),
737+
y := Maybe(x + 1)] in result << Maybe(x + y)
738+
assert result == Maybe(21)
739+
740+
# List — Pythagorean triples via the list monad
741+
def r(lo, hi):
742+
return List.from_iterable(range(lo, hi))
743+
with monadic_do[List] as pt:
744+
[z := r(1, 21),
745+
x := r(1, z + 1),
746+
y := r(x, z + 1),
747+
_ := List.guard(x*x + y*y == z*z)] in pt << List((x, y, z))
748+
assert tuple(sorted(pt)) == ((3, 4, 5), (5, 12, 13), (6, 8, 10),
749+
(8, 15, 17), (9, 12, 15), (12, 16, 20))
750+
```
751+
752+
Body shape is a single `[bindings] in result << final_expr` statement: bindings on the left of `in`, the "send to box" exit pattern on the right. `:=` is the primary bind arrow (parsed by the same `letdoutil` machinery as the modern `let[]` syntax); `<<` also works.
753+
</details>
701754
<details><summary>Genuine multi-shot continuations (call/cc).</summary>
702755

703756
[[docs](doc/macros.md#continuations-callcc-for-python)]

TODO_DEFERRED.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,9 @@ Next unused item code: D16
5454
- **D15: Audit bare `{path}` interpolation for repr/raw asymmetry on Windows**: Fleet-wide audit across all projects. The known failure mode (mcpyrate `cacbfd2`, 2026-04-15): an f-string interpolates a file path with bare `{__file__}`, producing raw backslashes (`C:\a\b`), while the other side of a comparison uses `repr()`/`unparse()` output with escaped backslashes (`C:\\a\\b`) — mismatch on Windows, passes on POSIX by accident. Fix is `{__file__!r}` so both sides speak the same dialect. The risk is NOT f-string reinterpretation (that's safe), but asymmetry when a bare-interpolated path is compared against, compiled as, or embedded into Python source. Grep hints: `__file__` in f-strings; also any path value interpolated into strings that later reach `compile()`, `eval()`, `ast.unparse()`, assertions, or similar. (Noted 2026-04-17.)
5555

5656

57+
- **D16: Remove `unpythonic.amb.MonadicList` alias (3.0.0)**: As part of D13 monads port, `MonadicList` was moved to `unpythonic.monads.List` with a varargs constructor (`List(1, 2, 3)` instead of `MonadicList([1, 2, 3])`). A silent alias `MonadicList = List` is kept in `unpythonic/amb.py` for backward-name compatibility during the 2.x series. Remove the alias in 3.0.0 along with the accompanying `TODO(3.0.0)` comment at the alias site. Users must then import `List` directly from `unpythonic.monads`. Note: this is name-only compat — the constructor signature changed at 2.0.0, so existing callers of `MonadicList([...])` already needed to switch to varargs or `from_iterable(...)` at 2.0.0. (Noted 2026-04-17.)
58+
59+
60+
- **D17: `monadic_do[List]` inside Pytkell's auto-lazify yields wrapped generators**: The Pythagorean-triples-style `monadic_do[List]` computation, when run under the Pytkell dialect (which wraps the whole module in `with lazify, autocurry:`), produces a result whose `tuple(sorted(pt))` is a 1-tuple containing a generator instead of the expected 6-tuple of triples. The `_make = from_iterable` hook on `List` (added to make `mogrify` rebuild the container elementwise) fixed the analogous `forall`-based test and the container-monad cases (`Maybe`, `Writer`, `Either` under Pytkell work fine), but something in the deeper bind-chain recursion under `lazify`'s `mogrify` still produces a generator that doesn't get forced. Workaround for now: use `monadic_do[List]` outside Pytkell, or materialize intermediate results explicitly. Debug starting point: `unpythonic/dialects/tests/test_pytkell.py`, the commented-out List-Pythagorean case in the "monadic do-notation" testset. (Noted 2026-04-17.)
61+
62+

doc/features.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ The exception are the features marked **[M]**, which are primarily intended as a
8585
- [`catch`, `throw`: escape continuations (ec)](#catch-throw-escape-continuations-ec) (as in [Lisp's `catch`/`throw`](http://www.gigamonkeys.com/book/the-special-operators.html), unlike C++ or Java)
8686
- [`call_ec`: first-class escape continuations](#call_ec-first-class-escape-continuations), like Racket's `call/ec`.
8787
- [`forall`: nondeterministic evaluation](#forall-nondeterministic-evaluation), a tuple comprehension with multiple body expressions.
88+
- [Monads](#monads): Identity, Maybe, Either, List, Writer, State, Reader — plus `liftm`/`liftm2`/`liftm3`. For do-notation syntax, see the [`monadic_do` macro](macros.md#monadic_do-do-notation-for-any-monad).
8889
- [`handlers`, `restarts`: conditions and restarts](#handlers-restarts-conditions-and-restarts), a.k.a. **resumable exceptions**.
8990
- [Fundamental signaling protocol](#fundamental-signaling-protocol)
9091
- [API summary](#api-summary)
@@ -3596,6 +3597,159 @@ The implementation is based on the List monad, and a bastardized variant of do-n
35963597
- Last line = implicit `return ...`
35973598
35983599
3600+
### Monads
3601+
3602+
**Added in v2.1.0.**
3603+
3604+
A small zoo of classical monads, living in the `unpythonic.monads` subpackage. For do-notation syntax over any of these, see the [`monadic_do` macro](macros.md#monadic_do-do-notation-for-any-monad).
3605+
3606+
The subpackage is **not** re-exported at the top level — import directly as `from unpythonic.monads import Maybe, Left, Right, ...`. Same style as `from unpythonic.env import env`.
3607+
3608+
Bind uses `>>` (Python's `>>=` is `__irshift__`, in-place, doesn't chain). Sequence uses `.then(other_monad)`. The class itself is the `unit` constructor, so `Identity(x)`, `Maybe(x)`, `List(x)` are the monadic unit forms.
3609+
3610+
#### The base classes
3611+
3612+
- `Monad` — the base class all monads inherit from. Provides default `__rshift__` (bind, via `fmap . join`) and `then` (sequence). Abstract methods: `__init__` (unit), `fmap`, `join`.
3613+
3614+
- `LiftableMonad(Monad)` — adds `lift`, i.e. `(a -> b) -> (a -> M b)`. Inherited by monads where `lift` is well-defined (`Identity`, `Maybe`, `Either`, `List`, `Writer`). `State` and `Reader` inherit from `Monad` directly.
3615+
3616+
Modeled on `unpythonic.slicing.Sliced`: duck-first, `@abstractmethod` as documentation marker rather than strict enforcement.
3617+
3618+
#### `Identity`
3619+
3620+
Pedagogical no-op — ordinary function composition dressed as a monad. Useful as a reference when building or debugging other monads.
3621+
3622+
```python
3623+
from unpythonic.monads import Identity
3624+
3625+
result = Identity(2) >> (lambda x: Identity(x + 1))
3626+
assert result == Identity(3)
3627+
```
3628+
3629+
#### `Maybe`
3630+
3631+
Short-circuiting on "nothing." The unpythonic convention uses `nil` (from `unpythonic.llist`) as the "nothing" sentinel, avoiding proliferation of null singletons. `Maybe(x)` for `x is not nil` is "Just x"; `Maybe(nil)` is "Nothing."
3632+
3633+
```python
3634+
from unpythonic.llist import nil
3635+
from unpythonic.monads import Maybe
3636+
3637+
# happy path
3638+
assert Maybe(10) >> (lambda x: Maybe(x + 1)) == Maybe(11)
3639+
3640+
# short-circuit: Nothing propagates; the lambda is never called
3641+
assert Maybe(nil) >> (lambda x: Maybe(x + 1)) == Maybe(nil)
3642+
```
3643+
3644+
Trade-off: This encoding cannot wrap ``nil`` itself as a present value (Haskell: `Just nil`). In all other cases this yields better UX vs. demanding a ``Some(...)`` wrapper per value.
3645+
3646+
#### `Either`, `Left`, `Right`
3647+
3648+
Maybe's richer sibling — carries an error value down the short-circuit path. `Right` is success, `Left` is failure (by Haskell convention). `Either` itself is abstract; use `Left` and `Right` directly.
3649+
3650+
```python
3651+
from unpythonic.monads import Left, Right
3652+
3653+
assert Right(10) >> (lambda x: Right(x + 1)) == Right(11)
3654+
assert Left("boom") >> (lambda x: Right(x + 1)) == Left("boom")
3655+
```
3656+
3657+
#### `List`
3658+
3659+
Multivalued / nondeterministic computation. Binding through a `List` is `flatMap`: each value in the list becomes a sub-computation that produces its own list of results, and all sub-results are concatenated.
3660+
3661+
Replaces `MonadicList` from `unpythonic.amb`, which is kept as a deprecated alias — see `unpythonic.amb.MonadicList` for the back-compat note.
3662+
3663+
Varargs constructor — the class itself is the monadic unit. `List(1, 2, 3)` for literals; `List.from_iterable(iter)` to build from an existing iterable.
3664+
3665+
```python
3666+
from unpythonic.monads import List
3667+
3668+
# bind = flatMap
3669+
assert (List(1, 2, 3) >> (lambda x: List(x, x * 10))) == List(1, 10, 2, 20, 3, 30)
3670+
3671+
# Pythagorean triples — the canonical List-monad example
3672+
def r(lo, hi):
3673+
return List.from_iterable(range(lo, hi))
3674+
pt = r(1, 21) >> (lambda z:
3675+
r(1, z + 1) >> (lambda x:
3676+
r(x, z + 1) >> (lambda y:
3677+
List.guard(x*x + y*y == z*z).then(
3678+
List((x, y, z))))))
3679+
assert tuple(sorted(pt)) == ((3, 4, 5), (5, 12, 13), (6, 8, 10),
3680+
(8, 15, 17), (9, 12, 15), (12, 16, 20))
3681+
```
3682+
3683+
Full `Sequence` ABC (`__len__`, `__getitem__`, `__contains__`, etc.); ABC registration so `isinstance(List(...), Sequence)` is `True`. Concatenation via `+`.
3684+
3685+
#### `Writer`
3686+
3687+
Pure-functional audit log. `Writer(value, log)` wraps a pair; binding threads the value through while concatenating logs. The log can be any type supporting `+` (default: empty `""`).
3688+
3689+
```python
3690+
from unpythonic.monads import Writer
3691+
3692+
result = (Writer(10)
3693+
>> (lambda x: Writer(x + 1, "added 1; "))
3694+
>> (lambda y: Writer(y * 2, "doubled; ")))
3695+
assert result.data == (22, "added 1; doubled; ")
3696+
```
3697+
3698+
`Writer.tell(msg)` interleaves a log entry without touching the value — useful as `computation.then(Writer.tell("done; "))`.
3699+
3700+
#### `State`
3701+
3702+
Threading a state value through a pure computation. Wraps a function `s -> (a, s)`: takes an input state, produces a data value, returns a new state. The state only becomes bound when the composed chain is `.run(s0)`; until then, it's a recipe.
3703+
3704+
```python
3705+
from unpythonic.monads import State
3706+
3707+
bump = State(lambda s: (s, s + 1))
3708+
chain = (bump
3709+
>> (lambda a: bump
3710+
>> (lambda b: bump
3711+
>> (lambda c: State.unit((a, b, c))))))
3712+
values, final = chain.run(10)
3713+
assert values == (10, 11, 12) and final == 13
3714+
```
3715+
3716+
Helper classmethods: `State.unit`, `State.get`, `State.put`, `State.modify`, `State.gets`. Accessors on a `State` instance: `.run(s)`, `.eval(s)` (data only), `.exec(s)` (state only).
3717+
3718+
Does not inherit from `LiftableMonad``lift` doesn't have a canonical shape for State.
3719+
3720+
#### `Reader`
3721+
3722+
Read-only shared environment. Wraps a function `e -> a`. The environment threads through the chain; each step can `.ask()` for it.
3723+
3724+
```python
3725+
from unpythonic.monads import Reader
3726+
3727+
config = {"multiplier": 3, "offset": 10}
3728+
chain = (Reader.asks(lambda e: e["multiplier"])
3729+
>> (lambda m: Reader.asks(lambda e: e["offset"])
3730+
>> (lambda o: Reader.unit(m * 5 + o))))
3731+
assert chain.run(config) == 25
3732+
```
3733+
3734+
Helper classmethods: `Reader.unit`, `Reader.ask`, `Reader.asks`. Instance methods: `.run(env)`, `.local(f)` (run in an `f`-modified environment).
3735+
3736+
Does not inherit from `LiftableMonad` for the same reason as `State`.
3737+
3738+
#### `liftm`, `liftm2`, `liftm3`
3739+
3740+
Lift regular 1-, 2-, 3-argument functions into monadic ones. Distinct from `LiftableMonad.lift`: `lift: (a -> b) -> (a -> M b)` expects the caller to bind; `liftm: (a -> r) -> (M a -> M r)` binds internally.
3741+
3742+
```python
3743+
from unpythonic.monads import Maybe, liftm2
3744+
3745+
add = lambda x, y: x + y
3746+
add_m = liftm2(Maybe, add)
3747+
assert add_m(Maybe(3), Maybe(4)) == Maybe(7)
3748+
```
3749+
3750+
The `M` parameter is curry-friendly (changes least often) — `functools.partial(liftm2, Maybe)` gives you a Maybe-specific lifter.
3751+
3752+
35993753
### `handlers`, `restarts`: conditions and restarts
36003754
36013755
**Changed in v0.15.0.** *Functions `resignal_in` and `resignal` added; these perform the same job for conditions as `reraise_in` and `reraise` do for exceptions, that is, they allow you to map library exception types to semantically appropriate application exception types, with minimum boilerplate.*

doc/macros.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Because in Python macro expansion occurs *at import time*, Python programs whose
6565
- [Why this syntax?](#why-this-syntax)
6666
- [`prefix`: prefix function call syntax for Python](#prefix-prefix-function-call-syntax-for-python)
6767
- [`autoreturn`: implicit `return` in tail position](#autoreturn-implicit-return-in-tail-position), like in Lisps.
68+
- [`monadic_do`: do-notation for any monad](#monadic_do-do-notation-for-any-monad), over [unpythonic's classical monad zoo](features.md#monads).
6869
- [`forall`: nondeterministic evaluation](#forall-nondeterministic-evaluation) with monadic do-notation for Python.
6970

7071
[**Convenience features**](#convenience-features)
@@ -1809,6 +1810,64 @@ For code using **conditions and restarts**: there is no special integration betw
18091810
- The `with handlers` form itself is just `with` block, so it also gets the `autoreturn` treatment.
18101811

18111812

1813+
### `monadic_do`: do-notation for any monad
1814+
1815+
**Added in v2.1.0.**
1816+
1817+
Monadic do-notation over any of the monads in [`unpythonic.monads`](features.md#monads) (or, for that matter, any object that implements `__rshift__` as monadic bind).
1818+
1819+
The body of `with monadic_do[M] as result:` must be a single statement of the form `[bindings] in result << final_expr`. Each binding is a `name := mexpr` pair; `name << mexpr` is accepted as a deprecated alternative (the same shapes `letdoutil` understands for `let[]`). The `result << final_expr` on the RHS of `in` is where the final monadic value lands; this is the "send to box" exit idiom unpythonic uses elsewhere (e.g., the condition/restart subsystem), sidestepping the stmt/expr distinction without hijacking `return`.
1820+
1821+
```python
1822+
from unpythonic.syntax import macros, monadic_do
1823+
from unpythonic.monads import Maybe, Left, Right, List, Writer
1824+
from unpythonic.llist import nil
1825+
1826+
# Maybe — happy path
1827+
with monadic_do[Maybe] as result:
1828+
[x := Maybe(10),
1829+
y := Maybe(x + 1)] in result << Maybe(x + y)
1830+
assert result == Maybe(21)
1831+
1832+
# Maybe — short-circuit. The `y := ...` line is never evaluated.
1833+
with monadic_do[Maybe] as result:
1834+
[x := Maybe(nil),
1835+
y := Maybe(x + 1)] in result << Maybe(x + y)
1836+
assert result == Maybe(nil)
1837+
1838+
# List — Pythagorean triples
1839+
def r(lo, hi):
1840+
return List.from_iterable(range(lo, hi))
1841+
with monadic_do[List] as pt:
1842+
[z := r(1, 21),
1843+
x := r(1, z + 1),
1844+
y := r(x, z + 1),
1845+
_ := List.guard(x*x + y*y == z*z)] in pt << List((x, y, z))
1846+
assert tuple(sorted(pt)) == ((3, 4, 5), (5, 12, 13), (6, 8, 10),
1847+
(8, 15, 17), (9, 12, 15), (12, 16, 20))
1848+
```
1849+
1850+
Sequencing-only lines (Haskell `do { mx; ...; }` — a bind whose result is discarded) are spelled `_ := mexpr`. The throwaway `_` makes the intent visible. Empty bindings are allowed: `[] in result << M.unit(x)` reduces to `result = M.unit(x)`.
1851+
1852+
Expands to a nested lambda-bind chain:
1853+
1854+
```python
1855+
result = mx >> (lambda x: my(x) >> (lambda y: final_expr))
1856+
```
1857+
1858+
**Placement in the xmas tree**: `monadic_do` is always the **innermost** `with`. Its body-shape constraint (a single `[bindings] in result << expr` statement) forbids lexically wrapping other `with` blocks inside. Outer two-pass macros (`lazify`, `continuations`, `tco`, `autocurry`, etc.) expand inner macros between their passes, so they will see and edit the expanded bind chain in the right order.
1859+
1860+
```python
1861+
with lazify:
1862+
with monadic_do[Maybe] as result:
1863+
...
1864+
```
1865+
1866+
For the pure-Python monads themselves and the `liftm` helpers, see [features.md](features.md#monads).
1867+
1868+
For the List-monad-specific do-notation that existed first, see [`forall`](#forall-nondeterministic-evaluation) below.
1869+
1870+
18121871
### `forall`: nondeterministic evaluation
18131872

18141873
**Changed in v0.15.3.** *Env-assignment now uses the assignment expression syntax `x := range(3)`. The old syntax `x << range(3)` is still supported for backward compatibility.*

0 commit comments

Comments
 (0)