# -*- 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 Callable, Generator, Iterable, Iterator from dataclasses import FrozenInstanceError from itertools import zip_longest from typing import Any from .fun import composer1i from .fold import foldr, foldl from .it import rev from .singleton import Singleton from .symbol import gensym _fill = gensym("fill") # explicit list better for tooling support _exports = ["FrozenAttributeError", "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 FrozenAttributeError(TypeError, FrozenInstanceError): """Raised on a write/delete attempt against a frozen-instance type. Multiply-inherits from `TypeError` (the legacy unpythonic <= 2.x base) and `dataclasses.FrozenInstanceError` (the standard-library convention since Python 3.7, itself a subclass of `AttributeError`). Either `except` clause catches it. Compatibility shim: lets unpythonic align with the stdlib idiom without breaking user code that catches `TypeError`. The `TypeError` base will be dropped in 3.0.0 (see issue #35), at which point this class becomes a plain `FrozenInstanceError` and likely goes away. """ class Nil(Singleton): """The empty linked list. Singleton.""" # support the iterator protocol so we can say tuple(nil) --> () def __iter__(self) -> "Nil": return self def __next__(self) -> Any: raise StopIteration() def __repr__(self) -> str: 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: "cons", walker: Callable[["cons"], Generator]) -> None: 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) -> "ConsIterator": return self def __next__(self) -> Any: 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: "cons", _fullerror: bool = True) -> None: 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: "cons", _fullerror: bool = True) -> None: 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: "cons", _fullerror: bool = True) -> None: 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: "cons") -> None: 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: "cons") -> None: # 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: "cons") -> None: # @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: Any, v2: Any) -> None: # Bypass our locked-down `__setattr__` to populate the read-only fields once. object.__setattr__(self, "car", v1) object.__setattr__(self, "cdr", v2) def __setattr__(self, k: str, v: Any) -> None: raise FrozenAttributeError(f"'cons' object does not support attribute assignment; tried to set {k!r}") def __delattr__(self, k: str) -> None: raise FrozenAttributeError(f"'cons' object does not support attribute deletion; tried to delete {k!r}") def __iter__(self) -> LinkedListOrCellIterator: """Return iterator with default iteration scheme: single cell or list.""" return LinkedListOrCellIterator(self) def __reversed__(self) -> LinkedListReverseIterator: """For lists. Caution: O(n), works by building a reversed list.""" return LinkedListReverseIterator(self) def __repr__(self) -> str: """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) -> str: # 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: Any) -> bool: if other is self: return True if isinstance(other, cons): try: # duck test linked lists ia, ib = (LinkedListIterator(x) for x in (self, other)) return all(a == b for a, b in zip_longest(ia, ib, fillvalue=_fill)) except TypeError: return self.car == other.car and self.cdr == other.cdr return False def __hash__(self) -> int: try: # duck test linked list tpl = tuple(LinkedListIterator(self)) except TypeError: tpl = (self.car, self.cdr) return hash(tpl) def _car(x: cons) -> Any: return _typecheck(x).car def _cdr(x: cons) -> Any: return _typecheck(x).cdr def _typecheck(x: Any) -> cons: if not isinstance(x, cons): raise TypeError(f"Expected a cons, got {type(x)} with value {x}") return x def _build_accessor(name: str) -> Callable[[cons], Any]: spec = name[1:-1] f = {'a': _car, 'd': _cdr} return composer1i(f[char] for char in spec) def car(x: cons) -> Any: """Return the first half of a cons cell.""" return _car(x) def cdr(x: cons) -> Any: """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: Any) -> "cons | Nil": """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: Iterable) -> "cons | Nil": """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: Iterable) -> "cons | Nil": """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: "cons | Nil") -> "cons | Nil": """Append the given linked lists left-to-right.""" def lappend_two(l1: "cons | Nil", l2: "cons | Nil") -> "cons | Nil": return foldr(cons, l2, l1) return foldr(lappend_two, nil, ls) def member(x: Any, l: cons) -> "cons | bool": # noqa: E741 -- standard Lisp name for a linked list """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: "cons | Nil") -> "cons | Nil": """Zip linked lists, producing a linked list of linked lists. Built-in zip() works too, but produces tuples. """ return llist(map(ll, *ls))