Skip to content

Commit c7f01ea

Browse files
authored
Add Associative ABC and apply to Map and Vector (#105)
1 parent 6a97a30 commit c7f01ea

File tree

8 files changed

+166
-26
lines changed

8 files changed

+166
-26
lines changed

basilisp/compiler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1525,8 +1525,8 @@ def with_lineno_and_col(ctx: CompilerContext, form: LispForm) -> ASTStream:
15251525
col = meta.get(reader.READER_COL_KW) # type: ignore
15261526

15271527
for astnode in f(ctx, form):
1528-
astnode.node.lineno = line
1529-
astnode.node.col_offset = col
1528+
astnode.node.lineno = line # type: ignore
1529+
astnode.node.col_offset = col # type: ignore
15301530
yield astnode
15311531
except AttributeError:
15321532
yield from f(ctx, form)

basilisp/lang/associative.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from abc import ABC, abstractmethod
2+
from typing import TypeVar, Mapping
3+
4+
K = TypeVar('K')
5+
V = TypeVar('V')
6+
7+
8+
class Associative(ABC, Mapping[K, V]):
9+
__slots__ = ()
10+
11+
@abstractmethod
12+
def assoc(self, *kvs) -> "Associative":
13+
raise NotImplementedError()
14+
15+
@abstractmethod
16+
def contains(self, k):
17+
raise NotImplementedError()
18+
19+
@abstractmethod
20+
def entry(self, k):
21+
raise NotImplementedError()

basilisp/lang/map.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pyrsistent import pmap, PMap
66

77
import basilisp.lang.vector as vec
8+
from basilisp.lang.associative import Associative
89
from basilisp.lang.meta import Meta
910
from basilisp.lang.seq import Seqable, sequence, Seq
1011
from basilisp.lang.util import lrepr
@@ -57,9 +58,8 @@ def from_vec(v: Sequence) -> "MapEntry":
5758
return MapEntry(vec.vector(v))
5859

5960

60-
class Map(Meta, Seqable):
61+
class Map(Associative, Meta, Seqable):
6162
"""Basilisp Map. Delegates internally to a pyrsistent.PMap object.
62-
6363
Do not instantiate directly. Instead use the m() and map() factory
6464
methods below."""
6565
__slots__ = ('_inner', '_meta',)
@@ -119,19 +119,30 @@ def with_meta(self, meta: "Map") -> "Map":
119119
return Map(self._inner, meta=new_meta)
120120

121121
def assoc(self, *kvs) -> "Map":
122-
m = self._inner
122+
m = self._inner.evolver()
123123
for k, v in seq(kvs).grouped(2):
124-
m = m.set(k, v)
125-
return Map(m)
124+
m[k] = v
125+
return Map(m.persistent())
126+
127+
def contains(self, k):
128+
if k in self._inner:
129+
return True
130+
return False
126131

127132
def dissoc(self, *ks) -> "Map":
128133
return self.discard(*ks)
129134

130135
def discard(self, *ks) -> "Map":
131-
m: PMap = self._inner
136+
m = self._inner.evolver()
132137
for k in ks:
133-
m = m.discard(k)
134-
return Map(m)
138+
try:
139+
del m[k]
140+
except KeyError:
141+
pass
142+
return Map(m.persistent())
143+
144+
def entry(self, k):
145+
return self._inner.get(k, None)
135146

136147
def update(self, *maps) -> "Map":
137148
m: PMap = self._inner.update(*maps)

basilisp/lang/runtime.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
from functional import seq
88
from pyrsistent import pmap, PMap, PSet, pset
99

10+
import basilisp.lang.associative as lassoc
1011
import basilisp.lang.list as llist
12+
import basilisp.lang.map as lmap
1113
import basilisp.lang.seq as lseq
1214
import basilisp.lang.symbol as sym
1315
from basilisp.lang import atom
@@ -23,7 +25,6 @@
2325

2426
def _new_module(name: str, doc=None) -> types.ModuleType:
2527
"""Create a new empty Basilisp Python module.
26-
2728
Modules are created for each Namespace when it is created."""
2829
mod = types.ModuleType(name, doc=doc)
2930
mod.__loader__ = None
@@ -158,26 +159,21 @@ class Namespace:
158159
"""Namespaces serve as organizational units in Basilisp code, just as
159160
they do in Clojure code. Vars are mutable containers for functions and
160161
data which may be interned in a namespace and referred to by a Symbol.
161-
162162
Namespaces additionally may have aliases to other namespaces, so code
163163
organized in one namespace may conveniently refer to code or data in
164164
other namespaces using that alias as the Symbol's namespace.
165-
166165
Namespaces are constructed def-by-def as Basilisp reads in each form
167166
in a file (which will typically declare a namespace at the top).
168-
169167
Namespaces have the following fields of interest:
170-
171168
- `mappings` is a mapping between a symbolic name and a Var. The
172169
Var may point to code, data, or nothing, if it is unbound.
173-
174170
- `aliases` is a mapping between a symbolic alias and another
175171
Namespace. The fully qualified name of a namespace is also
176172
an alias for itself.
177-
178173
- `imports` is a set of Python modules imported into the current
179174
namespace"""
180175
DEFAULT_IMPORTS = atom.Atom(pset(seq(['builtins',
176+
'operator',
181177
'basilisp.lang.exception',
182178
'basilisp.lang.keyword',
183179
'basilisp.lang.list',
@@ -204,7 +200,6 @@ def __init__(self, name: sym.Symbol, module: types.ModuleType = None) -> None:
204200
@classmethod
205201
def add_default_import(cls, module: str):
206202
"""Add a gated default import to the default imports.
207-
208203
In particular, we need to avoid importing 'basilisp.core' before we have
209204
finished macro-expanding."""
210205
if module in cls.GATED_IMPORTS:
@@ -221,7 +216,6 @@ def module(self):
221216
@module.setter
222217
def module(self, m: types.ModuleType):
223218
"""Override the Python module for this Namespace.
224-
225219
This should only be done by basilisp.importer code to make sure the
226220
correct module is generated for `basilisp.core`."""
227221
self._module = m
@@ -254,7 +248,6 @@ def get_alias(self, alias: sym.Symbol) -> "Optional[Namespace]":
254248

255249
def intern(self, sym: sym.Symbol, var: Var, force: bool = False) -> Var:
256250
"""Intern the Var given in this namespace mapped by the given Symbol.
257-
258251
If the Symbol already maps to a Var, this method _will not overwrite_
259252
the existing Var mapping unless the force keyword argument is given
260253
and is True."""
@@ -322,15 +315,13 @@ def __get_or_create(ns_cache: PMap,
322315
def get_or_create(cls, name: sym.Symbol, module: types.ModuleType = None) -> "Namespace":
323316
"""Get the namespace bound to the symbol `name` in the global namespace
324317
cache, creating it if it does not exist.
325-
326318
Return the namespace."""
327319
return cls._NAMESPACES.swap(Namespace.__get_or_create, name, module=module)[name]
328320

329321
@classmethod
330322
def remove(cls, name: sym.Symbol) -> Optional["Namespace"]:
331323
"""Remove the namespace bound to the symbol `name` in the global
332324
namespace cache and return that namespace.
333-
334325
Return None if the namespace did not exist in the cache."""
335326
while True:
336327
oldval: PMap = cls._NAMESPACES.deref()
@@ -446,10 +437,8 @@ def concat(*seqs) -> lseq.Seq:
446437

447438
def apply(f, args):
448439
"""Apply function f to the arguments provided.
449-
450440
The last argument must always be coercible to a Seq. Intermediate
451441
arguments are not modified.
452-
453442
For example:
454443
(apply max [1 2 3]) ;=> 3
455444
(apply max 4 [1 2 3]) ;=> 4"""
@@ -483,6 +472,16 @@ def nth(coll, i):
483472
raise TypeError(f"nth not supported on object of type {type(coll)}")
484473

485474

475+
def assoc(m, *kvs):
476+
"""Associate keys to values in associative data structure m. If m is None,
477+
returns a new Map with key-values kvs."""
478+
if m is None:
479+
return lmap.Map.empty().assoc(*kvs)
480+
if isinstance(m, lassoc.Associative):
481+
return m.assoc(*kvs)
482+
raise TypeError(f"Object of type {type(m)} does not implement Associative interface")
483+
484+
486485
def _collect_args(args) -> lseq.Seq:
487486
"""Collect Python starred arguments into a Basilisp list."""
488487
if isinstance(args, tuple):

basilisp/lang/vector.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from pyrsistent import PVector, pvector
22

3+
from basilisp.lang.associative import Associative
34
from basilisp.lang.meta import Meta
45
from basilisp.lang.seq import Seqable, Seq, sequence
56
from basilisp.lang.util import lrepr
67

78

8-
class Vector(Meta, Seqable):
9+
class Vector(Associative, Meta, Seqable):
910
"""Basilisp Vector. Delegates internally to a pyrsistent.PVector object.
10-
1111
Do not instantiate directly. Instead use the v() and vec() factory
1212
methods below."""
1313
__slots__ = ('_inner', '_meta',)
@@ -48,6 +48,18 @@ def with_meta(self, meta) -> "Vector":
4848
def conj(self, elem) -> "Vector":
4949
return Vector(self._inner.append(elem), meta=self.meta)
5050

51+
def assoc(self, *kvs):
52+
return Vector(self._inner.mset(*kvs))
53+
54+
def contains(self, k):
55+
return 0 <= k < len(self._inner)
56+
57+
def entry(self, k):
58+
try:
59+
return self._inner[k]
60+
except IndexError:
61+
return None
62+
5163
@staticmethod
5264
def empty() -> "Vector":
5365
return v()

tests/map_test.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22

3+
import basilisp.lang.associative as lassoc
34
import basilisp.lang.map as lmap
45
import basilisp.lang.meta as meta
56
import basilisp.lang.seq as lseq
@@ -8,6 +9,11 @@
89
from basilisp.lang.symbol import symbol
910

1011

12+
def test_map_associative_interface():
13+
assert isinstance(lmap.m(), lassoc.Associative)
14+
assert issubclass(lmap.Map, lassoc.Associative)
15+
16+
1117
def test_map_meta_interface():
1218
assert isinstance(lmap.m(), meta.Meta)
1319
assert issubclass(lmap.Map, meta.Meta)
@@ -18,6 +24,43 @@ def test_map_seqable_interface():
1824
assert issubclass(lmap.Map, lseq.Seqable)
1925

2026

27+
def test_assoc():
28+
m = lmap.m()
29+
assert lmap.map({"k": 1}) == m.assoc("k", 1)
30+
assert lmap.Map.empty() == m
31+
assert lmap.map({"a": 1, "b": 2}) == m.assoc("a", 1, "b", 2)
32+
33+
m1 = lmap.map({"a": 3})
34+
assert lmap.map({"a": 1, "b": 2}) == m1.assoc("a", 1, "b", 2)
35+
assert lmap.map({"a": 3, "b": 2}) == m1.assoc("b", 2)
36+
37+
38+
def test_contains():
39+
assert True is lmap.map({"a": 1}).contains("a")
40+
assert False is lmap.map({"a": 1}).contains("b")
41+
assert False is lmap.Map.empty().contains("a")
42+
43+
44+
def test_dissoc():
45+
assert lmap.Map.empty() == lmap.Map.empty().dissoc("a")
46+
assert lmap.Map.empty() == lmap.Map.empty().dissoc("a", "b", "c")
47+
48+
m1 = lmap.map({"a": 3})
49+
assert m1 == m1.dissoc("b")
50+
assert lmap.Map.empty() == m1.dissoc("a")
51+
52+
m2 = lmap.map({"a": 3, "b": 2})
53+
assert lmap.map({"a": 3}) == m2.dissoc("b")
54+
assert lmap.map({"b": 2}) == m2.dissoc("a")
55+
assert lmap.Map.empty() == m2.dissoc("a", "b")
56+
57+
58+
def test_entry():
59+
assert 1 == lmap.map({"a": 1}).entry("a")
60+
assert None is lmap.map({"a": 1}).entry("b")
61+
assert None is lmap.Map.empty().entry("a")
62+
63+
2164
def test_map_conj():
2265
meta = lmap.m(tag="async")
2366
m1 = lmap.map({"first": "Chris"}, meta=meta)

tests/runtime_test.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,20 @@ def test_nth():
150150

151151
with pytest.raises(TypeError):
152152
runtime.nth(3, 1)
153+
154+
155+
def test_assoc():
156+
assert lmap.Map.empty() == runtime.assoc(None)
157+
assert lmap.map({"a": 1}) == runtime.assoc(None, "a", 1)
158+
assert lmap.map({"a": 8}) == runtime.assoc(lmap.map({"a": 1}), "a", 8)
159+
assert lmap.map({"a": 1, "b": "string"}) == runtime.assoc(lmap.map({"a": 1}), "b", "string")
160+
161+
assert vec.v("a") == runtime.assoc(vec.Vector.empty(), 0, "a")
162+
assert vec.v("c", "b") == runtime.assoc(vec.v("a", "b"), 0, "c")
163+
assert vec.v("a", "c") == runtime.assoc(vec.v("a", "b"), 1, "c")
164+
165+
with pytest.raises(IndexError):
166+
runtime.assoc(vec.Vector.empty(), 1, "a")
167+
168+
with pytest.raises(TypeError):
169+
runtime.assoc(llist.List.empty(), 1, "a")

tests/vector_test.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import basilisp.lang.associative as lassoc
12
import basilisp.lang.map as lmap
23
import basilisp.lang.meta as meta
34
import basilisp.lang.seq as lseq
@@ -6,6 +7,11 @@
67
from basilisp.lang.symbol import symbol
78

89

10+
def test_map_associative_interface():
11+
assert isinstance(lmap.m(), lassoc.Associative)
12+
assert issubclass(lmap.Map, lassoc.Associative)
13+
14+
915
def test_vector_meta_interface():
1016
assert isinstance(vector.v(), meta.Meta)
1117
assert issubclass(vector.Vector, meta.Meta)
@@ -20,6 +26,27 @@ def test_vector_slice():
2026
assert isinstance(vector.v(1, 2, 3)[1:], vector.Vector)
2127

2228

29+
def test_assoc():
30+
v = vector.Vector.empty()
31+
assert vector.v("a") == v.assoc(0, "a")
32+
assert vector.Vector.empty() == v
33+
assert vector.vector(["a", "b"]) == v.assoc(0, "a", 1, "b")
34+
35+
v1 = vector.v("a")
36+
assert vector.v("c", "b") == v1.assoc(0, "c", 1, "b")
37+
assert vector.v("a", "b") == v1.assoc(1, "b")
38+
39+
40+
def test_contains():
41+
assert True is vector.v("a").contains(0)
42+
assert True is vector.v("a", "b").contains(1)
43+
assert False is vector.v("a", "b").contains(2)
44+
assert False is vector.v("a", "b").contains(-1)
45+
assert False is vector.Vector.empty().contains(0)
46+
assert False is vector.Vector.empty().contains(1)
47+
assert False is vector.Vector.empty().contains(-1)
48+
49+
2350
def test_vector_conj():
2451
meta = lmap.m(tag="async")
2552
v1 = vector.v(keyword("kw1"), meta=meta)
@@ -31,6 +58,16 @@ def test_vector_conj():
3158
assert meta == v2.meta
3259

3360

61+
def test_entry():
62+
assert "a" == vector.v("a").entry(0)
63+
assert "b" == vector.v("a", "b").entry(1)
64+
assert None is vector.v("a", "b").entry(2)
65+
assert "b" == vector.v("a", "b").entry(-1)
66+
assert None is vector.Vector.empty().entry(0)
67+
assert None is vector.Vector.empty().entry(1)
68+
assert None is vector.Vector.empty().entry(-1)
69+
70+
3471
def test_vector_meta():
3572
assert vector.v("vec").meta is None
3673
meta = lmap.m(type=symbol("str"))

0 commit comments

Comments
 (0)