Skip to content

Commit e50da8b

Browse files
authored
Convert Map to use immutables.Map rather than pyrsistent (#559)
Convert `basilisp.lang.map.Map` and `basilisp.lang.set.Set` to use `immutables.Map` under the hood, rather than the Python-only implementation provided by `pyrsistent`, which should offer some decent performance improvements for both types. As another performance improvement, most of the basic runtime functions like `first`, `rest`, `cons`, et al were converted to single dispatch (using `@functools.singledispatch`), which should be a little faster than a series of `isinstance` checks.
1 parent 844ba29 commit e50da8b

File tree

11 files changed

+479
-264
lines changed

11 files changed

+479
-264
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
* Added support for Volatiles (#460)
1818
* Add JSON encoder and decoder in `basilisp.json` namespace (#484)
1919

20+
### Changed
21+
* Basilisp set and map types are now backed by the HAMT provided by `immutables` (#557)
22+
2023
### Fixed
2124
* Fixed a bug where the Basilisp AST nodes for return values of `deftype` members could be marked as _statements_ rather than _expressions_, resulting in an incorrect `nil` return (#523)
2225
* Fixed a bug where `defonce` would throw a Python SyntaxError due to a superfluous `global` statement in the generated Python (#525)
@@ -27,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2730
* Fixed a bug where not all builtin Basilisp types could be pickled (#518)
2831
* Fixed a bug where `deftype` forms could not be created interfaces declared not at the top-level of a code block in a namespace (#376)
2932
* Fixed multiple bugs relating to symbol resolution of `import`ed symbols in various contexts (#544)
33+
* Fixed a bug where the `=` function did not respect the equality partition for various builtin collection types (#556)
3034

3135
## [v0.1.dev13] - 2020-03-16
3236
### Added

Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pyfunctional = "*"
2626
pyrsistent = "*"
2727
python-dateutil = "*"
2828
pytest = "*"
29+
immutables = "*"
2930

3031
[requires]
3132
python_version = "3.6"

Pipfile.lock

Lines changed: 156 additions & 93 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"atomos",
2626
"attrs",
2727
"click",
28+
"immutables",
2829
"prompt-toolkit>=3.0.0",
2930
"pyfunctional",
3031
"pyrsistent",

src/basilisp/core.lpy

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,7 @@
241241
:arglists '([coll])}
242242
count
243243
(fn count [coll]
244-
(try
245-
(python/len coll)
246-
(catch TypeError _
247-
(count (apply vector coll))))))
244+
(basilisp.lang.runtime/count coll)))
248245

249246
(def
250247
^{:doc "Returns a basilisp.lang.exception/ExceptionInfo instance with
@@ -1098,6 +1095,11 @@
10981095
[x]
10991096
(python/callable x))
11001097

1098+
(defn indexed?
1099+
"Return true if elements of x can be accessed by index efficiently."
1100+
[x]
1101+
(instance? basilisp.lang.interfaces/IIndexed x))
1102+
11011103
(def ^{:doc "Return true if x is an integer.
11021104

11031105
Note that unlike Clojure, Basilisp uses Python integers and

src/basilisp/lang/interfaces.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ def deref(
3737

3838

3939
class ICounted(ABC):
40+
"""ICounted types can produce their length in constant time.
41+
42+
All of the builtin collections are ICounted, except Lists whose length is
43+
determined by counting all of the elements in the list in linear time."""
44+
45+
__slots__ = ()
46+
47+
48+
class IIndexed(ICounted, ABC):
49+
"""IIndexed types can be accessed by index.
50+
51+
Of the builtin collections, only Vectors are IIndexed. IIndexed types respond
52+
True to the `indexed?` predicate."""
53+
4054
__slots__ = ()
4155

4256

@@ -107,6 +121,12 @@ def reset_meta(self, meta: "IPersistentMap") -> "IPersistentMap":
107121

108122

109123
class IReversible(Generic[T]):
124+
"""IReversible types can produce a sequences of their elements in reverse in
125+
constant time.
126+
127+
Of the builtin collections, only Vectors are IReversible. IIndexed types respond
128+
True to the `reversible` predicate."""
129+
110130
__slots__ = ()
111131

112132
@abstractmethod
@@ -115,6 +135,11 @@ def rseq(self) -> "ISeq[T]":
115135

116136

117137
class ISeqable(Iterable[T]):
138+
"""ISeqable types can produce sequences of their elements, but are not ISeqs.
139+
140+
All of the builtin collections are ISeqable, except Lists which directly
141+
implement ISeq. Values of type ISeqable respond True to the `seqable?` predicate."""
142+
118143
__slots__ = ()
119144

120145
@abstractmethod
@@ -123,6 +148,11 @@ def seq(self) -> "ISeq[T]":
123148

124149

125150
class ISequential(ABC):
151+
"""ISequential is a marker interface for sequential types.
152+
153+
Lists and Vectors are both considered ISequential and respond True to the
154+
`sequential?` predicate."""
155+
126156
__slots__ = ()
127157

128158

@@ -222,7 +252,7 @@ def disj(self: T_set, *elems: T) -> T_set:
222252
class IPersistentVector(
223253
Sequence[T],
224254
IAssociative[int, T],
225-
ICounted,
255+
IIndexed,
226256
IReversible[T],
227257
ISequential,
228258
IPersistentStack[T],
@@ -243,6 +273,10 @@ def seq(self) -> "ISeq[T]": # type: ignore[override]
243273

244274

245275
class IRecord(ILispObject):
276+
"""IRecord is a marker interface for types def'ed by `defrecord` forms.
277+
278+
All types created by `defrecord` are automatically marked with IRecord."""
279+
246280
__slots__ = ()
247281

248282
@classmethod
@@ -347,4 +381,8 @@ def __iter__(self):
347381

348382

349383
class IType(ABC):
384+
"""IType is a marker interface for types def'ed by `deftype` forms.
385+
386+
All types created by `deftype` are automatically marked with IType."""
387+
350388
__slots__ = ()

src/basilisp/lang/map.py

Lines changed: 58 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from builtins import map as pymap
2-
from typing import Callable, Iterable, Mapping, Optional, TypeVar, Union, cast
2+
from typing import Callable, Iterable, Mapping, Optional, Tuple, TypeVar, Union, cast
33

4-
from pyrsistent import PMap, pmap # noqa # pylint: disable=unused-import
4+
from immutables import Map as _Map
55

66
from basilisp.lang.interfaces import (
77
ILispObject,
@@ -25,16 +25,18 @@
2525

2626

2727
class Map(IPersistentMap[K, V], ILispObject, IWithMeta):
28-
"""Basilisp Map. Delegates internally to a pyrsistent.PMap object.
28+
"""Basilisp Map. Delegates internally to a immutables.Map object.
2929
Do not instantiate directly. Instead use the m() and map() factory
3030
methods below."""
3131

3232
__slots__ = ("_inner", "_meta")
3333

3434
def __init__(
35-
self, wrapped: "PMap[K, V]", meta: Optional[IPersistentMap] = None
35+
self,
36+
members: Union[Mapping[K, V], Iterable[Tuple[K, V]]],
37+
meta: Optional[IPersistentMap] = None,
3638
) -> None:
37-
self._inner = wrapped
39+
self._inner = _Map(members)
3840
self._meta = meta
3941

4042
def __call__(self, key, default=None):
@@ -50,7 +52,7 @@ def __eq__(self, other):
5052
return NotImplemented
5153
if len(self._inner) != len(other):
5254
return False
53-
return dict(self._inner.iteritems()) == dict(other.items())
55+
return self._inner == other
5456

5557
def __getitem__(self, item):
5658
return self._inner[item]
@@ -59,14 +61,14 @@ def __hash__(self):
5961
return hash(self._inner)
6062

6163
def __iter__(self):
62-
yield from self._inner.iterkeys()
64+
return iter(self._inner)
6365

6466
def __len__(self):
6567
return len(self._inner)
6668

6769
def _lrepr(self, **kwargs):
6870
return _map_lrepr(
69-
self._inner.iteritems, start="{", end="}", meta=self._meta, **kwargs
71+
self._inner.items, start="{", end="}", meta=self._meta, **kwargs
7072
)
7173

7274
@property
@@ -77,24 +79,22 @@ def with_meta(self, meta: Optional[IPersistentMap]) -> "Map":
7779
return Map(self._inner, meta=meta)
7880

7981
def assoc(self, *kvs):
80-
m = self._inner.evolver()
81-
for k, v in partition(kvs, 2):
82-
m[k] = v
83-
return Map(m.persistent())
82+
with self._inner.mutate() as m:
83+
for k, v in partition(kvs, 2):
84+
m[k] = v
85+
return Map(m.finish())
8486

8587
def contains(self, k):
86-
if k in self._inner:
87-
return True
88-
return False
88+
return k in self._inner
8989

9090
def dissoc(self, *ks):
91-
m = self._inner.evolver()
92-
for k in ks:
93-
try:
94-
del m[k]
95-
except KeyError:
96-
pass
97-
return Map(m.persistent())
91+
with self._inner.mutate() as m:
92+
for k in ks:
93+
try:
94+
del m[k]
95+
except KeyError:
96+
pass
97+
return Map(m.finish())
9898

9999
def entry(self, k):
100100
v = self._inner.get(k, cast("V", _ENTRY_SENTINEL))
@@ -106,14 +106,17 @@ def val_at(self, k, default=None):
106106
return self._inner.get(k, default)
107107

108108
def update(self, *maps: Mapping[K, V]) -> "Map":
109-
m: PMap = self._inner.update(*maps)
109+
m: _Map = self._inner.update(*(m.items() for m in maps))
110110
return Map(m)
111111

112112
def update_with(
113113
self, merge_fn: Callable[[V, V], V], *maps: Mapping[K, V]
114114
) -> "Map[K, V]":
115-
m: PMap = self._inner.update_with(merge_fn, *maps)
116-
return Map(m)
115+
with self._inner.mutate() as m:
116+
for map in maps:
117+
for k, v in map.items():
118+
m.set(k, merge_fn(m[k], v) if k in m else v)
119+
return Map(m.finish())
117120

118121
def cons( # type: ignore[override]
119122
self,
@@ -124,51 +127,53 @@ def cons( # type: ignore[override]
124127
Mapping[K, V],
125128
],
126129
) -> "Map[K, V]":
127-
e = self._inner.evolver()
128-
try:
129-
for elem in elems:
130-
if isinstance(elem, (IPersistentMap, Mapping)):
131-
for k, v in elem.items():
132-
e.set(k, v)
133-
elif isinstance(elem, IMapEntry):
134-
e.set(elem.key, elem.value)
135-
elif elem is None:
136-
continue
137-
else:
138-
# This block leniently allows nearly any 2 element sequential
139-
# type including Vectors, Python lists, and Python tuples.
140-
entry: IMapEntry[K, V] = MapEntry.from_vec(elem)
141-
e.set(entry.key, entry.value)
142-
except (TypeError, ValueError):
143-
raise ValueError(
144-
"Argument to map conj must be another Map or castable to MapEntry"
145-
)
146-
else:
147-
return Map(e.persistent(), meta=self.meta)
130+
with self._inner.mutate() as m:
131+
try:
132+
for elem in elems:
133+
if isinstance(elem, (IPersistentMap, Mapping)):
134+
for k, v in elem.items():
135+
m.set(k, v)
136+
elif isinstance(elem, IMapEntry):
137+
m.set(elem.key, elem.value)
138+
elif elem is None:
139+
continue
140+
else:
141+
entry: IMapEntry[K, V] = MapEntry.from_vec(elem)
142+
m.set(entry.key, entry.value)
143+
except (TypeError, ValueError):
144+
raise ValueError(
145+
"Argument to map conj must be another Map or castable to MapEntry"
146+
)
147+
else:
148+
return Map(m.finish(), meta=self.meta)
148149

149150
@staticmethod
150151
def empty() -> "Map":
151152
return m()
152153

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

156157

157158
def map(kvs: Mapping[K, V], meta=None) -> Map[K, V]: # pylint:disable=redefined-builtin
158159
"""Creates a new map."""
159-
return Map(pmap(initial=kvs), meta=meta)
160+
# For some reason, creating a new `immutables.Map` instance from an existing
161+
# `basilisp.lang.map.Map` instance causes issues because the `__iter__` returns
162+
# only the keys rather than tuple of key/value pairs, even though it adheres to
163+
# the `Mapping` protocol. Passing the `.items()` directly bypasses this problem.
164+
return Map(kvs.items(), meta=meta)
160165

161166

162167
def m(**kvs) -> Map[str, V]:
163168
"""Creates a new map from keyword arguments."""
164-
return Map(pmap(initial=kvs))
169+
return Map(kvs)
165170

166171

167172
def from_entries(entries: Iterable[MapEntry[K, V]]) -> Map[K, V]:
168-
m = pmap().evolver() # type: ignore
169-
for entry in entries:
170-
m.set(entry.key, entry.value)
171-
return Map(m.persistent())
173+
with _Map().mutate() as m: # type: ignore[var-annotated]
174+
for entry in entries:
175+
m.set(entry.key, entry.value)
176+
return Map(m.finish())
172177

173178

174179
def hash_map(*pairs) -> Map:

0 commit comments

Comments
 (0)