Vectors
My core.logic fork, part 4 of 9. Index · ← Making it fast · Where this sits: the literature, and parity with miniKanren →
After maps, vectors. Same rule as for maps: every relation correct in every mode, or waiting visibly.
What upstream core.logic does
Vectors, lists and partial lists already unify with each other as sequences.
(== [a b] (lcons 1 r)) works. What's missing is everything that makes a
vector a vector:
- The only partly known sequence is a known front plus an unknown tail,
(1 2 . _0). Nothing can be known about the end of a sequence whose length is unknown. - Answers built from partial lists come back as lists, not vectors.
- No relations for positions (
nth), length (count), the end (peek,pop), slices (subvec) or concatenation that know about vectors. Upstream'sconjois a constraint that waits until two of its arguments are ground.
The term: front, middle, back
A partly known vector is a known front, an unknown middle and a known back. In answers it prints as
(lvec [a b] | _0 | [y z])
The middle is an unbound variable standing for a vector of any length,
including zero. A partial list (a b . t) is the special case with an empty
back.
Why the back: Clojure vectors grow at the end. conj, peek and pop act on
the last element. With only a front and a tail, every one of those has to
search over the unknown length. With a back, they're direct:
(conjo v x out) ;; out is [f | m | b x]: no search (peeko v x) ;; the last of b, if b isn't empty
When b is empty, peeko has two cases: the middle is not empty (then
m = [| m' | x], one general answer), or it is empty and x is the last of the
front. Finitely many branches, never an unbounded search.
It's the same idea as Prolog's difference lists, which make the end of a list reachable, extended to both ends.
Unification
Unifying [f1 | m1 | b1] with [f2 | m2 | b2]:
- Unify the fronts element by element, as far as both go. Same for the backs, from the end.
- What's left has at most one side with a front left over, and at most one with a back left over.
- If one side has nothing left over, its middle is the other side: a single binding.
- Otherwise it's
m1 ++ B = F ++ m2: the two middles overlap in an unknown way. There are at most|F| + 1cases:m1covers all ofF(one general answer with a fresh middle), or ends insideFat a place where the rest ofFfits intoB.
With two different middles, case 4 is finite, so it uses the same mechanism as map unification with unknown keys: try it, commit if exactly one case survives, otherwise keep it pending, and enumerate only before reifying.
With the same middle on both sides it is not. [| x | 1] = [2 | x |],
that is x ++ [1] = [2] ++ x, has no solution, but every length passes the
length check (|x| + 1 on both sides), so enumerating never ends. This is a
genuine word equation. The relation can list every answer, but it can't
always fail in finite time.
(Part 5 has the theory.)
The full problem, any number of unknown pieces concatenated, is solving word
equations. That's decidable but very hard, which is why the term has exactly
one unknown part. Concatenation of two open vectors (concato) is a relation
that waits, not a term.
Lengths and positions
Every open vector has a length, and lengths are reasoned about exactly,
before anything is enumerated. A length is a constant plus a sum of the
lengths of unknown middles. concato posts |out| = |a| + |b|, unifying two
vectors equates their lengths, and these linear equations are solved by
elimination, with every length at least 0. Impossible length systems then fail
at once: x ++ [1] = [1] ++ x ++ [2] fails on |x| + 1 ≠ |x| + 2 without
trying a single length. (Part 5 is why lengths
get this treatment.)
Why not core.logic's FD solver? It narrows intervals one step at a time.
Lengths have no upper bound, and on |x| + 1 = |x| + 2 with lengths up to
10,000 it overflows the stack before it notices the contradiction. FD
variables still work with counto: a length relation waits until an FD
variable is labeled, and FD labeling then enumerates only its domain.
When a length or position is still unknown at the end, relations enumerate:
(run 3 [i v] (ntho v i :x)) ;;=> ([0 (lvec [:x] | _0 | [])] ;; [1 (lvec [_0 :x] | _1 | [])] ;; [2 (lvec [_0 _1 :x] | _2 | [])])
The middle stays open in each answer: nothing ties its length to anything. On
an open vector that's infinitely many answers, which is what membero already
does on a partial list. With a finite FD domain, only that domain is
enumerated:
(run* [v n] (fd/in n (fd/interval 1 2)) (counto v n) (ntho v 0 :a)) ;;=> ([[:a] 1] [[:a _0] 2])
A known length closes the vector, filling the middle with fresh variables:
(run* [q] (fresh [v] (ntho v 0 :a) (ntho v 2 :c) (counto v 3) (== q v))) ;;=> ([:a _0 :c])
And lengths that can't match fail before any enumeration. Here
|a| = |x| + 1 = |b|, but also |a| = |b| + 1:
(run* [q] (fresh [x a b]
(concato x [1] a) (concato [1] x b) (concato b [2] a)
(== q 1)))
;;=> ()
Relations
| Relation | Means |
|---|---|
(ntho v i x) |
(nth v i) is x |
(counto c n) |
(count c) is n, for vectors and maps |
(conjo c x out) |
out is (conj c x): at the back of a vector, the front of a list |
(peeko c x) / (popo c out) |
(peek c) / (pop c), at the same end as conj |
(subveco v start end out) |
out is (subvec v start end) |
(concato a b out) |
out is the elements of a, then those of b |
(geto v i x), (assoco v i x out) |
as get/assoc on vectors |
(get-ino m ks v), (assoc-ino m ks v out) |
get-in/assoc-in, through maps and vectors |
(run* [a b] (concato a b [1 2 3])) ;;=> ([[] [1 2 3]] [[1] [2 3]] [[1 2] [3]] [[1 2 3] []]) (run* [v] (assoco v 1 :x [1 :x 3])) ;;=> ([1 _0 3]) ;; v had anything at index 1
geto and assoco work on both maps and vectors, like get and assoc in
Clojure, so paths through nested data work:
(run* [x] (fresh [u e]
(geto {:users [{:name "a"} {:name "b"}]} :users u)
(geto u 1 e)
(geto e :name x)))
;;=> ("b")
When it's not yet known whether something is a map or a vector and the key is an index, the relation waits, and tries both before the answer comes out:
(run* [m] (geto m 0 1)) ;;=> ((lmap {0 1} | _0) (lvec [1] | _0 | []))
Paths: get-ino and assoc-ino
That chain of geto=s is =get-in, so it has a name. (get-ino m ks v) says
(get-in m ks) is v, through any mix of maps and vectors, and the path is
just another argument. Take some data:
(def db {:users [{:name "ada" :age 36} {:name "bob" :age 40}] :admin {:name "ada" :age 36}})
Forwards it is get-in. With the path unknown, it finds where a value is:
(run* [x] (get-ino db [:users 1 :name] x)) ;;=> ("bob") (run* [p] (get-ino db p "ada")) ;;=> ((:admin :name) (:users 0 :name))
Part of a path can be unknown, and the value can have unknown parts:
(run* [i n] (get-ino db [:users i :name] n)) ;;=> ([0 "ada"] [1 "bob"]) (run* [p a] (get-ino db p {:name "ada" :age a})) ;;=> ([(:admin) 36] [(:users 0) 36]) ;; every map that is ada, and where
A map literal unifies exactly: {:name "ada" :age a} finds maps with those
two keys and no others. To ask for at least some keys, matcho takes a
pattern: a map in it matches a map with at least its keys, a set a set with
at least its elements, a vector a sequence of its length, all nested. With
get-ino that's a small query language:
(run* [p n] (fresh [u] (get-ino db p u) (matcho u {:age 40 :name n})))
;;=> ([(:users 1) "bob"]) ;; anything aged 40, whatever its other keys
(run* [p] (fresh [u] (get-ino db p u) (matcho u {:name "ada"})))
;;=> ((:admin) (:users 0))
(run* [a b] (matcho db {:users [{:name a} {:name b}]}))
;;=> (["ada" "bob"])
matcho runs backwards too: with x unknown, (matcho x {:a {:b 1}})
answers (lmap {:a (lmap {:b 1} | _0)} | _1), the open map the pattern
describes. The pattern itself is Clojure data, read when the goal is built,
so a logic var in it stands for a value, not for a sub-pattern. It
generalizes featurec, which waits until the map is known instead of
building it, and looks into nested maps but not into vectors or sets.
With the data unknown, it says what the data must look like, and doesn't
decide whether :users is a map with key 0 or a vector:
(run 2 [d] (get-ino d [:users 0 :name] "bob")) ;;=> ((lmap {:users (lmap {0 (lmap {:name "bob"} | _0)} | _1)} | _2) ;; (lmap {:users (lvec [(lmap {:name "bob"} | _0)] | _1 | [])} | _2))
assoc-ino is assoc-in. Backwards, given the data before and after, it
finds the path that changed, and given only the result, every value
assoc-in could have started from:
(run* [out] (assoc-ino db [:users 1 :age] 41 out)) ;;=> ({:admin {:name "ada", :age 36}, :users [{:name "ada", :age 36} {:name "bob", :age 41}]}) (run* [p] (assoc-ino db p 37 (assoc-in db [:admin :age] 37))) ;;=> ((:admin :age)) (run* [m] (assoc-ino m [:a :b] 1 {:a {:b 1}})) ;;=> (nil {} {:a nil} {:a {}} {:a {:b _0}})
That last one is Clojure's assoc-in read backwards: nil counts as an
empty map, and missing levels are created, so all five start values give
{:a {:b 1}}.
Vectors and lists
(= [1 2] '(1 2)) is true in Clojure, and core.logic already unifies them.
That stays. Answers built by the vector relations come back as vectors.
Where it waits, and where it can't finish
- Unknown kind.
conj,peekandpopwork at different ends of vectors and lists, andcountof an unknown map has no finite answer. Soconjo,peeko,popoandcountoon a collection whose kind is still unknown wait, and appear in the answer as constraints, e.g.(_0 :- (clojure.core.logic/counto _0 3)). - Infinite answers.
nthowith an unknown index, orcountowith an unknown length, on an open vector has infinitely many answers. Userun n. Positions are enumerated fairly: answer k covers position k in the middle and every position in the back that a middle of matching length allows. - Word equations. A middle with elements around it equated with itself
with other elements around it,
x ++ [1] = [2] ++ x, is enumerated by the length ofx. It finds every answer, but when there is none it does not stop.
Next: Literature and miniKanren parity.
Generated by Claude on Benjamin Schwerdtner's behalf.