-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathllist.py
More file actions
393 lines (342 loc) · 14.4 KB
/
llist.py
File metadata and controls
393 lines (342 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# -*- coding: utf-8 -*-
"""Cons and friends.
Hashable, pickleable, hooks into the built-in reversed(), can print like in Lisps.
"""
from abc import ABCMeta, abstractmethod
from collections.abc import Iterable, Iterator
from itertools import zip_longest
from .fun import composer1i
from .fold import foldr, foldl
from .it import rev
from .singleton import Singleton
# from .symbol import gensym
# explicit list better for tooling support
_exports = ["cons", "nil",
"LinkedListIterator", "LinkedListOrCellIterator", "TailIterator",
"BinaryTreeIterator", "ConsIterator",
"car", "cdr",
"caar", "cadr", "cdar", "cddr",
"caaar", "caadr", "cadar", "caddr", "cdaar", "cdadr", "cddar", "cdddr",
"caaaar", "caaadr", "caadar", "caaddr", "cadaar", "cadadr", "caddar", "cadddr",
"cdaaar", "cdaadr", "cdadar", "cdaddr", "cddaar", "cddadr", "cdddar", "cddddr",
"ll", "llist", "lreverse", "lappend", "lzip"]
#from itertools import product, repeat
#_ads = lambda n: product(*repeat("ad", n))
#_c2r = ["c{}{}r".format(*x) for x in _ads(2)]
#_c3r = ["c{}{}{}r".format(*x) for x in _ads(3)]
#_c4r = ["c{}{}{}{}r".format(*x) for x in _ads(4)]
#_exports.extend(_c2r)
#_exports.extend(_c3r)
#_exports.extend(_c4r)
__all__ = _exports
class Nil(Singleton):
"""The empty linked list. Singleton."""
# support the iterator protocol so we can say tuple(nil) --> ()
def __iter__(self):
return self
def __next__(self):
raise StopIteration()
def __repr__(self):
return "nil"
nil = Nil()
class ConsIterator(metaclass=ABCMeta):
"""Abstract base class for iterators operating on cons cells.
Can be used to define your own walking strategies for custom structures
built out of cons cells.
``startcell`` is the cons cell to start in (will be checked it is a cons),
and ``walker`` is a generator function (i.e. not started yet) that yields
the data in the desired order.
Basically all a derived class needs to do is define a walker and then call
``super().__init__(startcell, walker)``.
For usage examples see the predefined iterators in ``unpythonic.llist``.
"""
@abstractmethod
def __init__(self, startcell, walker):
if not isinstance(startcell, cons):
raise TypeError(f"Expected a cons, got {type(startcell)} with value {startcell}")
self.walker = iter(walker(startcell)) # iter() needed to support gtrampolined generators
def __iter__(self):
return self
def __next__(self):
return next(self.walker)
Iterable.register(ConsIterator)
Iterator.register(ConsIterator)
class LinkedListIterator(ConsIterator):
"""Iterator for linked lists built from cons cells."""
def __init__(self, head, _fullerror=True):
def walker(head):
cell = head
while cell is not nil:
yield cell.car
if isinstance(cell.cdr, cons) or cell.cdr is nil:
cell = cell.cdr
else:
if _fullerror:
raise TypeError(f"Not a linked list: {head}")
else: # avoid infinite loop in cons.__repr__
raise TypeError("Not a linked list")
super().__init__(head, walker)
class LinkedListReverseIterator(LinkedListIterator):
"""Iterator for walking a linked list backwards.
Computes the reversed list at init time, so it can then be walked forward.
Cost O(n).
"""
def __init__(self, head, _fullerror=True):
self._data = lreverse(head)
super().__init__(self._data, _fullerror)
class LinkedListOrCellIterator(ConsIterator):
"""Like LinkedListIterator, but allow also a single cons cell.
Default iteration strategy. Useful for sequence unpacking of cons and ll.
"""
def __init__(self, head, _fullerror=True):
def walker(head):
cell = head
while cell is not nil:
yield cell.car
if isinstance(cell.cdr, cons) or cell.cdr is nil:
cell = cell.cdr
elif cell is head:
yield cell.cdr
break
else:
if _fullerror:
raise TypeError(f"Not a linked list or a single cons cell: {head}")
else: # avoid infinite loop in cons.__repr__ (it uses LinkedListIterator, though) # pragma: no cover
raise TypeError("Not a linked list or a single cons cell")
super().__init__(head, walker)
class TailIterator(ConsIterator): # for member()
"""Like LinkedListIterator, but yield successive tails (cdr, cddr, ...).
Example::
TailIterator(ll(1, 2, 3)) --> ll(1, 2, 3), ll(2, 3), ll(3)
"""
def __init__(self, head):
def walker(head):
cell = head
while cell is not nil:
yield cell # tail of list from this cell on
if isinstance(cell.cdr, cons) or cell.cdr is nil:
cell = cell.cdr
else:
raise TypeError(f"Not a linked list: {head}")
super().__init__(head, walker)
class BinaryTreeIterator(ConsIterator):
"""Iterator for binary trees built from cons cells."""
def __init__(self, root):
# def walker(cell): # FP, call stack overflow for deep trees
# for x in (cell.car, cell.cdr):
# if isinstance(x, cons):
# yield from walker(x)
# else:
# yield x
def walker(cell): # imperative, no call stack overflow (we keep our own data stack instead)
stack = [cell]
while stack:
cell = stack.pop()
a, d = cell.car, cell.cdr
ac, dc = isinstance(a, cons), isinstance(d, cons)
if not ac:
yield a
if not dc:
yield d
if dc:
stack.append(d)
if ac:
stack.append(a) # LIFO
super().__init__(root, walker)
class JackOfAllTradesIterator(ConsIterator):
"""Iterator that supports both binary trees and linked lists.
**CAUTION**: *jack of all trades*, because:
- To handle trees correctly, this iterator descends into any cons cell
contained in the input, **also in the car half**. This implies that
**nested linked lists will be flattened**.
- To handle list termination correctly, whenever the **cdr half**
contains a ``nil``, it will be omitted from the output. This implies
that for a tree which contains ``nil`` entries, **some** of those
``nil`` may be missing from the output.
If you want the ace for a particular trade, use the specific iterator for
the specific kind of cons structure you have.
"""
def __init__(self, root):
# @gtrampolined
# def walker(cell): # FP, tail-recursive in the cdr half only
# if isinstance(cell.car, cons):
# yield from walker(cell.car)
# else:
# yield cell.car
# if isinstance(cell.cdr, cons):
# return walker(cell.cdr) # signal gtrampolined to tail-chain
# elif cell.cdr is not nil:
# yield cell.cdr
def walker(cell):
stack = [cell]
while stack:
cell = stack.pop()
a, d = cell.car, cell.cdr
ac, dc = isinstance(a, cons), isinstance(d, cons)
if not ac:
yield a
if not dc and d is not nil:
yield d
if dc:
stack.append(d)
if ac:
stack.append(a) # LIFO
super().__init__(root, walker)
class cons:
"""Cons cell a.k.a. pair. Immutable, like in Racket.
Iterable. Default is to iterate as a linked list.
"""
def __init__(self, v1, v2):
self.car = v1
self.cdr = v2
self._immutable = True
def __setattr__(self, k, v):
if hasattr(self, "_immutable"):
raise TypeError("'cons' object does not support item assignment")
super().__setattr__(k, v)
def __iter__(self):
"""Return iterator with default iteration scheme: single cell or list."""
return LinkedListOrCellIterator(self)
def __reversed__(self):
"""For lists. Caution: O(n), works by building a reversed list."""
return LinkedListReverseIterator(self)
def __repr__(self):
"""Representation in pythonic notation.
Suitable for ``eval`` if all elements are."""
try: # duck test linked list (true list only, no single-cell pair)
# listcomp, not genexpr, since we want to trigger any exceptions **now**.
result_list = [repr(x) for x in LinkedListIterator(self, _fullerror=False)]
result_str = ", ".join(result_list)
return f"ll({result_str})"
except TypeError:
result_list = (repr(self.car), repr(self.cdr))
result_str = ", ".join(result_list)
return f"cons({result_str})"
def lispyrepr(self): # TODO: maybe rename or alias this to `__str__`?
"""Representation in Lisp-like dot notation."""
try:
result_list = [repr(x) for x in LinkedListIterator(self, _fullerror=False)]
except TypeError:
r = lambda obj: obj.lispyrepr() if hasattr(obj, "lispyrepr") else repr(obj)
result_list = (r(self.car), ".", r(self.cdr))
result_str = " ".join(result_list)
return f"({result_str})"
def __eq__(self, other):
if other is self:
return True
if isinstance(other, cons):
try: # duck test linked lists
ia, ib = (LinkedListIterator(x) for x in (self, other))
fill = object() # gensym("fill"), but object() is much faster, and we don't need a label, or pickle support.
for a, b in zip_longest(ia, ib, fillvalue=fill):
if a != b:
return False
return True
except TypeError:
return self.car == other.car and self.cdr == other.cdr
return False
def __hash__(self):
try: # duck test linked list
tpl = tuple(LinkedListIterator(self))
except TypeError:
tpl = (self.car, self.cdr)
return hash(tpl)
def _car(x):
return _typecheck(x).car
def _cdr(x):
return _typecheck(x).cdr
def _typecheck(x):
if not isinstance(x, cons):
raise TypeError(f"Expected a cons, got {type(x)} with value {x}")
return x
def _build_accessor(name):
spec = name[1:-1]
f = {'a': _car, 'd': _cdr}
return composer1i(f[char] for char in spec)
def car(x):
"""Return the first half of a cons cell."""
return _car(x)
def cdr(x):
"""Return the second half of a cons cell."""
return _cdr(x)
caar = _build_accessor("caar")
cadr = _build_accessor("cadr")
cdar = _build_accessor("cdar")
cddr = _build_accessor("cddr")
caaar = _build_accessor("caaar")
caadr = _build_accessor("caadr")
cadar = _build_accessor("cadar") # look, it's Darth Cadar!
caddr = _build_accessor("caddr")
cdaar = _build_accessor("cdaar")
cdadr = _build_accessor("cdadr")
cddar = _build_accessor("cddar")
cdddr = _build_accessor("cdddr")
caaaar = _build_accessor("caaaar")
caaadr = _build_accessor("caaadr")
caadar = _build_accessor("caadar")
caaddr = _build_accessor("caaddr")
cadaar = _build_accessor("cadaar")
cadadr = _build_accessor("cadadr")
caddar = _build_accessor("caddar")
cadddr = _build_accessor("cadddr")
cdaaar = _build_accessor("cdaaar")
cdaadr = _build_accessor("cdaadr")
cdadar = _build_accessor("cdadar")
cdaddr = _build_accessor("cdaddr")
cddaar = _build_accessor("cddaar")
cddadr = _build_accessor("cddadr")
cdddar = _build_accessor("cdddar")
cddddr = _build_accessor("cddddr")
def ll(*elts):
"""Make a linked list with the given elements.
``ll(...)`` plays the same role as ``[...]`` or ``(...)`` for lists or tuples,
respectively, but for linked lists. See also ``llist``.
**NOTE**: The returned data type is ``cons``, there is no ``ll`` type.
A linked list is just one kind of structure that can be built out of cons cells.
Equivalent to ``(list ...)`` in Lisps. Since in Python the name ``list``
refers to the builtin dynamic array type, we use the name ``ll``.
"""
return llist(elts)
def llist(iterable):
"""Make a linked list from iterable.
``llist(...)`` plays the same role as ``list(...)`` or ``tuple(...)`` for
lists or tuples, respectively, but for linked lists. See also ``ll``.
**NOTE**: The returned data type is ``cons``, there is no ``llist`` type.
A linked list is just one kind of structure that can be built out of cons cells.
**Efficiency**:
Because cons appends to the front, this is efficient for:
- ``reversed(some_linked_list)``, by just returning the already computed
reversed list that is internally stored by the reverse-iterator.
- Sequences, since they can be walked backwards; a linear walk is enough.
For a general iterable input, this costs a linear walk (forwards),
plus an ``lreverse``.
"""
if isinstance(iterable, LinkedListReverseIterator):
# avoid two extra reverses by reusing the internal data.
return iterable._data
return lreverse(rev(iterable))
def lreverse(iterable):
"""Reverse an iterable, loading the result into a linked list.
If you have a linked list and want an iterator instead, use ``reversed(l)``.
The computational cost is the same in both cases, O(n).
"""
return foldl(cons, nil, iterable)
def lappend(*ls):
"""Append the given linked lists left-to-right."""
def lappend_two(l1, l2):
return foldr(cons, l2, l1)
return foldr(lappend_two, nil, ls)
def member(x, l):
"""Walk linked list l and check if item x is in it.
Returns:
The matching cons cell (tail of l) if x was found; False if not.
"""
for t in TailIterator(l):
if t.car == x:
return t
return False
def lzip(*ls):
"""Zip linked lists, producing a linked list of linked lists.
Built-in zip() works too, but produces tuples.
"""
return llist(map(ll, *ls))