; Quick miniKanren-like code ; ; written at the meeting of a Functional Programming Group ; (Toukyou/Shibuya, Apr 29, 2006), as a quick illustration of logic ; programming. The code is really quite trivial and unsophisticated: ; it was written without any preparation whatsoever. The present file ; adds comments and makes minor adjustments. ; ; $Id: sokuza-kanren.scm,v 1.1 2006/05/10 23:12:41 oleg Exp oleg $ ; Point 1: `functions' that can have more (or less) than one result ; ; As known from logic, a binary relation xRy (where x \in X, y \in Y) ; can be represented by a _function_ X -> PowerSet{Y}. As usual in ; computer science, we interpret the set PowerSet{Y} as a multi-set ; (realized as a regular scheme list). Compare with SQL, which likewise ; uses multisets and sequences were sets are properly called for. ; Also compare with Wadler's `representing failure as a list of successes.' ; ; Thus, we represent a 'relation' (aka `non-deterministic function') ; as a regular scheme function that returns a list of possible results. ; Here, we use a regular list rather than a lazy list, just to be quick. ; First, we define two primitive non-deterministic functions; ; one of them yields no result whatsoever for any argument; the other ; merely returns its argument as the sole result. (define (fail x) '()) (define (succeed x) (list x)) ; We build more complex non-deterministic functions by combining ; the existing ones with the help of the following two combinators. ; (disj f1 f2) returns all the results of f1 and all the results of f2. ; (disj f1 f2) returns no results only if neither f1 nor f2 returned ; any. In that sense, it is analogous to the logical disjunction. (define (disj f1 f2) (lambda (x) (append (f1 x) (f2 x)))) ; (conj f1 f2) looks like a `functional composition' of f2 and f1. ; Only (f1 x) may return several results, so we have to apply f2 to ; each of them. ; Obviously (conj fail f) and (conj f fail) are both equivalent to fail: ; they return no results, ever. It that sense, conj is analogous to the ; logical conjunction. (define (conj f1 f2) (lambda (x) (apply append (map f2 (f1 x))))) ; Examples (define (cout . args) (for-each display args)) (define nl #\newline) (cout "test1" nl ((disj (disj fail succeed) (conj (disj (lambda (x) (succeed (+ x 1))) (lambda (x) (succeed (+ x 10)))) (disj succeed succeed))) 100) nl) ; => (100 101 101 110 110) ; Point 2: (Prolog-like) Logic variables ; ; One may think of regular variables as `certain knowledge': they give ; names to definite values. A logic variable then stands for ; `improvable ignorance'. An unbound logic variable represents no ; knowledge at all; in other words, it represents the result of a ; measurement _before_ we have done the measurement. A logic variable ; may be associated with a definite value, like 10. That means ; definite knowledge. A logic variable may be associated with a ; semi-definite value, like (list X) where X is an unbound ; variable. We know something about the original variable: it is ; associated with the list of one element. We can't say though what ; that element is. A logic variable can be associated with another, ; unbound logic variable. In that case, we still don't know what ; precisely the original variable stands for. However, we can say that it ; represents the same thing as the other variable. So, our ; uncertainty is reduced. ; We chose to represent logic variables as vectors: (define (var name) (vector name)) (define var? vector?) ; We implement associations of logic variables and their values ; (aka, _substitutions_) as associative lists of (variable . value) ; pairs. ; One may say that a substitution represents our current knowledge ; of the world. (define empty-subst '()) (define (ext-s var value s) (cons (cons var value) s)) ; Find the value associated with var in substitution s. ; Return var itself if it is unbound. ; In miniKanren, this function is called 'walk' (define (lookup var s) (cond ((not (var? var)) var) ((assq var s) => (lambda (b) (lookup (cdr b) s))) (else var))) ; There are actually two ways of implementing substitutions as ; associative list. ; If the variable x is associated with y and y is associated with 1, ; we could represent this knowledge as ; ((x . 1) (y . 1)) ; It is easy to lookup the value associated with the variable then, ; via a simple assq. OTH, if we have the substitution ((x . y)) ; and we wish to add the association of y to 1, we have ; to make rearrangements so to produce ((x . 1) (y . 1)). ; OTH, we can just record the associations as we learn them, without ; modifying the previous ones. If originally we knew ((x . y)) ; and later we learned that y is associated with 1, we can simply ; prepend the latter association, obtaining ((y . 1) (x . y)). ; So, adding new knowledge becomes fast. The lookup procedure becomes ; more complex though, as we have to chase the chains of variables. ; To obtain the value associated with x in the latter substitution, we ; first lookup x, obtain y (another logic variable), then lookup y ; finally obtaining 1. ; We prefer the latter, incremental way of representing knowledge: ; it is easier to backtrack if we later find out our ; knowledge leads to a contradiction. ; Unification is the process of improving knowledge: or, the process ; of measurement. That measurement may uncover a contradiction though ; (things are not what we thought them to be). To be precise, the ; unification is the statement that two terms are the same. For ; example, unification of 1 and 1 is successful -- 1 is indeed the ; same as 1. That doesn't add however to our knowledge of the world. If ; the logic variable X is associated with 1 in the current ; substitution, the unification of X with 2 yields a contradiction ; (the new measurement is not consistent with the previous ; measurements/hypotheses). Unification of an unbound logic variable ; X and 1 improves our knowledge: the `measurement' found that X is ; actually 1. We record that fact in the new substitution. ; return the new substitution, or #f on contradiction. (define (unify t1 t2 s) (let ((t1 (lookup t1 s)) ; find out what t1 actually is given our knowledge s (t2 (lookup t2 s))); find out what t2 actually is given our knowledge s (cond ((eq? t1 t2) s) ; t1 and t2 are the same; no new knowledge ((var? t1) ; t1 is an unbound variable (ext-s t1 t2 s)) ((var? t2) ; t2 is an unbound variable (ext-s t2 t1 s)) ((and (pair? t1) (pair? t2)) ; if t1 is a pair, so must be t2 (let ((s (unify (car t1) (car t2) s))) (and s (unify (cdr t1) (cdr t2) s)))) ((equal? t1 t2) s) ; t1 and t2 are really the same values (else #f)))) ; define a bunch of logic variables, for convenience (define vx (var 'x)) (define vy (var 'y)) (define vz (var 'z)) (define vq (var 'q)) (cout "test-u1" nl (unify vx vy empty-subst) nl) ; => ((#(x) . #(y))) (cout "test-u2" nl (unify vx 1 (unify vx vy empty-subst)) nl) ; => ((#(y) . 1) (#(x) . #(y))) (cout "test-u3" nl (lookup vy (unify vx 1 (unify vx vy empty-subst))) nl) ; => 1 ; when two variables are associated with each other, ; improving our knowledge about one of them improves the knowledge of the ; other (cout "test-u4" nl (unify (cons vx vy) (cons vy 1) empty-subst) nl) ; => ((#(y) . 1) (#(x) . #(y))) ; exactly the same substitution as in test-u2 ; Part 3: Logic system ; ; Now we can combine non-deterministic functions (Part 1) and ; the representation of knowledge (Part 2) into a logic system. ; We introduce a 'goal' -- a non-deterministic function that takes ; a substitution and produces 0, 1 or more other substitutions (new ; knowledge). In case the goal produces 0 substitutions, we say that the ; goal failed. We will call any result produced by the goal an 'outcome'. ; The functions 'succeed' and 'fail' defined earlier are obviously ; goals. The latter is the failing goal. OTH, 'succeed' is the ; trivial successful goal, a tautology that doesn't improve our ; knowledge of the world. We can now add another primitive goal, the ; result of a `measurement'. The quantum-mechanical connotations of ; `the measurement' must be obvious by now. (define (== t1 t2) (lambda (s) (cond ((unify t1 t2 s) => succeed) (else (fail s))))) ; We also need a way to 'run' a goal, ; to see what knowledge we can obtain starting from sheer ignorance (define (run g) (g empty-subst)) ; We can build more complex goals using lambda-abstractions and previously ; defined combinators, conj and disj. ; For example, we can define the function `choice' such that ; (choice t1 a-list) is a goal that succeeds if t1 is an element of a-list. (define (choice var lst) (if (null? lst) fail (disj (== var (car lst)) (choice var (cdr lst))))) (cout "test choice 1" nl (run (choice 2 '(1 2 3))) nl) ; => (()) success (cout "test choice 2" nl (run (choice 10 '(1 2 3))) nl) ; => () ; empty list of outcomes: 10 is not a member of '(1 2 3) (cout "test choice 3" nl (run (choice vx '(1 2 3))) nl) ; => (((#(x) . 1)) ((#(x) . 2)) ((#(x) . 3))) ; three outcomes ; The name `choice' should evoke The Axiom of Choice... ; Now we can write a very primitive program: find an element that is ; common in two lists: (define (common-el l1 l2) (conj (choice vx l1) (choice vx l2))) (cout "common-el-1" nl (run (common-el '(1 2 3) '(3 4 5))) nl) ; => (((#(x) . 3))) (cout "common-el-2" nl (run (common-el '(1 2 3) '(3 4 1 7))) nl) ; => (((#(x) . 1)) ((#(x) . 3))) ; two elements are in common (cout "common-el-3" nl (run (common-el '(11 2 3) '(13 4 1 7))) nl) ; => () ; nothing in common ; Let us do something a bit more complex (define (conso a b l) (== (cons a b) l)) ; (conso a b l) is a goal that succeeds if in the current state ; of the world, (cons a b) is the same as l. ; That may, at first, sound like the meaning of cons. However, the ; declarative formulation is more powerful, because a, b, or l might ; be logic variables. ; ; By running the goal which includes logic variables we are ; essentially asking the question what the state of the world should ; be so that (cons a b) could be the same as l. (cout "conso-1" nl (run (conso 1 '(2 3) vx)) nl) ; => (((#(x) 1 2 3))) === (((#(x) . (1 2 3)))) (cout "conso-2" nl (run (conso vx vy (list 1 2 3))) nl) ; => (((#(y) 2 3) (#(x) . 1))) ; That looks now like 'cons' in reverse. The answer means that ; if we replace vx with 1 and vy with (2 3), then (cons vx vy) ; will be the same as '(1 2 3) ; Terminology: (conso vx vy '(1 2 3)) is a goal (or, to be more precise, ; an expression that evaluates to a goal). By itself, 'conso' ; is a parameterized goal (or, abstraction over a goal): ; conso === (lambda (x y z) (conso x y z)) ; We will call such an abstraction 'relation'. ; Let us attempt a more complex relation: appendo ; That is, (appendo l1 l2 l3) holds if the list l3 is the ; concatenation of lists l1 and l2. ; The first attempt: (define (apppendo l1 l2 l3) (disj (conj (== l1 '()) (== l2 l3)) ; [] ++ l == l (let ((h (var 'h)) (t (var 't)) ; (h:t) ++ l == h : (t ++ l) (l3p (var 'l3p))) (conj (conso h t l1) (conj (conso h l3p l3) (apppendo t l2 l3p)))))) ; If we run the following, we get into the infinite loop. ; (cout "t1" ; (run (apppendo '(1) '(2) vq)) ; nl) ; It is instructive to analyze why. The reason is that ; (apppendo t l2 l3p) is a function application in Scheme, ; and so the (call-by-value) evaluator tries to find its value first, ; before invoking (conso h t l1). But evaluating (apppendo t l2 l3p) ; will again require the evaluation of (apppendo t1 l21 l3p1), etc. ; So, we have to introduce eta-expansion. Now, the recursive ; call to apppendo gets evaluated only when conj applies ; (lambda (s) ((apppendo t l2 l3p) s)) to each result of (conso h l3p l3). ; If the latter yields '() (no results), then appendo will not be ; invoked. Compare that with the situation above, where appendo would ; have been invoked anyway. (define (apppendo l1 l2 l3) (disj ; In Haskell notation: (conj (== l1 '()) (== l2 l3)) ; [] ++ l == l (let ((h (var 'h)) (t (var 't)) ; (h:t) ++ l == h : (t ++ l) (l3p (var 'l3p))) (conj (conso h t l1) (lambda (s) ((conj (conso h l3p l3) (lambda (s) ((apppendo t l2 l3p) s))) s)))))) (cout "t1" nl (run (apppendo '(1) '(2) vq)) nl) ; => (((#(l3p) 2) (#(q) #(h) . #(l3p)) (#(t)) (#(h) . 1))) ; That all appears to work, but the result is kind of ugly; ; and all the eta-expansion spoils the code. ; To hide the eta-expansion (that is, (lambda (s) ...) forms), ; we have to introduce a bit of syntactic sugar: (define-syntax conj* (syntax-rules () ((conj*) succeed) ((conj* g) g) ((conj* g gs ...) (conj g (lambda (s) ((conj* gs ...) s)))))) ; Incidentally, for disj* we can use a regular function ; (because we represent all the values yielded by a non-deterministic ; function as a regular list rather than a lazy list). All branches ; of disj will be evaluated anyway, in our present model. (define (disj* . gs) (if (null? gs) fail (disj (car gs) (apply disj* (cdr gs))))) ; And so we can re-define appendo as follows. It does look ; quite declarative, as the statement of two equations that ; define what list concatenation is. (define (apppendo l1 l2 l3) (disj ; In Haskell notation: (conj* (== l1 '()) (== l2 l3)) ; [] ++ l == l (let ((h (var 'h)) (t (var 't)) ; (h:t) ++ l == h : (t ++ l) (l3p (var 'l3p))) (conj* (conso h t l1) (conso h l3p l3) (apppendo t l2 l3p))))) ; We also would like to make the result yielded by run more ; pleasant to look at. ; First of all, let us assume that the variable vq (if bound), ; holds the answer to our inquiry. Thus, our new run will try to ; find the value associated with vq in the final substitution. ; However, the found value may itself contain logic variables. ; We would like to replace them, too, with their associated values, ; if any, so the returned value will be more informative. ; We define a more diligent version of lookup, which replaces ; variables with their values even if those variables occur deep ; inside a term. (define (lookup* var s) (let ((v (lookup var s))) (cond ((var? v) v) ; if lookup returned var, it is unbound ((pair? v) (cons (lookup* (car v) s) (lookup* (cdr v) s))) (else v)))) ; We can now redefine run as (define (run g) (map (lambda (s) (lookup* vq s)) (g empty-subst))) ; and we can re-run the test (cout "t1" nl (run (apppendo '(1) '(2) vq)) nl) ; => ((1 2)) (cout "t2" nl (run (apppendo '(1) '(2) '(1))) nl) ; => () ; That is, concatenation of '(1) and '(2) is not the same as '(1) (cout "t3" nl (run (apppendo '(1 2 3) vq '(1 2 3 4 5))) nl) ; => ((4 5)) (cout "t4" nl (run (apppendo vq '(4 5) '(1 2 3 4 5))) nl) ; => ((1 2 3)) (cout "t5" nl (run (apppendo vq vx '(1 2 3 4 5))) nl) ; => (() (1) (1 2) (1 2 3) (1 2 3 4) (1 2 3 4 5)) ; All prefixes of '(1 2 3 4 5) (cout "t6" nl (run (apppendo vx vq '(1 2 3 4 5))) nl) ; => ((1 2 3 4 5) (2 3 4 5) (3 4 5) (4 5) (5) ()) ; All suffixes of '(1 2 3 4 5) (cout "t7" nl (run (let ((x (var 'x)) (y (var 'y))) (conj* (apppendo x y '(1 2 3 4 5)) (== vq (list x y))))) nl) ; => ((() (1 2 3 4 5)) ((1) (2 3 4 5)) ((1 2) (3 4 5)) ; ((1 2 3) (4 5)) ((1 2 3 4) (5)) ((1 2 3 4 5) ())) ; All the ways to split (1 2 3 4 5) into two complementary parts ; For more detail, please see `The Reasoned Schemer'