Seite an Seite mit miniKanren
Mein core.logic-Fork, Teil 8 von 9. Übersicht · ← Sets · Ein relationaler Clojure-Interpreter →
faster-miniKanren ist das meistgenutzte miniKanren: Es treibt die relationalen Interpreter hinter den Arbeiten zu Quines und Programmsynthese (Barliman) an und enthält die Arithmetik aus The Reasoned Schemer. Dieser Post stellt zwei Fragen:
- Wo core.logic und miniKanren sich überschneiden: Geben sie dieselben Antworten?
- Wo dieser Fork weiter geht (Maps, Vektoren, Sets): Wie sieht das miniKanren-Programm aus, und was antwortet es?
Jede Ausgabe unten stammt aus echten Läufen: miniKanren mit faster-miniKanren unter Racket, core.logic auf der JVM.
miniKanrens eigene Tests ausführen
script/fmk_tests.py übersetzt die Testsuite von faster-miniKanren nach
core.logic: 355 der 357 Tests (zwei berechnen ihre erwartete Antwort und lassen
sich nicht übersetzen). Sie decken ==, =/=, symbolo, numbero, stringo,
absento, Typinferenz und die Arithmetik aus The Reasoned Schemer ab. Die
beiden Systeme drucken Antworten unterschiedlich, deshalb werden beide Seiten
vor dem Vergleich normalisiert (_.0 und _0, =/= und !=,
Variablennummerierung, Reihenfolge der Constraints).
Beim ersten Lauf stimmten 202 von 355 überein. Der Rest ging auf sieben Bugs und ein fehlendes Feature zurück.
| Bug | Seit |
|---|---|
!= zwischen zwei unechten Listen warf eine Exception. |
upstream |
(!= (lcons 1 x) [1 2]) sagte "immer verschieden" und verlor x ≠ (2). |
upstream |
(!= [q q] [p r]) behielt nur q ≠ r: Die zweite Paarung von q überschrieb die erste. Dadurch wurde q=1, p=2, r=1 fälschlich abgelehnt. |
upstream |
Die erneute Prüfung von != nutzte = auf Listen, das Logikvariablen als Wildcards behandelt. (!= q (a . b)) gefolgt von q = (3 . 4) schlug fehl. |
upstream |
Eine Variable mit Constraints löst sich zu einer Kopie ihrer selbst auf; != verglich per Identität und speicherte das unmögliche a ≠ a. |
upstream |
Die Variablen in verschachtelten unechten Listen wurden nicht beobachtet, also wachte != auf ihnen nie auf. |
upstream |
absento wartete darauf, dass sein erstes Argument ground ist, also war (absento q q) erfolgreich. |
dieser Fork |
Alle außer dem ersten geben falsche Antworten: entweder Antworten, die keine Lösungen sind, oder fehlende Lösungen. Alle sind behoben, und jeder hat seinen eigenen Regressionstest.
Das fehlende Feature war das Aufräumen der Antworten. faster-miniKanren lässt
Constraints weg, die für die Antwort keine Rolle spielen können. Das ist jeder
Constraint auf einer Variablen, die die Antwort nicht zeigt, x ≠ 5, wenn x
ein Symbol ist, x ≠ y, wenn (absento x y) da ist, und so weiter. core.logic
druckte sie alle. Jetzt vereinfacht es auf dieselbe Weise:
;; miniKanren (run* (q) (fresh (x) (== q `(,x)) (symbolo x) (=/= x 'lambda))) ;;=> (((_.0) (=/= ((_.0 lambda))) (sym _.0)))
;; core.logic (run* [q] (fresh [x] (== q [x]) (symbolo x) (!= x 'lambda))) ;;=> (([_0] :- (!= (_0 lambda)) (clojure.core.logic/symbolo _0)))
Jetzt stimmen 352 von 355 überein, und alle laufen mit lein test, in etwa
drei Sekunden. Die drei abweichenden Tests werden übersprungen und sind im
Skript dokumentiert:
- Suchreihenfolge: Einer ist
run 12auf einem unendlichen Stream, wo eine andere Suchreihenfolge andere erste zwölf liefert. - Äquivalente Form: Einer druckt denselben Constraint in einer anderen
Normalform:
(!= (_0 _1) (_1 _2)), wo miniKanren((_.0 _.1) (_.0 _.2))schreibt. - Redundanter Constraint: Einer zeigt einen zusätzlichen Constraint, den eine Ungleichung schon impliziert.
Dasselbe Programm gibt dieselben Antworten
Programme, die nur Listen nutzen, lassen sich Zeile für Zeile übersetzen:
;; miniKanren (defrel (appendo l s out) (conde [(== '() l) (== s out)] [(fresh (a d res) (== `(,a . ,d) l) (== `(,a . ,res) out) (appendo d s res))])) (run* (x y) (appendo x y '(1 2 3))) ;;=> ((() (1 2 3)) ((1) (2 3)) ((1 2) (3)) ((1 2 3) ()))
;; core.logic, in a namespace that excludes core.logic's own appendo (defrel appendo [l s out] (conde [(== '() l) (== s out)] [(fresh [a d res] (== (lcons a d) l) (== (lcons a res) out) (appendo d s res))])) (run* [x y] (appendo x y '(1 2 3))) ;;=> ([() (1 2 3)] [(1) (2 3)] [(1 2) (3)] [(1 2 3) ()])
Der Rest dieses Posts handelt von Daten, die miniKanren als Listen kodieren muss.
Maps: Assoziationslisten gegen geto
miniKanren hat keine Maps. Die übliche Kodierung ist eine Assoziationsliste, wie in der Umgebung des relationalen Interpreters:
(defrel (lookupo k m v)
(fresh (k2 v2 rest)
(== `((,k2 . ,v2) . ,rest) m)
(conde
[(== k k2) (== v v2)]
[(=/= k k2) (lookupo k rest v)])))
Einen Schlüssel nachschlagen funktioniert in beiden gleich, vorwärts und rückwärts:
(run* (k) (lookupo k '((a . 1) (b . 2)) 2)) ;;=> (b)
(run* [k] (geto {:a 1 :b 2} k 2)) ;;=> (:b)
Bei der Frage nach einer Map mit zwei Einträgen trennen sich die Wege. Eine Map hat unendlich viele Assoziationslisten: jede Reihenfolge, beliebige andere Schlüssel dazwischen. Also zählt miniKanren sie auf, ohne Ende:
(run 4 (m) (lookupo 'a m 1) (lookupo 'b m 2)) ;;=> (((a . 1) (b . 2) . _.0) ;; ((b . 2) (a . 1) . _.0) ;; (((a . 1) (_.0 . _.1) (b . 2) . _.2) (=/= ((_.0 b)))) ;; (((a . 1) (_.0 . _.1) (_.2 . _.3) (b . 2) . _.4) (=/= ((_.0 b)) ((_.2 b))))) ;; run* never returns
core.logic antwortet einmal, mit einer offenen Map, und hört auf:
(run* [m] (geto m :a 1) (geto m :b 2)) ;;=> ((lmap {:b 2, :a 1} | _0))
Gleichheit ist der andere Unterschied. Zwei Assoziationslisten in verschiedener Reihenfolge unifizieren nicht, obwohl sie dieselbe Map bedeuten:
(run* (q) (== '((a . 1) (b . 2)) '((b . 2) (a . 1)))) ;;=> ()
(run* [q] (== {:a 1 :b 2} {:b 2 :a 1})) ;;=> (_0)
(run* [x k] (== {:a x :b 2} {k 2 :a 1})) ;;=> ([1 :b])
Sets: Listen gegen containso
Bei Sets wiederholt sich das Muster. Mit Listen und membero ist jede
Reihenfolge und jedes Auffüllen eine weitere Antwort:
(run 4 (s) (membero 1 s) (membero 2 s)) ;;=> ((1 2 . _.0) (2 1 . _.0) (1 _.0 2 . _.1) (1 _.0 _.1 2 . _.2))
(run* [s] (containso s 1) (containso s 2)) ;;=> ((lset #{1 2} | _0))
Set-Gleichheit und Mengenalgebra laufen in jede Richtung:
(run* [x y] (== #{x y} #{1 2})) ;;=> ([1 2] [2 1])
(count (run* [a b] (uniono a b #{1 2}))) ;;=> 9, each element in a, b or both
Vektoren: appendo gegen concato
appendo schaut nur auf Elemente. Wenn beide Listen unbekannt sind, erzeugt es
Längen eine nach der anderen, und wenn die Antwort von Längen abhängt, merkt es
das nie:
(run 1 (x) (appendo x '(1) x)) ;; no answer within 20 seconds: it tries ever longer x (run 1 (x) (fresh (l r) (appendo x '(1) l) (appendo '(1) x r) (appendo r '(2) l))) ;; no answer within 20 seconds
Die Vektoren des Forks lösen zuerst die Längen (Teil 4), also schlagen beide sofort fehl:
(run* [x] (concato x [1] x)) ;;=> () ;; |x| + 1 ≠ |x| (run* [x] (fresh [l r] (concato x [1] l) (concato [1] x r) (concato r [2] l))) ;;=> () ;; |x| + 1 ≠ |x| + 2
Wo es unendlich viele Antworten gibt, zählen beide sie auf:
x ++ (1) = (1) ++ x ergibt (() (1) (1 1)) für run 3 in faster-miniKanren
und ([] [1] [1 1]) im Fork.
Längenschließen macht nicht jede Wortgleichung endlich. Wenn die Längen
übereinstimmen, wie bei x ++ [1] = [2] ++ x, zeigen erst die Elemente den
Konflikt, und auch der Fork zählt ohne Ende auf (Teil 5).
Indizierung und Pfade haben kein direktes Gegenstück in miniKanren. Sie bräuchten Peano-Zahlen und eine Kette von Lookups:
(run* [i] (ntho [:a :b :a] i :a)) ;;=> (0 2) (run* [p] (get-ino {:users [{:name "ada"} {:name "bob"}]} p "bob")) ;;=> ((:users 1 :name))
Was noch anders ist
- Notation:
_0für_.0,!=für=/=,symbolofürsym, und Antworten als(value :- constraints). - Suchreihenfolge:
conde,freshunddefrelverschränken anders, also kannrun nandere erstenAntworten liefern.run*liefert dieselbe Menge. - ClojureScript: Nichts davon läuft dort bisher; siehe Future Work.
Generiert von Claude im Auftrag von Benjamin Schwerdtner.