Skip to content

Commit 36a6d2b

Browse files
Technologicatclaude
andcommitted
amb: standardize MonadicList constructor, register as Sequence
MonadicList.__init__ now takes a single iterable (like list/tuple), replacing the non-standard *elts variadic form. This makes it a well-behaved Sequence — registered as Iterable, Sized, Sequence. Added __reversed__, __contains__, index, count for full Sequence compliance. Removed nil sentinel dependency (empty constructor suffices). Also: Assignment renamed to Choice, internal env to Scope (previous commit); D13 added to TODO_DEFERRED.md (teaching-friendly monads). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 666692b commit 36a6d2b

3 files changed

Lines changed: 40 additions & 45 deletions

File tree

TODO_DEFERRED.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Deferred Issues
22

3-
Next unused item code: D13
3+
Next unused item code: D14
44

55
- **D5**: `dispatch.py` — moved to GitHub issue #99. Dispatch-layer improvements for parametric ABCs (warn/error on indistinguishable multimethods). Typecheck-layer part resolved.
66

@@ -41,3 +41,6 @@ Next unused item code: D13
4141
**When to actually do it**: only if tier 1 coverage turns out to miss something important (a regression hits prod that tier 1 would not have caught). The in-thread server + scripted client approach already exercises most of the protocol surface; tier 2 is primarily a safety net for terminal-semantics and signal-path bugs. Until one of those bites, tier 1 is the main win. (Added 2026-04-15, alongside the tier 1 bring-up.)
4242

4343

44+
- **D13: Teaching-friendly monad abstractions**: Port the monad hacks from https://github.com/Technologicat/python-3-scicomp-intro/tree/master/examples (monads.py) into unpythonic. `MonadicList` already exists in `amb.py` as precedent; the teaching examples include additional monad abstractions that could be generally useful. Some overlap with OSlash, but unpythonic already duplicates stdlib/third-party functionality where it adds value in its own voice (conditions/restarts, fold/scan suite). (Noted 2026-04-16.)
45+
46+

unpythonic/amb.py

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,10 @@
3333
__all__ = ["forall", "choice", "insist", "deny"]
3434

3535
from collections import namedtuple
36-
from collections.abc import Callable, Container, Iterable, Iterator, Sized
36+
from collections.abc import Callable, Iterable, Iterator, Sequence, Sized
3737
from typing import Any
3838

3939
from .arity import arity_includes, UnknownArity
40-
from .llist import nil # we need a sentinel, let's recycle the existing one
4140

4241
Choice = namedtuple("Choice", "k v")
4342

@@ -219,22 +218,20 @@ def monadify(value: Any, unpack: bool = True) -> "MonadicList":
219218
return MonadicList.from_iterable(value)
220219
except TypeError:
221220
pass # fall through
222-
return MonadicList(value) # unit(MonadicList, value)
221+
return MonadicList((value,)) # unit
223222

224223
class MonadicList: # TODO: This if anything is **the** place to use @typed.
225224
"""A monadic list."""
226-
def __init__(self, *elts: Any) -> None:
227-
"""The unit operator. Lift value(s) into a MonadicList.
225+
def __init__(self, iterable: Iterable = ()) -> None:
226+
"""Construct a MonadicList from an iterable.
228227
229-
*elts: a or [a]
228+
iterable: Iterable[a]
230229
returns: M a
230+
231+
Like ``list`` and ``tuple``, accepts a single iterable argument.
232+
Use ``MonadicList((value,))`` for a singleton (the unit operator).
231233
"""
232-
# Accept the sentinel nil as a special **item** that, when passed to
233-
# the MonadicList constructor, produces an empty list.
234-
if len(elts) == 1 and elts[0] is nil:
235-
self.x = ()
236-
else:
237-
self.x = elts
234+
self.x = tuple(iterable)
238235

239236
def __rshift__(self, f: Callable) -> "MonadicList":
240237
"""Monadic bind; standard notation ">>=" in Haskell.
@@ -283,13 +280,12 @@ def guard(cls, b: Any) -> "MonadicList":
283280
- Use `.then(...)` just after the `guard` to discard the dummy, and replace with
284281
the actual output you want. The value (that passed the guard) from the original
285282
`MonadicList` is still live in the current scope.
286-
- If you just want to filter, just `MonadicList(x)` it (recall that here the
287-
constructor stands for the `unit` operator).
283+
- If you just want to filter, just `MonadicList((x,))` it (the unit operator).
288284
- When an input doesn't pass the guard, the blank output from `guard` automatically
289285
cancels the rest of that branch of the computation.
290286
"""
291287
if b:
292-
return cls(True) # MonadicList with one element; value not intended to be actually used.
288+
return cls((True,)) # MonadicList with one element; value not intended to be actually used.
293289
return cls() # 0-element MonadicList; short-circuit this branch of the computation.
294290

295291
# Sequence ABC interface.
@@ -334,15 +330,12 @@ def from_iterable(cls, iterable: Iterable) -> "MonadicList":
334330
Eager; the input iterable will be iterated over in its entirety
335331
to produce the list. If it is consumable, it will be consumed.
336332
"""
337-
try:
338-
return cls(*iterable)
339-
except TypeError: # maybe a generator; try forcing it before giving up.
340-
return cls(*tuple(iterable))
333+
return cls(iterable)
341334

342335
def copy(self) -> "MonadicList":
343336
"""Return a copy of this MonadicList."""
344337
cls = self.__class__
345-
return cls(*self.x)
338+
return cls(self.x)
346339

347340
@classmethod
348341
def lift(cls, f: Callable) -> Callable:
@@ -351,7 +344,7 @@ def lift(cls, f: Callable) -> Callable:
351344
f: a -> b
352345
returns: a -> M b
353346
"""
354-
return lambda x: cls(f(x))
347+
return lambda x: cls((f(x),))
355348

356349
def fmap(self, f: Callable) -> "MonadicList":
357350
"""The map operator.
@@ -381,9 +374,8 @@ def deny(v: Any) -> Any:
381374
return insist(not v)
382375

383376
# register virtual base classes
384-
# Not registering as Sequence: mogrify (in unpythonic.collections) expects
385-
# Sequence constructors to accept a single iterable, but MonadicList uses *elts.
386-
for _abscls in (Container, Iterable, Sized):
377+
# register virtual base classes
378+
for _abscls in (Iterable, Sized, Sequence):
387379
_abscls.register(MonadicList)
388380
del _abscls
389381

unpythonic/tests/test_amb.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,62 +4,62 @@
44
from ..test.fixtures import session, testset
55

66
from ..amb import (forall, choice, insist, deny, ok, fail,
7-
Choice, MonadicList, nil)
7+
Choice, MonadicList)
88

99
def runtests():
1010
with testset("MonadicList (internal utility)"):
11-
m = MonadicList(1, 2, 3)
11+
m = MonadicList([1, 2, 3])
1212
test[tuple(m) == (1, 2, 3)]
1313
test[len(m) == 3]
1414
test[m[0] == 1 and m[1] == 2 and m[2] == 3]
1515

16-
m = MonadicList(nil) # special *item* that produces an empty *list*
16+
m = MonadicList() # empty
1717
test[tuple(m) == ()]
1818

1919
# Monadic bind (for MonadicList, it's flatmap).
2020
# This also tests fmap and join.
21-
m = MonadicList(1, 2, 3)
22-
f = lambda a: MonadicList(a, 10 * a) # a -> M b
21+
m = MonadicList([1, 2, 3])
22+
f = lambda a: MonadicList([a, 10 * a]) # a -> M b
2323
test[tuple(m >> f) == (1, 10, 2, 20, 3, 30)]
2424

2525
# .then(...): discard current value, replace by given value.
2626
# The new value must be wrapped in MonadicList.
27-
m = MonadicList(1, 2, 3)
28-
const = MonadicList(42) # M b
27+
m = MonadicList([1, 2, 3])
28+
const = MonadicList((42,)) # M b (singleton)
2929
test[tuple(m.then(const)) == (42, 42, 42)] # one 42 for each element of m
3030

3131
test_raises[TypeError, m.then(f)] # expected a MonadicList, got a function
3232

33-
m1 = MonadicList(1, 2)
34-
m2 = MonadicList(3, 4, 5)
33+
m1 = MonadicList([1, 2])
34+
m2 = MonadicList([3, 4, 5])
3535
test[m1 == m1]
3636
test[the[m2] != the[m1]]
3737

38-
m1 = MonadicList(1, 2)
39-
m2 = MonadicList(3, 4)
40-
test[m1 + m2 == MonadicList(1, 2, 3, 4)]
38+
m1 = MonadicList([1, 2])
39+
m2 = MonadicList([3, 4])
40+
test[m1 + m2 == MonadicList([1, 2, 3, 4])]
4141

42-
m1 = MonadicList(1, 2)
42+
m1 = MonadicList([1, 2])
4343
notamonadiclist = (3, 4)
4444
test_raises[TypeError, m1 + notamonadiclist]
4545

46-
test[MonadicList.from_iterable(range(3)) == MonadicList(0, 1, 2)]
46+
test[MonadicList.from_iterable(range(3)) == MonadicList([0, 1, 2])]
4747

48-
m1 = MonadicList(1, 2, 3)
48+
m1 = MonadicList([1, 2, 3])
4949
m2 = m1.copy()
5050
test[the[m2] is not the[m1] and m2 == m1]
5151

5252
double = lambda x: 2 * x
53-
m = MonadicList(1, 2, 3)
53+
m = MonadicList([1, 2, 3])
5454
test[tuple(m >> MonadicList.lift(double)) == (2, 4, 6)]
5555

56-
m = MonadicList(1, 2, 3)
56+
m = MonadicList([1, 2, 3])
5757
test_raises[TypeError, m.join()] # join() flattens a nested list, which m isn't
5858

5959
# Usage example for `guard`
60-
m = MonadicList(1, 2, 3)
60+
m = MonadicList([1, 2, 3])
6161
test[tuple(m >> (lambda x: MonadicList.guard(x % 2 == 1)
62-
.then(MonadicList(x)))) == (1, 3)]
62+
.then(MonadicList((x,))))) == (1, 3)]
6363

6464
with testset("basic usage"):
6565
test[forall(choice(x=range(5)),
@@ -114,7 +114,7 @@ def runtests():
114114
with testset("error cases"):
115115
test_raises[ValueError, choice(a=1, b=2)] # choice() takes only one binding
116116

117-
# To trigger this corner case, we must manually create an `Choice`
117+
# To trigger this corner case, we must manually create a `Choice`
118118
# that has an invalid name - in normal use, `choice()` protects against
119119
# that by its syntax, since the name of a kwarg must be a valid identifier.
120120
invalid_name = "∀δ>0∃ε>0:f(x+δ)-f(x)<ε"

0 commit comments

Comments
 (0)