Skip to content

Instantly share code, notes, and snippets.

@m00nlight
Forked from Pet3ris/sokuza-kanren.scm
Last active August 29, 2015 14:14
Show Gist options
  • Select an option

  • Save m00nlight/252fdc7a70a771bcadd6 to your computer and use it in GitHub Desktop.

Select an option

Save m00nlight/252fdc7a70a771bcadd6 to your computer and use it in GitHub Desktop.

Revisions

  1. @Pet3ris Pet3ris created this gist Dec 22, 2011.
    477 changes: 477 additions & 0 deletions sokuza-kanren.scm
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,477 @@
    ; 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'