Skip to content

Commit d88c8e2

Browse files
committed
enh: JackOfAllTradesIterator and BinaryTreeIterator now understand arbitrarily deep cons structures
1 parent 6dd4777 commit d88c8e2

File tree

4 files changed

+44
-23
lines changed

4 files changed

+44
-23
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -480,13 +480,15 @@ There is no ``copy`` method or ``lcopy`` function, because cons cells are immuta
480480

481481
In general, copying cons structures can be error-prone. Given just a starting cell it is impossible to tell if a given instance of a cons structure represents a linked list, or something more general (such as a binary tree) that just happens to locally look like one, along the path that would be traversed if it was indeed a linked list.
482482

483-
The linked list iteration strategy (which supports arbitrarily long lists) does not recurse in the ``car`` half, which could lead to incomplete copying. The tree strategy that recurses on both halves, on the other hand, is not safe to use on linked lists, because if the list is long, it will cause a stack overflow (due to lack of TCO in Python).
483+
The linked list iteration strategy does not recurse in the ``car`` half, which could lead to incomplete copying. The tree strategy that recurses on both halves, on the other hand, will flatten nested linked lists and produce also the final ``nil``.
484484

485-
*Added in v0.13.0.* We provide a ``JackOfAllTradesIterator`` that uses unpythonic's TCO and understands both trees and linked lists, but it has to make some compromises to be able to do this: nested lists will be flattened, and in a tree any ``nil`` in a ``cdr`` position will be omitted from the output.
485+
*Added in v0.13.0.* We provide a ``JackOfAllTradesIterator`` as a compromise that understands both trees and linked lists. Nested lists will be flattened, and in a tree any ``nil`` in a ``cdr`` position will be omitted from the output.
486+
487+
*Changed in v0.13.1.* ``BinaryTreeIterator`` and ``JackOfAllTradesIterator`` now use an explicit data stack instead of implicitly using the call stack for keeping track of the recursion. Hence now all ``cons`` iterators work for arbitrarily deep cons structures without causing Python's call stack to overflow, and without the need for TCO.
486488

487489
``cons`` has no ``collections.abc`` virtual superclasses (except the implicit ``Hashable`` since ``cons`` provides ``__hash__`` and ``__eq__``), because general cons structures do not fit into the contracts represented by membership in those classes. For example, size cannot be known without iterating, and depends on which iteration scheme is used (e.g. ``nil`` dropping, flattening); which scheme is appropriate depends on the content.
488490

489-
**Caution**: the ``nil`` singleton is freshly created in each session; newnil is not oldnil, so don't pickle a standalone ``nil``. The unpickler of ``cons`` automatically refreshes any ``nil`` instances inside a pickled cons structure, so that **cons structures** support the illusion that ``nil`` is a special value like ``None`` or ``...``. After unpickling, ``car(c) is nil`` and ``cdr(c) is nil`` still work as expected, even though ``id(nil)`` has changed.
491+
**Caution**: the ``nil`` singleton is freshly created in each session; newnil is not oldnil, so don't pickle a standalone ``nil``. The unpickler of ``cons`` automatically refreshes any ``nil`` instances inside a pickled cons structure, so that **cons structures** support the illusion that ``nil`` is a special value like ``None`` or ``...``. After unpickling, ``car(c) is nil`` and ``cdr(c) is nil`` still work as expected, even though ``id(nil)`` has changed between sessions.
490492

491493

492494
### ``box``: a mutable single-item container
@@ -1416,7 +1418,7 @@ For convenience, we introduce some special cases:
14161418
14171419
- The ``box`` container from ``unpythonic.collections``; although mutable, its update is not conveniently expressible by the ``collections.abc`` APIs.
14181420
1419-
- The ``cons`` container from ``unpythonic.llist`` (including the ``ll``, ``llist`` linked lists). This is treated with the general tree strategy, so for long linked lists this will crash when the maximum call stack depth runs out.
1421+
- The ``cons`` container from ``unpythonic.llist`` (including the ``ll``, ``llist`` linked lists). This is treated with the general tree strategy, so nested linked lists will be flattened, and the final ``nil`` is also processed.
14201422
14211423
Note that since ``cons`` is immutable, anyway, if you know you have a long linked list where you need to update the values, just iterate over it and produce a new copy - that will work as intended.
14221424

unpythonic/collections.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ def mogrify(func, container):
5959
6060
- The ``cons`` container from ``unpythonic.llist`` (including the
6161
``llist`` linked lists). This is treated with the general tree
62-
strategy, so for long linked lists this will crash.
62+
strategy, so nested linked lists will be flattened, and the
63+
final ``nil`` is also processed.
6364
6465
Any value that does not match any of these is treated as an atom.
6566

unpythonic/llist.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from .fun import composer1i
1111
from .fold import foldr, foldl
1212
from .it import rev
13-
from .gtco import gtrampolined
1413

1514
# explicit list better for tooling support
1615
_exports = ["cons", "nil",
@@ -142,19 +141,27 @@ def walker(head):
142141
class BinaryTreeIterator(ConsIterator):
143142
"""Iterator for binary trees built from cons cells."""
144143
def __init__(self, root):
145-
def walker(cell):
146-
for x in (cell.car, cell.cdr):
147-
if isinstance(x, cons):
148-
yield from walker(x)
149-
else:
150-
yield x
144+
# def walker(cell): # FP, call stack overflow for deep trees
145+
# for x in (cell.car, cell.cdr):
146+
# if isinstance(x, cons):
147+
# yield from walker(x)
148+
# else:
149+
# yield x
150+
def walker(cell): # imperative, no call stack overflow (we keep our own data stack instead)
151+
stack = [cell]
152+
while stack:
153+
cell = stack.pop()
154+
a, d = cell.car, cell.cdr
155+
ac, dc = isinstance(a, cons), isinstance(d, cons)
156+
if not ac: yield a
157+
if not dc: yield d
158+
if dc: stack.append(d)
159+
if ac: stack.append(a) # LIFO
151160
super().__init__(root, walker)
152161

153162
class JackOfAllTradesIterator(ConsIterator):
154163
"""Iterator that supports both binary trees and linked lists.
155164
156-
An optimized tail call is used to descend into the cdr half.
157-
158165
**CAUTION**: *jack of all trades*, because:
159166
160167
- To handle trees correctly, this iterator descends into any cons cell
@@ -170,16 +177,26 @@ class JackOfAllTradesIterator(ConsIterator):
170177
the specific kind of cons structure you have.
171178
"""
172179
def __init__(self, root):
173-
@gtrampolined
180+
# @gtrampolined
181+
# def walker(cell): # FP, tail-recursive in the cdr half only
182+
# if isinstance(cell.car, cons):
183+
# yield from walker(cell.car)
184+
# else:
185+
# yield cell.car
186+
# if isinstance(cell.cdr, cons):
187+
# return walker(cell.cdr) # signal gtrampolined to tail-chain
188+
# elif cell.cdr is not nil:
189+
# yield cell.cdr
174190
def walker(cell):
175-
if isinstance(cell.car, cons):
176-
yield from walker(cell.car)
177-
else:
178-
yield cell.car
179-
if isinstance(cell.cdr, cons):
180-
return walker(cell.cdr) # signal gtrampolined to tail-chain
181-
elif cell.cdr is not nil:
182-
yield cell.cdr
191+
stack = [cell]
192+
while stack:
193+
cell = stack.pop()
194+
a, d = cell.car, cell.cdr
195+
ac, dc = isinstance(a, cons), isinstance(d, cons)
196+
if not ac: yield a
197+
if not dc and d is not nil: yield d
198+
if dc: stack.append(d)
199+
if ac: stack.append(a) # LIFO
183200
super().__init__(root, walker)
184201

185202
class cons:

unpythonic/test/test_llist.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ def test():
7979
assert tuple(JackOfAllTradesIterator(t2)) == (nil, 1, nil, 2) # but doesn't skip nil in the car slot
8080

8181
assert tuple(JackOfAllTradesIterator(llist(range(10000)))) == tuple(range(10000)) # no crash
82+
assert tuple(BinaryTreeIterator(llist(range(10000)))) == tuple(range(10000)) + (nil,) # no crash
8283

8384
# repr
8485
assert repr(cons(1, 2)) == "cons(1, 2)"

0 commit comments

Comments
 (0)