μKanren Learnings

Hi, Racket Discourse.

I am busy researching μKanren to see if it would be suited to some tasks at work, and I thought to share some of my observations on the subject.

So, the papers I am reading, are:

The second paper in particular, is quite interesting. It describes a driver for creating encapsulated constraint systems, building off of an "axiomatic" implementation of == (the unification goal).

Basically, each constraint operator is given its own substitution context, which is then recruited to invalidate any new unifications as one tries to solve a given goal.

The text is very easy to follow, although I am curious about how the invalidation rules were generated for the various constraint operators.

Specifically, there is a bit in the text for defining not-pairo which looks like:

(λ (s)
  (ormap (λ (n)
           (let ((t (walk n s)))
             (not (or (not (pair? t)) (var? t)))))
         not-pairo))

where s is the current == substitution context, and n is an element of the substitution context for not-pairo.

This stands out, because of the boolean expression, (not (or (not (pair? t)) (var? t))).

Applying De Morgan's law, we see that it is simply:

(not (or (not (pair? t)) (var? t)))
-> (and (pair? t) (not (var? t)))
; if it isn't a pair, it will fail, irrespective of the second term
-> (pair? t)

Not really a big deal, but I have been scratching my head about why this would be phrased this way, given that it seems to simplify handily. Am I missing something?


I opted to diverge from the paper's implementation somewhat, for fun, by not using natural numbers, nor vectors of a single natural number to represent variables, but instead using opaque structs with no fields.

This made some things easier, but when it came to reifying the results from a particular query, I was left perplexed, because the implementation they provide in the second paper does not impose any particular ordering on the variable and non-variable terms in the pairs which make up the bindings of a substitution context. They are simply recorded as is.

For example, the "raw" result of the following query:

(define-syntax-rule
  (run* (x ...) g₀ g ...)
  (let ([x (var)] ...)
    ;; I use § to mean system (S in the paper)
    (define §* (take-all (call/empty-state (conj* g₀ g ...))))
    (for/list ([§ (in-stream §*)])
      (hash-ref § '≡))))

(define (append-o l s r)
  (cond-o ;; I just use `-o` for everything, but this is technically `cond-e`
    [(≡ null l) (≡ s r)]
    [(fresh (a d r′)
       (≡ (cons a d) l)
       (≡ (cons a r′) r)
       (append-o d s r′))]))

(pretty-print
 (run* (q)
   (append-o '(a b c) '(d e) q)))

'((((d e) . #<var>)
   (() . #<var>)
   ((#<var> . #<var>) . #<var>)
   ((#<var> . #<var>) . #<var>)
   ((#<var> . #<var>) . #<var>)
   ((#<var> . #<var>) . #<var>)
   ((#<var> . #<var>) . #<var>)
   ((#<var> . #<var>) a b c)))

As you can see, the valid substitutions may appear in any order, and are not normalized by default.

Now, because I do not use natural numbers to indicate the ordering of the variables, from least to most recent, I could not employ the method of projection the paper defines.

However, if one thinks about this for a bit, you realize that each of these pairs are in fact unifiable!

That is to say, to impose a projection over them, you can simply unify the pairs, and then do the reification as per normal.

Fortunately, the paper already defines a procedure for doing just this, called valid-==:

 (define (valid-== ==)
   (foldr (λ (pr s) (and s (unify (car pr) (cdr pr) s))) '() ==))

where == is the substitution context for unification, and each pr is an element thereof.

Thus, we can define a projection π which gets everything back into shape:

(define (reify u ¢)
  (let ([v (walk u ¢)])
    (cond
      [(var? v) v]
      [(pair? v)
       (cons (reify (car v) ¢)
             (reify (cdr v) ¢))]
      [else v])))

(define (project §)
  (let ([¢ (valid≡ (hash-ref § '≡))]) ;; I use ≡ to mean ==,
                                      ;; and ¢ to mean substitution context
    (lambda (x) (reify x ¢))))

(define-syntax-rule
  (run* (x ...) g₀ g ...)
  (let ([x (var)] ...)
    (define §* (take-all (call/empty-state (conj* g₀ g ...))))
    (for/list ([§ (in-stream §*)])
      (let ([π (project §)])
        (list (π x) ...)))))

Now, everything makes sense.

(for-each
 displayln
 (run* (q)
   (append-o '(a b c) '(d e) q)))

'((a b c d e))

I haven't thought about whether this could be streamlined earlier in the process, but I thought it was neat that the total number of modifications to the paper's outline was minimal to support the use of non-indexed variables.

Mooiloop.


My implementation isn't great, but if you'd like to take a gander:

My Single-File System

5 Likes

Hi! We should touch base. Shoot me a letter? jason.hemann@gmail.com

3 Likes

Oh, this subject is so interesting.

Reading some more, I have implemented a rudimentary form of eigen variables.

For the interested reader, I am looking at:

The former is referenced by the latter.

To the point, an eigen variable is similar to a variable made with fresh except that it can only unify with itself, or a fresh variable created within its scope.

This will succeed:

(eigen (𝕩)
  (fresh (x)
    (≡ x 𝕩)))

But this will fail:

(fresh (x)
  (eigen (𝕩)
    (≡ x 𝕩)))

To demonstrate, say we want to distinguish a natural number greater than two:

;; fail
(run* ()
  (eigen (𝕩)
    (fresh (x)
      (≡ x `(s (s ,𝕩)))
      (≡ x '(s (z)))))) ;; (s (z)) is one, i.e. less than two, so it cannot unify

Similarly, using disequality, we can ask for a number less than two:

;; okay
(run* ()
  (eigen (𝕩)
    (fresh (x)
      (≢ x `(s (s ,𝕩)))
      (≡ x '(s (z))))))

Because eigen variables can reasonably be thought of as a "tree" of values, given that one has to keep track of their nested scope relative to fresh variables, it is convenient to define a wildcard variable, _, at the root of the tree, which can then unify with any fresh variable.

;; okay
(run* (x) (≡ x _))

;; fail
(run* (x) (≢ x _))

Next on my reading list, is Constructive Negation and probably Implication along with it.

Not that I understand the math of it all, but being able to mess around with the concepts goes a long way.

The combinators from this paper are super intriguing (pg. 5 onward), but I will need to shift gears to think in this mode.


Another avenue to consider, is typed variables! It ought to be simple to implement with the variables as structs formulation I am using, although I haven't considered the intricacies yet.

Ah, I love macros! I mean, they don't always love me back, but that's neither here nor there.

I have been thinking about how to "optimize" the pipeline from kanren-forms to streams.

One part of this, is "delaying" goals which may expand (and cause premature execution), before actually being used.

A simple way to do this, is to make sure that all the provided forms, like fresh, condᵉ, disj*, conj*, and so forth, wrap their arguments in await calls, which look like:

(define-syntax-rule
  (await g) (lambda (§) (pause (g §))))

where pause is an alias for stream-lazy.

This is perfectly serviceable, although it does cause inordinate amounts of useless wrapping, due to the fact that there is no way for the system to know what a goal looks like, before actually expanding it.

Easy fix (tongue in cheek), we make a goal syntax-class:

;; syntax-parameter wrapper
(define-syntax-parameter/slot stable-forms
  #:get unbox
  #:set set-box!
  #:new box null)

...

(define-syntax-class goal
  #:attributes (paused)
  #:literals (await)
    
  (pattern (await goal)
    #:with :goal (local-expand #'goal 'expression {the-stable-forms}))
    
  (pattern (stable . _)
    #:when (member #'stable {the-stable-forms} free-identifier=?)
    #:attr paused this-syntax)
    
  (pattern goal
    #:attr paused #'(await goal)))

The stable forms then use this goal class, like so:

(define-syntax (disj* stx)
  (syntax-parse stx
    [(_) #'failᵒ]
    [(_ :goal) #'paused]
    [(_ :goal g ...)
     #'(disj paused (disj* g ...))]))

I have not yet discovered the "caveats" of using things like local-expand but it does the job for now.

The the-stable-forms expression is the content of a box held by the syntax-parameter, stable-forms. These forms are added to when one used a defrel or defmatch macro, to include the relation as "stable".

(define-syntax-rule
  (defrel (name . args) g₀ g ...)
  (begin
    (define-stable-forms name)
    (define (name . args)
      (define-exclude-form name
        (conj* g₀ g ...)))))

Note the use of define-exclude-form, which is there to ensure runaway recursion doesn't happen.

Consider the definition,

(defrel (fivesᵒ x)
  (disj* (≡ x 5) (fivesᵒ x)))

($-each
 displayln
 (run 4 (x) (fivesᵒ x)))

Without the define-exclude-form the inside of the defrel will think that fivesᵒ is already stable and not wrap it at all, creating a stream that never matures.

This way, we can make sure our recursive relations are stable, too.

Anyways, I was just chuffed at using local-expand spontaneously for the first time.

"Optimized" :joy:

(time
 ($-each
  display-numerals
  (run 5 (x)
    (fresh (y z)
      (oddᵒ   y)
      (timesᵒ y y z)
      (timesᵒ y z x)))))
(1)
(27)
(125)
(343)
(729)
cpu time: 581328 real time: 591855 gc time: 23359

This has me thinking, what is a "proof" in kanren?

What I mean, is that in some sense, the substitution context produced as the result of a call like (timesᵒ y y z) is a proof that y^2 = z, right? If you substitute u for y and v for z everywhere in said context, it is a statement of proof (possibly invalidated by the substitution) of u^2 = v.

But how does one "canonize" a proof. The computations above take a very long time, because the intermediate work is repeated--is there a way to save the resulting substitution context to obviate unnecessary repetition?

1 Like

Holy smokes, 100x speedup using hash-tables instead of lists, and only about a doubling of memory (at a glance):

(1)
(27)
(125)
(343)
(729)
cpu time: 5671 real time: 5698 gc time: 718

^ I am terrible at heuristics.


Actually memory usage is similar, if somewhat more.

Using hasheq tables, cuts the time down by another factor of ~4:

(1)
(27)
(125)
(343)
(729)
cpu time: 1234 real time: 1228 gc time: 328

I initially got eigen a bit wrong, but I've reached a closer approximation by now.

One interesting thing you can do with eigen, is generate restricted patterns.

I have a macro called cartesianᵒ, which constructs the cartesian product of its rows, so that:

($-each
 (compose1 displayln list)
 (run* (x y)
   ((cartesianᵒ
     [1 2 3]
     [4 5 6])
    x y)))

shows

(1 4)
(2 4)
(1 5)
...
(3 5)
(2 6)
(3 6)

Basically, it is a conjunction of disjunctions (product of sums), or just (conj* (disj* ...) ...) for short.

Now, suppose we have the following code:

(defrel 3-things
  (cartesianᵒ
   [1 2 3]
   [1 2 3]
   [1 2 3]))

($-each
 (compose1 displayln list)
 (run* (x y z)
   (3-things x y z)))

This produces:

(1 1 1)
(2 1 1)
(1 2 1)
(3 1 1)
...
(2 2 3)
(3 2 3)
(2 3 3)
(3 3 3)

which is all possible 3-combinations of the items in each row.

But what if we wanted to restrict the result to unique permutations of the values 1,2,3?

This is where eigen comes in. We recall that

eigen can only unify with itself, or a fresh variable created within its scope

We can exploit this behaviour to restrict unification to only certain cases, e.g.

(defrel (3-things _₁ _₂ _₃)
  (cartesianᵒ
   [`(1 ,_₁) `(2 ,_₂) `(3 ,_₃)]
   [`(1 ,_₁) `(2 ,_₂) `(3 ,_₃)]
   [`(1 ,_₁) `(2 ,_₂) `(3 ,_₃)]))

($-each
 (compose1 displayln list)
 (run* (x y z)
   (eigen (𝕩 𝕪 𝕫)
     (fresh (u v w)
       ((3-things 𝕩 𝕪 𝕫) u v w)
       (matchᵉ (u v w)
         [{(,a ,m) (,b ,n) (,c ,o)}
          (≢ m n)
          (≢ m o)
          (≢ n o)
          (≡ (list x y z)
             (list a b c))])))))

which shows

(3 2 1)
(3 1 2)
(1 3 2)
(2 3 1)
(1 2 3)
(2 1 3)

If we only wanted a 3-repetition of a single value each time, we might say

($-each
 (compose1 displayln list)
 (run* (x y z)
   (eigen (𝕩 𝕪 𝕫)
     (fresh (u v w)
       ((3-things 𝕩 𝕪 𝕫) u v w)
       (matchᵉ (u v w)
         [{(,a ,m) (,b ,m) (,c ,m)} ;; note that we say m is the same everywhere
          (≡ (list x y z)
             (list a b c))])))))

to produce

(1 1 1)
(2 2 2)
(3 3 3)

Similarly, we can restrict the results to include two of the same value each time, like so

($-each
 (compose1 displayln list)
 (run* (x y z)
   (eigen (𝕩 𝕪 𝕫)
     (fresh (u v w)
       ((3-things 𝕩 𝕪 𝕫) u v w)
       (matchᵉ (u v w)
         [{(,a ,m) (,b ,n) (,c ,o)}
          (disj*
           (conj₂ (≡ m n) (≢ m o))
           (conj₂ (≡ m o) (≢ m n))
           (conj₂ (≡ n o) (≢ n m)))
          (≡ (list x y z)
             (list a b c))])))))

to produce

(2 1 1)
(1 2 1)
(3 1 1)
(1 1 2)
...
(3 3 2)
(2 2 3)
(3 2 3)
(2 3 3)

or even at most two repetitions per result (although not unique),

($-each
 (compose1 displayln list)
 (run* (x y z)
   (eigen (𝕩 𝕪 𝕫)
     (fresh (u v w)
       ((3-things 𝕩 𝕪 𝕫) u v w)
       (matchᵉ (u v w)
         [{(,a ,m) (,b ,n) (,c ,o)}
          (disj*
           (conj₂ (≢ m n) (≢ m o))
           (conj₂ (≢ n m) (≢ n o))
           (conj₂ (≢ o m) (≢ n o)))
          (≡ (list x y z)
             (list a b c))])))))

which shows

(2 1 1)
(1 2 1)
(3 1 1)
(1 1 2)
...
(2 1 3)
(3 1 3)
(2 1 3)
(3 2 2)
(2 1 3)
...
(2 2 3)
(3 2 3)
(2 3 3)

Pretty neat.