Skip to content

Commit 451eb67

Browse files
authored
Add support for Hierarchies (#633)
* Add support for Hierarchies * Talkin' words * Ok I think this actually works * Tighten it all up * Add subclasses utility function * Correctly spell descendants * Start of hierarchy tests w/ types * Fool of a took * Partial tests w/ keywords * Rename the thing * Additional test cases * Amazing * Test nil-punning disj and dissoc * Organization * Support nil values in contains? * Underive tests for keywords * Fix a test * isa? vector support * Partially broken underive tests * Fix underive implementation
1 parent deb6781 commit 451eb67

File tree

6 files changed

+542
-9
lines changed

6 files changed

+542
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
* Added a bootstrapping function for easily bootstrapping Basilisp projects from Python (#620)
1010
* Added support for watchers and validator functions on Atoms and Vars (#627)
1111
* Added support for Taps (#631)
12+
* Added support for hierarchies (#???)
1213

1314
### Changed
1415
* PyTest is now an optional extra dependency, rather than a required dependency (#622)

src/basilisp/core.lpy

Lines changed: 212 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
(import* attr)
2020

21+
(import* basilisp.lang.multifn)
22+
2123
(def ^{:doc "Create a list from the arguments."
2224
:arglists '([& args])}
2325
list
@@ -1414,9 +1416,14 @@
14141416
[x]
14151417
(= 0 x))
14161418

1417-
;;;;;;;;;;;;;;;;;;;
1418-
;; Type Coercion ;;
1419-
;;;;;;;;;;;;;;;;;;;
1419+
;;;;;;;;;;;;;;;;;;;;
1420+
;; Type Utilities ;;
1421+
;;;;;;;;;;;;;;;;;;;;
1422+
1423+
(defn bases
1424+
"Return the direct superclasses and interfaces of cls as a sequence."
1425+
[cls]
1426+
(apply list (.-__bases__ cls)))
14201427

14211428
(defn cast
14221429
"Throws a `TypeError` if x is not a cls. Otherwise, return x."
@@ -1431,11 +1438,29 @@
14311438
[x]
14321439
(python/type x))
14331440

1441+
(defn subclasses
1442+
"Return a set of subclasses of cls.
1443+
1444+
This set is not guaranteed to be exhaustive because classes only retain a
1445+
weak reference to their subclasses, so it is possible subclasses of cls
1446+
have been garbage collected."
1447+
[cls]
1448+
(set (.__subclasses__ cls)))
1449+
1450+
(defn supers
1451+
"Return the direct and indirect superclasses and interfaces of cls as a set."
1452+
[cls]
1453+
(set (python/filter #(not= cls %) (.-__mro__ cls))))
1454+
14341455
(defn type
14351456
"Return the type of x."
14361457
[x]
14371458
(python/type x))
14381459

1460+
;;;;;;;;;;;;;;;;;;;
1461+
;; Type Coercion ;;
1462+
;;;;;;;;;;;;;;;;;;;
1463+
14391464
(defn bigdec
14401465
"Coerce x to a Decimal."
14411466
[x]
@@ -2040,14 +2065,23 @@
20402065
"Return a new version of the set s without the given elements. If the elements
20412066
don't exist in s, they are ignored."
20422067
([s] s)
2043-
([s & elems]
2044-
(apply (.-disj s) elems)))
2068+
([s elem]
2069+
(when s
2070+
(.disj s elem)))
2071+
([s elem & elems]
2072+
(when s
2073+
(apply (.-disj s) elem elems))))
20452074

20462075
(defn dissoc
20472076
"Return a new version of m without the given keys. If the keys
20482077
don't exist in m, they are ignored."
2049-
[m & ks]
2050-
(apply (.-dissoc m) ks))
2078+
([m] m)
2079+
([m k]
2080+
(when m
2081+
(.dissoc m k)))
2082+
([m k & ks]
2083+
(when m
2084+
(apply (.-dissoc m) k ks))))
20512085

20522086
(defn get
20532087
"Return the entry of `m` corresponding to `k` if it exists or `nil`/`default`
@@ -4593,12 +4627,181 @@
45934627
[pattern s]
45944628
(lazy-re-seq (seq (re/finditer pattern s))))
45954629

4630+
;;;;;;;;;;;;;;;;;
4631+
;; Hierarchies ;;
4632+
;;;;;;;;;;;;;;;;;
4633+
4634+
(defn make-hierarchy
4635+
"Return a hierarchy that may be used to establish parent and child
4636+
relationships via `derive` (relationships may be removed via `underive`).
4637+
4638+
Relationships can be queried using `ancestors`, `descendants`, `parents`, and
4639+
`isa?`."
4640+
[]
4641+
{:parents {}
4642+
:ancestors {}
4643+
:descendants {}})
4644+
4645+
(def ^:private ^:redef global-hierarchy (make-hierarchy))
4646+
4647+
(defn ancestors
4648+
"Return the set of all ancestors (parents and grandparents and so forth) of
4649+
`tag` in hierarchy `h`.
4650+
4651+
`tag` may be either a valid Python type or a namespace-qualified keyword or
4652+
symbol. If `tag` is a Python type, ancestors include any relationships
4653+
established via calls to `derive` as well as any superclasses (as returned by
4654+
`supers`). If `tag` is a namespace-qualified keyword, only relationships from
4655+
`derive` are returned.
4656+
4657+
`h` must be a hierarchy returned by `make-hierarchy`. If `h` is not supplied,
4658+
the global hierarchy will be used."
4659+
([tag]
4660+
(ancestors global-hierarchy tag))
4661+
([h tag]
4662+
(not-empty
4663+
(let [hierarchy-ancestors (get-in h [:ancestors tag] #{})]
4664+
(if (class? tag)
4665+
(reduce conj hierarchy-ancestors (supers tag))
4666+
hierarchy-ancestors)))))
4667+
4668+
(defn descendants
4669+
"Return the set of all descendants (children and grandchildren and so forth)
4670+
of `tag` in hierarchy `h`.
4671+
4672+
`tag` must be a namespace-qualified keyword or symbol. Python types are not
4673+
supported for `descendants` checks.
4674+
4675+
`h` must be a hierarchy returned by `make-hierarchy`. If `h` is not supplied,
4676+
the global hierarchy will be used."
4677+
([tag]
4678+
(descendants global-hierarchy tag))
4679+
([h tag]
4680+
(not-empty
4681+
(let [hierarchy-ancestors (get-in h [:descendants tag] #{})]
4682+
(if (class? tag)
4683+
(throw (python/TypeError "Cannot get descendants of classes"))
4684+
hierarchy-ancestors)))))
4685+
4686+
(defn parents
4687+
"Return the set of all direct parents of `tag` in hierarchy `h`.
4688+
4689+
`tag` may be either a valid Python type or a namespace-qualified keyword or
4690+
symbol. If `tag` is a Python type, ancestors include any relationships
4691+
established via calls to `derive` as well as any immediate superclasses (as
4692+
returned by `bases`). If `tag` is a namespace-qualified keyword, only
4693+
relationships from `derive` are returned.
4694+
4695+
`h` must be a hierarchy returned by `make-hierarchy`. If `h` is not supplied,
4696+
the global hierarchy will be used."
4697+
([tag]
4698+
(parents global-hierarchy tag))
4699+
([h tag]
4700+
(not-empty
4701+
(let [hierarchy-ancestors (get-in h [:parents tag] #{})]
4702+
(if (class? tag)
4703+
(reduce conj hierarchy-ancestors (bases tag))
4704+
hierarchy-ancestors)))))
4705+
4706+
(defn isa?
4707+
"Return true if `tag` is equal to `parent` or is a descendant of `parent` in
4708+
hierarchy `h`.
4709+
4710+
Both `tag` and `parent` may be a valid Python type or a namespace-qualified
4711+
keyword or symbol or a vector of namespace-qualified keywords or symbols. If
4712+
either of `tag` or `parent` is a vector, the other must be a vector as well.
4713+
4714+
Ancestors will be fetched for `tag` using `ancestors`.
4715+
4716+
`h` must be a hierarchy returned by `make-hierarchy`. If `h` is not supplied,
4717+
the global hierarchy will be used."
4718+
([tag parent]
4719+
(isa? global-hierarchy tag parent))
4720+
([h tag parent]
4721+
(or (= tag parent)
4722+
(and (vector? tag)
4723+
(vector? parent)
4724+
(->> (map (partial isa? h) tag parent)
4725+
(every? identity)))
4726+
(contains? (ancestors h tag) parent))))
4727+
4728+
(defn derive
4729+
"Derive a parent/child relationship between `tag` and `parent`.
4730+
4731+
`tag` may be either a valid Python type or a namespace-qualified keyword or
4732+
symbol.
4733+
4734+
`parent` must be a namespace-qualified keyword or symbol.
4735+
4736+
`h` must be a hierarchy returned by `make-hierarchy`. If `h` is not supplied,
4737+
the global hierarchy will be used.
4738+
4739+
Relationships may be removed via `underive`."
4740+
([tag parent]
4741+
(alter-var-root #'global-hierarchy derive tag parent))
4742+
([h tag parent]
4743+
(assert (qualified-ident? parent) "Parent must be a qualified keyword or symbol")
4744+
(assert (or (qualified-ident? tag)
4745+
(class? tag))
4746+
"Tag must be a valid Python type or a qualified keyword or symbol")
4747+
4748+
(let [parent-ancestors (get-in h [:ancestors parent] #{})
4749+
cur-descendants (get-in h [:descendants tag] #{})]
4750+
{:parents (as-> (get-in h [:parents tag] #{}) $
4751+
(conj $ parent)
4752+
(assoc (:parents h) tag $))
4753+
:ancestors (reduce (fn [ancestors descendant]
4754+
(->> (get ancestors descendant)
4755+
(apply conj parent-ancestors parent)
4756+
(set)
4757+
(assoc ancestors descendant)))
4758+
(:ancestors h)
4759+
(conj cur-descendants tag))
4760+
:descendants (reduce (fn [descendants ancestor]
4761+
(->> (get descendants ancestor)
4762+
(apply conj cur-descendants tag)
4763+
(set)
4764+
(assoc descendants ancestor)))
4765+
(:descendants h)
4766+
(conj parent-ancestors parent))})))
4767+
4768+
(defn underive
4769+
"Remove a parent/child relationship between `tag` and `parent` originally
4770+
created via `derive`.
4771+
4772+
`tag` may be either a valid Python type or a namespace-qualified keyword or
4773+
symbol.
4774+
4775+
`parent` must be a namespace-qualified keyword or symbol.
4776+
4777+
`h` must be a hierarchy returned by `make-hierarchy`. If `h` is not supplied,
4778+
the global hierarchy will be used."
4779+
([tag parent]
4780+
(alter-var-root #'global-hierarchy underive tag parent))
4781+
([h tag parent]
4782+
(assert (qualified-ident? parent) "Parent must be a qualified keyword or symbol")
4783+
(assert (or (qualified-ident? tag)
4784+
(class? tag))
4785+
"Tag must be a valid Python type or a qualified keyword or symbol")
4786+
4787+
(let [tag-parents (-> (get-in h [:parents tag] #{})
4788+
(disj parent))
4789+
new-parents (if (seq tag-parents)
4790+
(assoc (:parents h) tag tag-parents)
4791+
(dissoc (:parents h) tag))]
4792+
(->> new-parents
4793+
(mapcat (fn [pair]
4794+
(let [tag (first pair)
4795+
parents (second pair)]
4796+
(map #(vector tag %) parents))))
4797+
(reduce (fn [h pair]
4798+
(derive h (first pair) (second pair)))
4799+
(make-hierarchy))))))
4800+
45964801
;;;;;;;;;;;;;;;;;;
45974802
;; Multimethods ;;
45984803
;;;;;;;;;;;;;;;;;;
45994804

4600-
(import* basilisp.lang.multifn)
4601-
46024805
(defmacro defmulti
46034806
"Define a new multimethod with the dispatch function."
46044807
[name & body]

src/basilisp/lang/runtime.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,11 @@ def contains(coll, k):
11101110
return k in coll
11111111

11121112

1113+
@contains.register(type(None))
1114+
def _contains_none(_, __):
1115+
return False
1116+
1117+
11131118
@contains.register(IAssociative)
11141119
def _contains_iassociative(coll, k):
11151120
return coll.contains(k)

tests/basilisp/core_test.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,7 @@ def test_bit_test(self):
10471047

10481048
class TestAssociativeFunctions:
10491049
def test_contains(self):
1050+
assert False is core.contains__Q__(None, "a")
10501051
assert True is core.contains__Q__(lmap.map({"a": 1}), "a")
10511052
assert False is core.contains__Q__(lmap.map({"a": 1}), "b")
10521053
assert True is core.contains__Q__(vec.v(1, 2, 3), 0)
@@ -1056,11 +1057,17 @@ def test_contains(self):
10561057
assert False is core.contains__Q__(vec.v(1, 2, 3), -1)
10571058

10581059
def test_disj(self):
1060+
assert None is core.disj(None)
1061+
assert None is core.disj(None, "a")
1062+
assert None is core.disj(None, "a", "b", "c")
10591063
assert lset.PersistentSet.empty() == core.disj(lset.PersistentSet.empty(), "a")
10601064
assert lset.PersistentSet.empty() == core.disj(lset.s("a"), "a")
10611065
assert lset.s("b", "d") == core.disj(lset.s("a", "b", "c", "d"), "a", "c", "e")
10621066

10631067
def test_dissoc(self):
1068+
assert None is core.dissoc(None)
1069+
assert None is core.dissoc(None, "a")
1070+
assert None is core.dissoc(None, "a", "b", "c")
10641071
assert lmap.PersistentMap.empty() == core.dissoc(lmap.map({"a": 1}), "a", "c")
10651072
assert lmap.map({"a": 1}) == core.dissoc(lmap.map({"a": 1}), "b", "c")
10661073

tests/basilisp/prompt_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def patch_completions(self, completions: Iterable[str]):
6464
"macroexpand",
6565
"macroexpand-1",
6666
"make-array",
67+
"make-hierarchy",
6768
"map",
6869
"map-entry",
6970
"map-entry?",
@@ -89,6 +90,7 @@ def patch_completions(self, completions: Iterable[str]):
8990
"macroexpand",
9091
"macroexpand-1",
9192
"make-array",
93+
"make-hierarchy",
9294
"map",
9395
"map-entry",
9496
"map-entry?",

0 commit comments

Comments
 (0)