You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
Copy file name to clipboardExpand all lines: AUTHORS.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
3
3
- Juha Jeronen (@Technologicat) - original author
4
4
-@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)
Copy file name to clipboardExpand all lines: CHANGELOG.md
+3Lines changed: 3 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,6 +4,8 @@
4
4
5
5
**New**:
6
6
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.
7
9
-`environ_override`: context manager to temporarily override OS environment variables within a `with` block, restoring the previous state on exit.
8
10
- Thread-safe (serialises concurrent overrides via `RLock`); same-thread nesting supported.
9
11
- 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 @@
25
27
26
28
**Changed**:
27
29
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)`.
28
31
-`unpythonic.net` (REPL server and client) now runs on MS Windows.
29
32
- Previously the whole subsystem was POSIX-only because `unpythonic.net.ptyproxy` required `termios`, `tty`, and `os.openpty`.
30
33
- A new `socket.socketpair()`-based backend stands in for the pty master/slave endpoints on Windows, plugged in via a platform dispatch in `PTYSocketProxy`.
Copy file name to clipboardExpand all lines: README.md
+53Lines changed: 53 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -335,6 +335,31 @@ The condition system is the clean, general solution to this problem. It automati
335
335
336
336
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.
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`.
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.
Copy file name to clipboardExpand all lines: TODO_DEFERRED.md
+6Lines changed: 6 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -54,3 +54,9 @@ Next unused item code: D16
54
54
-**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.)
55
55
56
56
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.)
Copy file name to clipboardExpand all lines: doc/features.md
+154Lines changed: 154 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -85,6 +85,7 @@ The exception are the features marked **[M]**, which are primarily intended as a
85
85
-[`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)
86
86
-[`call_ec`: first-class escape continuations](#call_ec-first-class-escape-continuations), like Racket's `call/ec`.
87
87
-[`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).
88
89
-[`handlers`, `restarts`: conditions and restarts](#handlers-restarts-conditions-and-restarts), a.k.a. **resumable exceptions**.
@@ -3596,6 +3597,159 @@ The implementation is based on the List monad, and a bastardized variant of do-n
3596
3597
- Last line = implicit `return...`
3597
3598
3598
3599
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 classall 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) >> (lambdax: 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 isnot nil`is"Just x"; `Maybe(nil)`is"Nothing."
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.
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, andall 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.
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 anytype supporting `+` (default: empty `""`).
3688
+
3689
+
```python
3690
+
from unpythonic.monads import Writer
3691
+
3692
+
result= (Writer(10)
3693
+
>> (lambdax: Writer(x +1, "added 1; "))
3694
+
>> (lambday: 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(lambdas: (s, s +1))
3708
+
chain= (bump
3709
+
>> (lambdaa: bump
3710
+
>> (lambdab: bump
3711
+
>> (lambdac: 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(lambdae: e["multiplier"])
3729
+
>> (lambdam: Reader.asks(lambdae: e["offset"])
3730
+
>> (lambdao: 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=lambdax, 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
+
3599
3753
### `handlers`, `restarts`: conditions and restarts
3600
3754
3601
3755
**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.*
Copy file name to clipboardExpand all lines: doc/macros.md
+59Lines changed: 59 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -65,6 +65,7 @@ Because in Python macro expansion occurs *at import time*, Python programs whose
65
65
-[Why this syntax?](#why-this-syntax)
66
66
-[`prefix`: prefix function call syntax for Python](#prefix-prefix-function-call-syntax-for-python)
67
67
-[`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).
68
69
-[`forall`: nondeterministic evaluation](#forall-nondeterministic-evaluation) with monadic do-notation for Python.
69
70
70
71
[**Convenience features**](#convenience-features)
@@ -1809,6 +1810,64 @@ For code using **conditions and restarts**: there is no special integration betw
1809
1810
- The `with handlers` form itself is just `with` block, so it also gets the `autoreturn` treatment.
1810
1811
1811
1812
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
+
defr(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))
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 >> (lambdax: my(x) >> (lambday: 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
+
1812
1871
### `forall`: nondeterministic evaluation
1813
1872
1814
1873
**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