Skip to content

Commit 844ba29

Browse files
authored
Equality defined between builtin collections respect partitions (#558)
This commit corrects the equality operators between builtin collection types to respect equality partitions. Equality partitions are segments of different collections within which collections can be considered equal and between which collections cannot be considered equal. The three equality partitions are: sequential types (vectors, seqs), maps, and sets. Several other small changes were made in support of that goal: * `Map` types now properly support the full Python `Mapping` protocol. * `Map.__iter__` now acts like a standard Python `Mapping` type, returning an iterable of keys. Formerly, it was an iterable of `MapEntry` types, which was occasionally convenient, but definitely un-idiomatic in Python. This change doesn't really affect Basilisp code though, since Basilisp always calls `.seq()` on maps to iterate over entries. * Removed the `.iteritems()`, `.iterkeys()`, and `.itervalues()` methods which simply forwarded to the base `pyrsistent.PMap` implementation. * `Mapping.items()`, `.keys()`, and `.values()` are now supported. All cases relying on the former `__iter__` implementation have generally been updated to use `.items()` (though a few cases were updated to use `.seq()`). * All builtin `ISeq` types and `IPersistentVector` have been annotated with the marker interface `ISequential` which is attached to all sequential types in that equality partition. * `IPersistentVector` types are no longer considered `Mapping` types, though they are still considered `IAssociative`. It never really made sense to consider them `Mapping` types and it was generally inconvenient since it is not useful to consider vectors as maps in most cases. * Removed the custom `Vector.rseq()` implementation since it was undoubtedly no more efficient than Python's `reversed` using the Python `Sequence` protocol. Two small fixes unrelated to the main changes were also included because I was already in the code: * `basilisp.core/rest` now never returns `nil`. Instead, it returns the empty seq in any case it would formerly return `nil`. * I fixed a [deprecation with the PyTest plugin](pytest-dev/pytest#6680) that emitted Warnings every time any Basilisp tests ran. References: * [Clojure Collection Model](https://insideclojure.org/2016/03/16/collections/) * [Sequences](https://insideclojure.org/2015/01/02/sequences/)
1 parent cf4346e commit 844ba29

19 files changed

+234
-111
lines changed

src/basilisp/lang/compiler/generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ def __init__(
228228
self._var_indirection_override: Deque[bool] = collections.deque([])
229229

230230
if logger.isEnabledFor(logging.DEBUG): # pragma: no cover
231-
for k, v in self._opts:
231+
for k, v in self._opts.items():
232232
logger.debug("Compiler option %s = %s", k, v)
233233

234234
@property

src/basilisp/lang/interfaces.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,7 @@ def empty() -> "IPersistentCollection[T]":
153153
T_assoc = TypeVar("T_assoc", bound="IAssociative")
154154

155155

156-
class IAssociative(
157-
ILookup[K, V], Mapping[K, V], IPersistentCollection[IMapEntry[K, V]]
158-
):
156+
class IAssociative(ILookup[K, V], IPersistentCollection[IMapEntry[K, V]]):
159157
__slots__ = ()
160158

161159
@abstractmethod
@@ -193,7 +191,7 @@ class IPersistentList(ISequential, IPersistentStack[T]):
193191
T_map = TypeVar("T_map", bound="IPersistentMap")
194192

195193

196-
class IPersistentMap(ICounted, IAssociative[K, V]):
194+
class IPersistentMap(ICounted, Mapping[K, V], IAssociative[K, V]):
197195
__slots__ = ()
198196

199197
@abstractmethod
@@ -267,6 +265,23 @@ def _record_lrepr(self, kwargs: Mapping) -> str:
267265
raise NotImplementedError()
268266

269267

268+
def seq_equals(s1, s2) -> bool:
269+
"""Return True if two sequences contain the exactly the same elements in the
270+
same order. Return False if one sequence is shorter than the other."""
271+
assert isinstance(s1, (ISeq, ISequential))
272+
273+
if not isinstance(s2, (ISeq, ISequential)):
274+
return NotImplemented
275+
276+
sentinel = object()
277+
for e1, e2 in itertools.zip_longest(s1, s2, fillvalue=sentinel): # type: ignore[arg-type]
278+
if bool(e1 is sentinel) or bool(e2 is sentinel):
279+
return False
280+
if e1 != e2:
281+
return False
282+
return True
283+
284+
270285
class ISeq(ILispObject, ISeqable[T]):
271286
__slots__ = ()
272287

@@ -320,17 +335,9 @@ def _lrepr(self, **kwargs):
320335
return seq_lrepr(iter(self), "(", ")", **kwargs)
321336

322337
def __eq__(self, other):
323-
sentinel = object()
324-
try:
325-
for e1, e2 in itertools.zip_longest(self, other, fillvalue=sentinel):
326-
if bool(e1 is sentinel) or bool(e2 is sentinel):
327-
return False
328-
if e1 != e2:
329-
return False
330-
except TypeError:
331-
return False
332-
else:
338+
if self is other:
333339
return True
340+
return seq_equals(self, other)
334341

335342
def __hash__(self):
336343
return hash(tuple(self))

src/basilisp/lang/keyword.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@ def complete(
6262
results = filter(
6363
lambda kw: (kw.ns is not None and kw.ns == prefix)
6464
and kw.name.startswith(suffix),
65-
kw_cache.itervalues(),
65+
kw_cache.values(),
6666
)
6767
else:
6868
results = filter(
6969
lambda kw: kw.name.startswith(text)
7070
or (kw.ns is not None and kw.ns.startswith(text)),
71-
kw_cache.itervalues(),
71+
kw_cache.values(),
7272
)
7373

7474
return map(str, results)

src/basilisp/lang/list.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@ def __init__(self, wrapped: "PList[T]", meta=None) -> None:
2222
self._inner = wrapped
2323
self._meta = meta
2424

25-
def __eq__(self, other):
26-
return self._inner == other
27-
2825
def __getitem__(self, item):
2926
if isinstance(item, slice):
3027
return List(self._inner[item])

src/basilisp/lang/map.py

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ def __contains__(self, item):
4444
return item in self._inner
4545

4646
def __eq__(self, other):
47-
return self._inner == other
47+
if self is other:
48+
return True
49+
if not isinstance(other, Mapping):
50+
return NotImplemented
51+
if len(self._inner) != len(other):
52+
return False
53+
return dict(self._inner.iteritems()) == dict(other.items())
4854

4955
def __getitem__(self, item):
5056
return self._inner[item]
@@ -53,8 +59,7 @@ def __hash__(self):
5359
return hash(self._inner)
5460

5561
def __iter__(self):
56-
for k, v in self._inner.iteritems():
57-
yield MapEntry.of(k, v)
62+
yield from self._inner.iterkeys()
5863

5964
def __len__(self):
6065
return len(self._inner)
@@ -64,24 +69,6 @@ def _lrepr(self, **kwargs):
6469
self._inner.iteritems, start="{", end="}", meta=self._meta, **kwargs
6570
)
6671

67-
def items(self):
68-
return self._inner.items()
69-
70-
def keys(self):
71-
return self._inner.keys()
72-
73-
def values(self):
74-
return self._inner.values()
75-
76-
def iteritems(self):
77-
return self._inner.iteritems()
78-
79-
def iterkeys(self):
80-
return self._inner.iterkeys()
81-
82-
def itervalues(self):
83-
return self._inner.itervalues()
84-
8572
@property
8673
def meta(self) -> Optional[IPersistentMap]:
8774
return self._meta
@@ -140,12 +127,7 @@ def cons( # type: ignore[override]
140127
e = self._inner.evolver()
141128
try:
142129
for elem in elems:
143-
if isinstance(elem, (IPersistentMap, Mapping)) and not isinstance(
144-
elem, IPersistentVector
145-
):
146-
# Vectors are handled in the final else block, since we
147-
# do not want to treat them as Mapping types for this
148-
# particular usage.
130+
if isinstance(elem, (IPersistentMap, Mapping)):
149131
for k, v in elem.items():
150132
e.set(k, v)
151133
elif isinstance(elem, IMapEntry):
@@ -169,7 +151,7 @@ def empty() -> "Map":
169151
return m()
170152

171153
def seq(self) -> ISeq[IMapEntry[K, V]]:
172-
return sequence(self)
154+
return sequence(MapEntry.of(k, v) for k, v in self._inner.iteritems())
173155

174156

175157
def map(kvs: Mapping[K, V], meta=None) -> Map[K, V]: # pylint:disable=redefined-builtin

src/basilisp/lang/reader.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,11 +296,11 @@ def __init__( # pylint: disable=too-many-arguments
296296
process_reader_cond: bool = True,
297297
) -> None:
298298
data_readers = Maybe(data_readers).or_else_get(lmap.Map.empty())
299-
for entry in data_readers:
300-
if not isinstance(entry.key, symbol.Symbol):
299+
for reader_sym in data_readers.keys():
300+
if not isinstance(reader_sym, symbol.Symbol):
301301
raise TypeError("Expected symbol for data reader tag")
302-
if not entry.key.ns:
303-
raise ValueError(f"Non-namespaced tags are reserved by the reader")
302+
if not reader_sym.ns:
303+
raise ValueError("Non-namespaced tags are reserved by the reader")
304304

305305
self._data_readers = ReaderContext._DATA_READERS.update_with(
306306
lambda l, r: l, # Do not allow callers to overwrite existing builtin readers

src/basilisp/lang/runtime.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -639,9 +639,7 @@ def refer_all(self, other_ns: "Namespace") -> None:
639639
"""Refer all the Vars in the other namespace."""
640640
with self._lock:
641641
final_refers = self._refers
642-
for entry in other_ns.interns:
643-
s: sym.Symbol = entry.key
644-
var: Var = entry.value
642+
for s, var in other_ns.interns.items():
645643
if not var.is_private:
646644
final_refers = final_refers.assoc(s, var)
647645
self._refers = final_refers
@@ -716,7 +714,8 @@ def __complete_alias(
716714
prefix from the list of aliased namespaces. If name_in_ns is given,
717715
further attempt to refine the list to matching names in that namespace."""
718716
candidates = filter(
719-
Namespace.__completion_matcher(prefix), ((s, n) for s, n in self.aliases)
717+
Namespace.__completion_matcher(prefix),
718+
((s, n) for s, n in self.aliases.items()),
720719
)
721720
if name_in_ns is not None:
722721
for _, candidate_ns in candidates:
@@ -739,12 +738,13 @@ def __complete_imports_and_aliases(
739738
aliases = lmap.map(
740739
{
741740
alias: imports.val_at(import_name)
742-
for alias, import_name in self.import_aliases
741+
for alias, import_name in self.import_aliases.items()
743742
}
744743
)
745744

746745
candidates = filter(
747-
Namespace.__completion_matcher(prefix), itertools.chain(aliases, imports)
746+
Namespace.__completion_matcher(prefix),
747+
itertools.chain(aliases.items(), imports.items()),
748748
)
749749
if name_in_module is not None:
750750
for _, module in candidates:
@@ -771,7 +771,7 @@ def is_match(entry: Tuple[sym.Symbol, Var]) -> bool:
771771

772772
return map(
773773
lambda entry: f"{entry[0].name}",
774-
filter(is_match, ((s, v) for s, v in self.interns)),
774+
filter(is_match, ((s, v) for s, v in self.interns.items())),
775775
)
776776

777777
# pylint: disable=unnecessary-comprehension
@@ -781,7 +781,8 @@ def __complete_refers(self, value: str) -> Iterable[str]:
781781
return map(
782782
lambda entry: f"{entry[0].name}",
783783
filter(
784-
Namespace.__completion_matcher(value), ((s, v) for s, v in self.refers)
784+
Namespace.__completion_matcher(value),
785+
((s, v) for s, v in self.refers.items()),
785786
),
786787
)
787788

@@ -807,13 +808,11 @@ def complete(self, text: str) -> Iterable[str]:
807808
return results
808809

809810

810-
def push_thread_bindings(m: IAssociative[Var, Any]) -> None:
811+
def push_thread_bindings(m: IPersistentMap[Var, Any]) -> None:
811812
"""Push thread local bindings for the Var keys in m using the values."""
812813
bindings = set()
813814

814-
for entry in m:
815-
var: Var = entry.key # type: ignore
816-
val = entry.value
815+
for var, val in m.items():
817816
if not var.dynamic:
818817
raise RuntimeException(
819818
"cannot set thread-local bindings for non-dynamic Var"
@@ -853,11 +852,11 @@ def first(o):
853852
return s.first
854853

855854

856-
def rest(o) -> Optional[ISeq]:
855+
def rest(o) -> ISeq:
857856
"""If o is a ISeq, return the elements after the first in o. If o is None,
858857
returns an empty seq. Otherwise, coerces o to a seq and returns the rest."""
859858
if o is None:
860-
return None
859+
return lseq.EMPTY
861860
if isinstance(o, ISeq):
862861
s = o.rest
863862
if s is None:
@@ -1233,7 +1232,7 @@ def _to_py_map(
12331232
) -> dict:
12341233
return {
12351234
to_py(key, keyword_fn=keyword_fn): to_py(value, keyword_fn=keyword_fn)
1236-
for key, value in o
1235+
for key, value in o.items()
12371236
}
12381237

12391238

src/basilisp/lang/seq.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from typing import Any, Callable, Iterable, Iterator, Optional, TypeVar
22

3-
from basilisp.lang.interfaces import IPersistentMap, ISeq, IWithMeta
3+
from basilisp.lang.interfaces import IPersistentMap, ISeq, ISequential, IWithMeta
44
from basilisp.util import Maybe
55

66
T = TypeVar("T")
77

88

9-
class _EmptySequence(IWithMeta, ISeq[T]):
9+
class _EmptySequence(IWithMeta, ISequential, ISeq[T]):
1010
def __repr__(self):
1111
return "()"
1212

@@ -39,7 +39,7 @@ def cons(self, elem: T) -> ISeq[T]:
3939
EMPTY: ISeq = _EmptySequence()
4040

4141

42-
class Cons(ISeq[T], IWithMeta):
42+
class Cons(ISeq[T], ISequential, IWithMeta):
4343
__slots__ = ("_first", "_rest", "_meta")
4444

4545
def __init__(
@@ -75,7 +75,7 @@ def with_meta(self, meta: Optional[IPersistentMap]) -> "Cons[T]":
7575
return Cons(first=self._first, seq=self._rest, meta=meta)
7676

7777

78-
class _Sequence(IWithMeta, ISeq[T]):
78+
class _Sequence(IWithMeta, ISequential, ISeq[T]):
7979
"""Sequences are a thin wrapper over Python Iterable values so they can
8080
satisfy the Basilisp `ISeq` interface.
8181
@@ -128,7 +128,7 @@ def cons(self, elem):
128128
return Cons(elem, self)
129129

130130

131-
class LazySeq(IWithMeta, ISeq[T]):
131+
class LazySeq(IWithMeta, ISequential, ISeq[T]):
132132
"""LazySeqs are wrappers for delaying sequence computation. Create a LazySeq
133133
with a function that can either return None or a Seq. If a Seq is returned,
134134
the LazySeq is a proxy to that Seq."""

src/basilisp/lang/set.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Iterable, Optional, TypeVar
1+
from collections.abc import Set as _PySet
2+
from typing import AbstractSet, Iterable, Optional, TypeVar
23

34
from pyrsistent import PSet, pset # noqa # pylint: disable=unused-import
45

@@ -36,7 +37,11 @@ def __contains__(self, item):
3637
return item in self._inner
3738

3839
def __eq__(self, other):
39-
return self._inner == other
40+
if self is other:
41+
return True
42+
if not isinstance(other, AbstractSet):
43+
return NotImplemented
44+
return _PySet.__eq__(self, other)
4045

4146
def __ge__(self, other):
4247
return self._inner >= other

src/basilisp/lang/vector.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
IPersistentVector,
1010
ISeq,
1111
IWithMeta,
12+
seq_equals,
1213
)
1314
from basilisp.lang.obj import seq_lrepr as _seq_lrepr
1415
from basilisp.lang.seq import sequence
@@ -33,12 +34,11 @@ def __contains__(self, item):
3334
return item in self._inner
3435

3536
def __eq__(self, other):
36-
if hasattr(other, "__len__"):
37-
return self._inner == other
38-
try:
39-
return all(e1 == e2 for e1, e2 in zip(self._inner, other))
40-
except TypeError:
37+
if self is other:
38+
return True
39+
if hasattr(other, "__len__") and len(self) != len(other):
4140
return False
41+
return seq_equals(self, other)
4242

4343
def __getitem__(self, item):
4444
if isinstance(item, slice):
@@ -106,15 +106,7 @@ def pop(self) -> "Vector[T]":
106106
return self[:-1]
107107

108108
def rseq(self) -> ISeq[T]:
109-
def _reverse_vec() -> Iterable[T]:
110-
l = len(self)
111-
for i in range(l - 1, 0, -1):
112-
yield self._inner[i]
113-
114-
if l:
115-
yield self._inner[0]
116-
117-
return sequence(_reverse_vec())
109+
return sequence(reversed(self))
118110

119111

120112
K = TypeVar("K")

0 commit comments

Comments
 (0)