Skip to content

Commit 65abdf3

Browse files
Technologicatclaude
andcommitted
monadic_do: final expression joins the list; drop the in separator
Third and last UX pass before shipping. The `in` separator was a vestige of the `let[]` syntax origins; in a monadic do-block, every line is morally "one step of the computation" — bindings and the final expression alike — so they all belong in the same list. Before: with monadic_do[List] as pt: [z := r(1, 21), x := r(1, z + 1), y := r(x, z + 1), List.guard(x*x + y*y == z*z)] in List((x, y, z)) After (item-for-item identical to the Haskell do-block it models): with monadic_do[List] as pt: [z := r(1, 21), x := r(1, z + 1), y := r(x, z + 1), List.guard(x*x + y*y == z*z), List((x, y, z))] Rule: body is a single list literal. All items except the last are binds (`name := mexpr`, `name << mexpr` legacy, or bare `mexpr` for sequencing); the last item is the final monadic expression. The previous empty-bindings shorthand `[] in M.unit(x)` now reads as the natural single-element list `[M.unit(x)]`. Also: .gitignore extended to cover stray artifacts (`.coverage`, `codecov-token`, scratch files) that nearly made it into an earlier commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cd5fdd8 commit 65abdf3

7 files changed

Lines changed: 135 additions & 97 deletions

File tree

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,19 @@ pdm.lock
1313
*.egg-info
1414
*.mypy_cache
1515
.python-version
16+
17+
# Coverage artifacts
18+
.coverage
19+
coverage.xml
20+
htmlcov/
21+
22+
# Secrets — should never be committed
23+
codecov-token
24+
*.token
25+
.env
26+
27+
# Scratch / local-only files
28+
coverage-notes.md
29+
test_system_notes.txt
30+
contidea.py
31+
unpythonic/tests/mwe.py

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -734,7 +734,8 @@ from unpythonic.monads import Maybe, List
734734
# Maybe — do-notation threads present values (`Maybe(value)`); any absence (`Maybe(nil)`) short-circuits.
735735
with monadic_do[Maybe] as result:
736736
[x := Maybe(10),
737-
y := Maybe(x + 1)] in Maybe(x + y)
737+
y := Maybe(x + 1),
738+
Maybe(x + y)]
738739
assert result == Maybe(21)
739740

740741
# List — Pythagorean triples via the list monad. Bare `List.guard(...)`
@@ -745,12 +746,13 @@ with monadic_do[List] as pt:
745746
[z := r(1, 21),
746747
x := r(1, z + 1),
747748
y := r(x, z + 1),
748-
List.guard(x*x + y*y == z*z)] in List((x, y, z))
749+
List.guard(x*x + y*y == z*z),
750+
List((x, y, z))]
749751
assert tuple(sorted(pt)) == ((3, 4, 5), (5, 12, 13), (6, 8, 10),
750752
(8, 15, 17), (9, 12, 15), (12, 16, 20))
751753
```
752754

753-
Body shape is a single `[bindings] in final_expr` statement: monadic binds on the left of `in` (`name := mexpr` or bare `mexpr` for sequencing), the final monadic expression on the right (any expression of the right type, like the last line of a Haskell `do`). The `as result` on the `with` names the target.
755+
Body shape is a single list literal. Each item is one line of a Haskell do-block: `name := mexpr` for monadic bind, `name << mexpr` (legacy) for the same, or bare `mexpr` for sequencing-only (matches Haskell's `guard`-style lines). The last item is the final monadic expression. `as result` on the `with` names the target.
754756
</details>
755757
<details><summary>Genuine multi-shot continuations (call/cc).</summary>
756758

doc/macros.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1816,12 +1816,12 @@ For code using **conditions and restarts**: there is no special integration betw
18161816

18171817
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).
18181818

1819-
The body of `with monadic_do[M] as result:` must be a single statement of the form `[bindings] in final_expr`. Each binding is one of:
1819+
The body of `with monadic_do[M] as result:` is a single list literal. Each item corresponds to one line of a Haskell do-block:
18201820

18211821
- `name := mexpr`**monadic bind**: unwrap the monadic value and bind it to `name` for subsequent lines. Also accepts the legacy `name << mexpr` form that `letdoutil` understands for `let[]`.
18221822
- a bare `mexpr`**sequencing-only** (Haskell's `do { mx; ... }`): the monadic value is threaded through the chain but its unwrapped value is discarded. Used e.g. for `guard`-style filter lines.
18231823

1824-
The RHS of `in` is the final monadic expression — any expression of the right monad type, same semantics as Haskell's last-line-of-do (can be a constructor call, a call to a monad-producing function, anything of type `M a`). The `as result` on the `with` tells the macro where to land the computed value.
1824+
The **last item** is the final monadic expression — any expression of the right monad type, same semantics as Haskell's last-line-of-do (a constructor call, a call to a monad-producing function, anything of type `M a`). The `as result` on the `with` tells the macro where to land the computed value.
18251825

18261826
```python
18271827
from unpythonic.syntax import macros, monadic_do
@@ -1831,13 +1831,15 @@ from unpythonic.llist import nil
18311831
# Maybe — happy path
18321832
with monadic_do[Maybe] as result:
18331833
[x := Maybe(10),
1834-
y := Maybe(x + 1)] in Maybe(x + y)
1834+
y := Maybe(x + 1),
1835+
Maybe(x + y)]
18351836
assert result == Maybe(21)
18361837

18371838
# Maybe — short-circuit. The `y := ...` line is never evaluated.
18381839
with monadic_do[Maybe] as result:
18391840
[x := Maybe(nil),
1840-
y := Maybe(x + 1)] in Maybe(x + y)
1841+
y := Maybe(x + 1),
1842+
Maybe(x + y)]
18411843
assert result == Maybe(nil)
18421844

18431845
# List — Pythagorean triples. The bare `List.guard(...)` line is a
@@ -1849,12 +1851,13 @@ with monadic_do[List] as pt:
18491851
[z := r(1, 21),
18501852
x := r(1, z + 1),
18511853
y := r(x, z + 1),
1852-
List.guard(x*x + y*y == z*z)] in List((x, y, z))
1854+
List.guard(x*x + y*y == z*z),
1855+
List((x, y, z))]
18531856
assert tuple(sorted(pt)) == ((3, 4, 5), (5, 12, 13), (6, 8, 10),
18541857
(8, 15, 17), (9, 12, 15), (12, 16, 20))
18551858
```
18561859

1857-
Empty bindings are allowed: `[] in M.unit(x)` reduces to `result = M.unit(x)`.
1860+
The no-binds case is just a single-element list: `[M.unit(x)]` reduces to `result = M.unit(x)`.
18581861

18591862
Expands to a nested lambda-bind chain:
18601863

@@ -1864,7 +1867,7 @@ result = mx >> (lambda x: my(x) >> (lambda y: final_expr))
18641867

18651868
Sequencing-only lines (bare `mexpr`) are rewritten to `_ := mexpr` internally and participate in the same chain; their unwrapped value is bound to `_` and ignored.
18661869

1867-
**Placement in the xmas tree**: `monadic_do` is always the **innermost** `with`. Its body-shape constraint (a single `[bindings] in final_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.
1870+
**Placement in the xmas tree**: `monadic_do` is always the **innermost** `with`. Its body-shape constraint (a single list-literal 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.
18681871

18691872
```python
18701873
with lazify:

unpythonic/dialects/tests/test_pytkell.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,12 +248,14 @@ def maybe_sqrt(x):
248248

249249
with monadic_do[Maybe] as root4:
250250
[a := maybe_sqrt(16),
251-
b := maybe_sqrt(a)] in Maybe(b)
251+
b := maybe_sqrt(a),
252+
Maybe(b)]
252253
test[root4 == Maybe(2.0)]
253254

254255
with monadic_do[Maybe] as bad:
255256
[a := maybe_sqrt(-1),
256-
b := maybe_sqrt(a)] in Maybe(b)
257+
b := maybe_sqrt(a),
258+
Maybe(b)]
257259
test[bad == Maybe(nil)] # noqa: F821 -- `nil` is in the Pytkell dialect
258260

259261
# List — classical Pythagorean triples.
@@ -264,14 +266,16 @@ def r(lo, hi):
264266
[z := r(1, 21),
265267
x := r(1, z + 1),
266268
y := r(x, z + 1),
267-
List.guard(x * x + y * y == z * z)] in List((x, y, z))
269+
List.guard(x * x + y * y == z * z),
270+
List((x, y, z))]
268271
test[tuple(sorted(pt)) == ((3, 4, 5), (5, 12, 13), (6, 8, 10),
269272
(8, 15, 17), (9, 12, 15), (12, 16, 20))]
270273

271274
# Writer — logged computation.
272275
with monadic_do[Writer] as w:
273276
[a := Writer(10, "start; "),
274-
b := Writer(a + 1, "+1; ")] in Writer(b * 2, "doubled; ")
277+
b := Writer(a + 1, "+1; "),
278+
Writer(b * 2, "doubled; ")]
275279
value, log = w.data
276280
test[value == 22]
277281
test[log == "start; +1; doubled; "]

unpythonic/syntax/monadic_do.py

Lines changed: 40 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,37 +6,33 @@
66
with monadic_do[M] as result:
77
[x := mx,
88
y := my(x),
9-
M.guard(...)] in M.unit(x + y)
9+
M.guard(...),
10+
M.unit(x + y)]
1011
11-
Expands to::
12+
The body is a single list literal. Each item corresponds to one line of
13+
a Haskell do-block. The **last item** is the final monadic expression
14+
(any expression of type ``M a``, matching Haskell's last-line-of-do).
15+
All **earlier items** are binds:
1216
13-
result = mx >> (lambda x: my(x) >> (lambda _: M.guard(...) >> (lambda _: M.unit(x + y))))
14-
15-
The bindings list on the left of ``in`` uses the same ``:=`` / ``<<``
16-
binding syntax that ``let`` uses, parsed by ``letdoutil.canonize_bindings``.
17+
- ``name := mexpr`` — monadic bind: the unwrapped value is bound to
18+
``name`` for subsequent lines.
19+
- ``name << mexpr`` — legacy alternative for ``:=`` (same shapes
20+
``letdoutil`` recognizes for ``let[]``).
21+
- a bare ``mexpr`` — sequencing-only (Haskell's ``do { mx; ... }``): the
22+
result is threaded but discarded. The short-circuit behavior of the
23+
monad still applies (``Maybe(nil)``, ``Left``, empty ``List`` all
24+
cancel the rest of the chain).
1725
18-
- A ``name := mexpr`` entry introduces a monadic bind: the ``name`` is
19-
bound to the unwrapped value for subsequent lines.
20-
- A bare ``mexpr`` entry (no ``:=``) is a sequencing-only line — matches
21-
Haskell do-notation's bare-expression form, used e.g. for ``guard``:
22-
the result is threaded through the chain but discarded, so the whole
23-
shape short-circuits for monads that do (Maybe's ``Nothing``, List's
24-
empty, Either's ``Left``, etc.).
26+
Expands to a nested lambda-bind chain::
2527
26-
The RHS of ``in`` is simply the final monadic expression — same
27-
semantics as Haskell, where the last line of a ``do`` block is any
28-
monadic value (``return (...)``, a direct constructor call, or a call
29-
to a monad-producing function). No specific form required.
30-
31-
Empty bindings shorthand is supported: ``[] in M.unit(x)`` expands to
32-
just ``result = M.unit(x)``.
28+
result = mx >> (lambda x: my(x) >> (lambda _: M.guard(...) >> (lambda _: M.unit(x + y))))
3329
3430
**Placement in the xmas tree**: always the innermost ``with``. Its body
35-
shape (a single ``[bindings] in final_expr`` statement) forbids
36-
lexically wrapping other ``with`` blocks inside it, and outer two-pass
37-
macros (``lazify``, ``continuations``, ``tco``, ``autocurry``, etc.)
38-
expand inner macros between their two passes, which means they will
39-
correctly see and edit the expanded bind chain.
31+
shape (a single list-literal statement) forbids lexically wrapping other
32+
``with`` blocks inside it, and outer two-pass macros (``lazify``,
33+
``continuations``, ``tco``, ``autocurry``, etc.) expand inner macros
34+
between their two passes, which means they will correctly see and edit
35+
the expanded bind chain.
4036
4137
**Always in its own nested ``with``** — unlike the other xmas-tree
4238
macros which chain in one ``with`` for brevity, ``monadic_do[M] as result``
@@ -46,7 +42,7 @@
4642

4743
__all__ = ["monadic_do"]
4844

49-
from ast import Compare, In, List, Name, NamedExpr, BinOp, LShift, Expr, Assign, Store, arg, expr
45+
from ast import List, Name, NamedExpr, BinOp, LShift, Expr, Assign, Store, arg, expr
5046

5147
from mcpyrate.quotes import macros, q, a, n # noqa: F401
5248

@@ -91,47 +87,37 @@ def _monadic_do(block_body: list, monad_type: expr, result_name: str) -> list:
9187
# Expand inner macros first (outside-in), just like `forall` and `autoref` do.
9288
block_body = dyn._macro_expander.visit_recursively(block_body)
9389

94-
# Body must be exactly one statement, an Expr wrapping a Compare(In).
90+
# Body must be exactly one statement, an Expr wrapping a List literal.
9591
if len(block_body) != 1:
9692
raise SyntaxError(
97-
f"monadic_do body must be a single statement of the form "
98-
f"`[bindings] in final_expr`, got {len(block_body)} statements"
93+
f"monadic_do body must be a single list-literal statement, got {len(block_body)} statements"
9994
) # pragma: no cover
10095
stmt = block_body[0]
101-
if type(stmt) is not Expr:
96+
if type(stmt) is not Expr or type(stmt.value) is not List:
10297
raise SyntaxError(
103-
"monadic_do body must be a single expression statement "
104-
"`[bindings] in final_expr`"
105-
) # pragma: no cover
106-
compare = stmt.value
107-
if not (type(compare) is Compare and
108-
len(compare.ops) == 1 and
109-
type(compare.ops[0]) is In):
110-
raise SyntaxError(
111-
"monadic_do body must have the form `[bindings] in final_expr`"
98+
"monadic_do body must be a single list literal `[bind, ..., final_expr]`"
11299
) # pragma: no cover
113100

114-
bindings_node = compare.left
115-
final_expr = compare.comparators[0]
116-
117-
# Bindings: must be a List literal.
118-
if type(bindings_node) is not List:
101+
items = stmt.value.elts
102+
if not items:
119103
raise SyntaxError(
120-
"monadic_do bindings must be a list literal `[x := mx, ...]`"
104+
"monadic_do body list must have at least one item (the final monadic expression)"
121105
) # pragma: no cover
122106

123-
# Wrap bare expressions as `_ := expr` so they look like sequencing-only
124-
# bindings to `canonize_bindings`. This mirrors Haskell's do-notation
125-
# where a bare expression line is sequence-only (>>, not >>=).
126-
normalized_elts = [
107+
# Split: all but the last are binds; the last is the final monadic expression.
108+
*binding_items, final_expr = items
109+
110+
# Normalize bare expressions in the binds as synthetic `_ := expr` so they
111+
# look like sequencing-only bindings to `canonize_bindings`. Matches Haskell's
112+
# do-notation where a bare expression line is sequence-only (>>, not >>=).
113+
normalized = [
127114
item if _is_binding_form(item) else NamedExpr(target=Name(id="_", ctx=Store()), value=item)
128-
for item in bindings_node.elts
115+
for item in binding_items
129116
]
130117

131-
# Parse via letdoutil — accepts := and << for each binding, and []/() for the list shape
132-
# (we already unpacked the outer List).
133-
if normalized_elts:
134-
canonical = canonize_bindings(normalized_elts) # [Tuple(elts=[Name(k), v]), ...]
118+
# Parse via letdoutil — accepts := and <<.
119+
if normalized:
120+
canonical = canonize_bindings(normalized) # [Tuple(elts=[Name(k), v]), ...]
135121
pairs = [(t.elts[0].id, t.elts[1]) for t in canonical]
136122
else:
137123
pairs = []

0 commit comments

Comments
 (0)