Skip to content

Commit c4a34da

Browse files
Remove cons_merge generic function and implement cons types
This also fixes some broken cons semantics (e.g. `cons("a", cons("b", "c"))` is *not* `cons(["a", "b"], "c")`) and introduces a `ConsError` exception.
1 parent 240569e commit c4a34da

File tree

4 files changed

+251
-168
lines changed

4 files changed

+251
-168
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ The `cons` package follows Scheme-like semantics for empty sequences:
3535
>>> car([])
3636
Traceback (most recent call last):
3737
File "<stdin>", line 1, in <module>
38-
TypeError: Not a cons pair
38+
ConsError: Not a cons pair
3939

4040
>>> cdr([])
4141
Traceback (most recent call last):
4242
File "<stdin>", line 1, in <module>
43-
TypeError: Not a cons pair
43+
ConsError: Not a cons pair
4444

4545
```
4646

cons/core.py

Lines changed: 127 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,69 @@
11
from functools import reduce
2-
from itertools import chain
32
from operator import length_hint
43
from collections import OrderedDict
5-
from collections.abc import Iterator
4+
from itertools import chain, islice
5+
from collections.abc import Iterator, Sequence, ItemsView
66

77
from multipledispatch import dispatch
88

99
from toolz import last, first
10-
from toolz.itertoolz import rest
10+
11+
# We can't use this because `islice` drops __length_hint__ info.
12+
# from toolz.itertoolz import rest
13+
14+
15+
def rest(seq):
16+
if isinstance(seq, Iterator) and length_hint(seq, 2) <= 1:
17+
return iter([])
18+
else:
19+
return islice(seq, 1, None)
20+
21+
22+
class ConsError(ValueError):
23+
pass
1124

1225

1326
class ConsType(type):
1427
def __instancecheck__(self, o):
15-
return is_cons(o)
28+
return (
29+
issubclass(type(o), (ConsPair, MaybeCons))
30+
and length_hint(o, 0) > 0
31+
)
32+
33+
34+
class ConsNullType(type):
35+
def __instancecheck__(self, o):
36+
if o is None:
37+
return True
38+
elif issubclass(type(o), MaybeCons):
39+
lhint = length_hint(o, -1)
40+
if lhint == 0:
41+
return True
42+
elif lhint > 0:
43+
return False
44+
else:
45+
return None
46+
else:
47+
return False
48+
49+
50+
class ConsNull(metaclass=ConsNullType):
51+
"""A class used to indicate a Lisp/cons-like null.
52+
53+
A "Lisp-like" null object is one that can be used as a `cdr` to produce a
54+
non-`ConsPair` collection (e.g. `None`, `[]`, `()`, `OrderedDict`, etc.)
55+
56+
It's important that this function be used when considering an arbitrary
57+
object as the terminating `cdr` for a given collection (e.g. when unifying
58+
`cons` objects); otherwise, fixed choices for the terminating `cdr`, such
59+
as `None` or `[]`, will severely limit the applicability of the
60+
decomposition.
61+
62+
Also, for relevant collections with no concrete length information, `None`
63+
is returned, and it signifies the uncertainty of the negative assertion.
64+
"""
65+
66+
pass
1667

1768

1869
class ConsPair(metaclass=ConsType):
@@ -41,18 +92,40 @@ def __new__(cls, *parts):
4192
elif len(parts) == 2:
4293
car_part = first(parts)
4394
cdr_part = last(parts)
44-
try:
45-
res = cons_merge(car_part, cdr_part)
46-
except NotImplementedError:
95+
96+
if cdr_part is None:
97+
cdr_part = []
98+
99+
if isinstance(
100+
cdr_part, (ConsNull, ConsPair, Iterator)
101+
) and not issubclass(type(cdr_part), ConsPair):
102+
res = cls.cons_merge(car_part, cdr_part)
103+
else:
47104
instance = super(ConsPair, cls).__new__(cls)
48105
instance.car = car_part
49106
instance.cdr = cdr_part
50107
res = instance
108+
51109
else:
52110
raise ValueError("Number of arguments must be greater than 2.")
53111

54112
return res
55113

114+
@classmethod
115+
def cons_merge(cls, car_part, cdr_part):
116+
117+
if isinstance(cdr_part, OrderedDict):
118+
cdr_part = cdr_part.items()
119+
120+
res = chain((car_part,), cdr_part)
121+
122+
if isinstance(cdr_part, ItemsView):
123+
res = OrderedDict(res)
124+
elif not isinstance(cdr_part, Iterator):
125+
res = type(cdr_part)(res)
126+
127+
return res
128+
56129
def __hash__(self):
57130
return hash([self.car, self.cdr])
58131

@@ -75,74 +148,78 @@ def __str__(self):
75148
cons = ConsPair
76149

77150

78-
@dispatch(object, type(None))
79-
def cons_merge(car_part, cdr_part):
80-
"""Merge a generic car and cdr.
81-
82-
This is the base/`nil` case with `cdr` `None`; it produces a standard list.
83-
"""
84-
return [car_part]
85-
86-
87-
@cons_merge.register(object, ConsPair)
88-
def cons_merge_ConsPair(car_part, cdr_part):
89-
"""Merge a car and a `ConsPair` cdr."""
90-
return ConsPair([car_part, car(cdr_part)], cdr(cdr_part))
151+
class MaybeConsType(type):
152+
_ignored_types = (object, type(None), ConsPair, str)
91153

154+
def __subclasscheck__(self, o):
155+
return o not in self._ignored_types and any(
156+
issubclass(o, d)
157+
for d in cdr.funcs.keys()
158+
if d not in self._ignored_types
159+
)
92160

93-
@cons_merge.register(object, Iterator)
94-
def cons_merge_Iterator(car_part, cdr_part):
95-
"""Merge a car and an `Iterator` cdr."""
96-
return chain([car_part], cdr_part)
97161

162+
class MaybeCons(metaclass=MaybeConsType):
163+
"""A class used to dynamically determine potential cons types from
164+
non-ConsPairs.
98165
99-
@cons_merge.register(object, (list, tuple))
100-
def cons_merge_list_tuple(car_part, cdr_part):
101-
"""Merge a car with a list or tuple cdr."""
102-
return type(cdr_part)([car_part]) + cdr_part
166+
For example,
103167
168+
issubclass(tuple, MaybeCons) is True
169+
issubclass(ConsPair, MaybeCons) is False
104170
105-
@cons_merge.register((list, tuple), OrderedDict)
106-
def cons_merge_OrderedDict(car_part, cdr_part):
107-
"""Merge a list/tuple car with a dict cdr."""
108-
if hasattr(cdr_part, "move_to_end"):
109-
cdr_part.update([car_part])
110-
cdr_part.move_to_end(first(car_part), last=False)
111-
else:
112-
cdr_part = OrderedDict([car_part] + list(cdr_part.items()))
171+
The potential cons types are drawn from the implemented `cdr` dispatch
172+
functions.
173+
"""
113174

114-
return cdr_part
175+
pass
115176

116177

117-
@dispatch(type(None))
178+
@dispatch((type(None), str))
118179
def car(z):
119-
raise TypeError("Not a cons pair")
180+
raise ConsError("Not a cons pair")
120181

121182

122183
@car.register(ConsPair)
123184
def car_ConsPair(z):
124185
return z.car
125186

126187

127-
@car.register((list, tuple, Iterator))
188+
@car.register(Iterator)
128189
def car_Iterator(z):
190+
"""Return the first element in the given iterator.
191+
192+
Warning: `car` necessarily draws from the iterator, and we can't do
193+
much--within this function--to make copies (e.g. with `tee`) that will
194+
appropriately replace the original iterator.
195+
Callers must handle this themselves.
196+
"""
129197
try:
198+
# z, _ = tee(z)
130199
return first(z)
131200
except StopIteration:
132-
raise TypeError("Not a cons pair")
201+
raise ConsError("Not a cons pair")
202+
203+
204+
@car.register(Sequence)
205+
def car_Sequence(z):
206+
try:
207+
return first(z)
208+
except StopIteration:
209+
raise ConsError("Not a cons pair")
133210

134211

135212
@car.register(OrderedDict)
136213
def car_OrderedDict(z):
137214
if len(z) == 0:
138-
raise TypeError("Not a cons pair")
215+
raise ConsError("Not a cons pair")
139216

140217
return first(z.items())
141218

142219

143-
@dispatch(type(None))
220+
@dispatch((type(None), str))
144221
def cdr(z):
145-
raise TypeError("Not a cons pair")
222+
raise ConsError("Not a cons pair")
146223

147224

148225
@cdr.register(ConsPair)
@@ -153,69 +230,19 @@ def cdr_ConsPair(z):
153230
@cdr.register(Iterator)
154231
def cdr_Iterator(z):
155232
if length_hint(z, 1) == 0:
156-
raise TypeError("Not a cons pair")
233+
raise ConsError("Not a cons pair")
157234
return rest(z)
158235

159236

160-
@cdr.register((list, tuple))
161-
def cdr_list_tuple(z):
237+
@cdr.register(Sequence)
238+
def cdr_Sequence(z):
162239
if len(z) == 0:
163-
raise TypeError("Not a cons pair")
164-
return type(z)(list(rest(z)))
240+
raise ConsError("Not a cons pair")
241+
return type(z)(rest(z))
165242

166243

167244
@cdr.register(OrderedDict)
168245
def cdr_OrderedDict(z):
169246
if len(z) == 0:
170-
raise TypeError("Not a cons pair")
247+
raise ConsError("Not a cons pair")
171248
return cdr(list(z.items()))
172-
173-
174-
def is_cons(a):
175-
"""Determine if an object is the result of a `cons`.
176-
177-
This is automatically determined by the accepted `cdr` types for each
178-
`cons_merge` implementation, since any such implementation implies that
179-
`cons` can construct that type.
180-
"""
181-
return issubclass(type(a), ConsPair) or (
182-
any(
183-
isinstance(a, d)
184-
for _, d in cons_merge.funcs.keys()
185-
if d not in (object, ConsPair)
186-
)
187-
and length_hint(a, 0) > 0
188-
)
189-
190-
191-
def is_null(a):
192-
"""Check if an object is a "Lisp-like" null.
193-
194-
A "Lisp-like" null object is one that can be used as a `cdr` to produce a
195-
non-`ConsPair` collection (e.g. `None`, `[]`, `()`, `OrderedDict`, etc.)
196-
197-
It's important that this function be used when considering an arbitrary
198-
object as the terminating `cdr` for a given collection (e.g. when unifying
199-
`cons` objects); otherwise, fixed choices for the terminating `cdr`, such
200-
as `None` or `[]`, will severely limit the applicability of the
201-
decomposition.
202-
203-
Also, for relevant collections with no concrete length information, `None`
204-
is returned, and it signifies the uncertainty of the negative assertion.
205-
"""
206-
if a is None:
207-
return True
208-
elif any(
209-
isinstance(a, d)
210-
for d, in cdr.funcs.keys()
211-
if not issubclass(d, (ConsPair, type(None)))
212-
):
213-
lhint = length_hint(a, -1)
214-
if lhint == 0:
215-
return True
216-
elif lhint > 0:
217-
return False
218-
else:
219-
return None
220-
else:
221-
return False

cons/unify.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,35 @@
1-
from collections import OrderedDict
2-
from collections.abc import Iterator
1+
from itertools import tee
2+
from collections import OrderedDict, Iterator
33

44
from unification.core import unify, _unify, reify, _reify
55

6-
from .core import is_cons, car, cdr, ConsPair, cons
6+
from .core import car, cdr, ConsPair, cons, MaybeCons, ConsError
77

88

9-
# Unfortunately, `multipledispatch` doesn't use `isinstance` on the arguments,
10-
# so it won't use our fancy setup for `isinstance(x, ConsPair)` and we have to
11-
# specify--and check--each `cons`-amenable type explicitly.
129
def _cons_unify(lcons, rcons, s):
1310

14-
if not is_cons(lcons) or not is_cons(rcons):
15-
# One of the arguments is necessarily a `ConsPair` object,
16-
# but the other could be an empty iterable, which isn't a
17-
# `cons`-derivable object.
18-
return False
11+
lcons_ = lcons
12+
rcons_ = rcons
13+
14+
if isinstance(lcons, Iterator):
15+
lcons, lcons_ = tee(lcons)
16+
17+
if isinstance(rcons, Iterator):
18+
rcons, rcons_ = tee(rcons)
19+
20+
try:
21+
s = unify(car(lcons), car(rcons), s)
22+
23+
if s is not False:
24+
return unify(cdr(lcons_), cdr(rcons_), s)
25+
except ConsError:
26+
pass
1927

20-
s = unify(car(lcons), car(rcons), s)
21-
if s is not False:
22-
return unify(cdr(lcons), cdr(rcons), s)
2328
return False
2429

2530

26-
_unify.add(
27-
(ConsPair, (ConsPair, list, tuple, Iterator, OrderedDict), dict),
28-
_cons_unify,
29-
)
30-
_unify.add(((list, tuple, Iterator, OrderedDict), ConsPair, dict), _cons_unify)
31+
_unify.add((ConsPair, (ConsPair, MaybeCons), dict), _cons_unify)
32+
_unify.add((MaybeCons, ConsPair, dict), _cons_unify)
3133

3234

3335
@_reify.register(OrderedDict, dict)

0 commit comments

Comments
 (0)