I am currently learning miniKanren by The Reasoned Schemer and Racket.
I have three versions of minikanren implementation:
The Reasoned Schemer, First Edition (MIT Press, 2005). I called it
TRS1https://github.com/miniKanren/TheReasonedSchemer
PS. It says that
condihas been replaced by an improved version ofcondewhich performs interleaving.The Reasoned Schemer, Second Edition (MIT Press, 2018). I called it
TRS2https://github.com/TheReasonedSchemer2ndEd/CodeFromTheReasonedSchemer2ndEd
The Reasoned Schemer, First Edition (MIT Press, 2005). I called it
TRS1*
I have did some experiments about the three implementations above:
1st experiment:
TRS1
(run* (r)
(fresh (x y)
(conde
((== 'a x) (conde
((== 'c y) )
((== 'd y))))
((== 'b x) (conde
((== 'e y) )
((== 'f y)))))
(== `(,x ,y) r)))
;; => '((a c) (a d) (b e) (b f))
TRS2
(run* (x y)
(conde
((== 'a x) (conde
((== 'c y) )
((== 'd y))))
((== 'b x) (conde
((== 'e y) )
((== 'f y))))))
;; => '((a c) (a d) (b e) (b f))
TRS1*
(run* (r)
(fresh (x y)
(conde
((== 'a x) (conde
((== 'c y) )
((== 'd y))))
((== 'b x) (conde
((== 'e y) )
((== 'f y)))))
(== `(,x ,y) r)))
;; => '((a c) (b e) (a d) (b f))
Notice that, in the 1st experiment, TRS1 and TRS2 produced the same result, but TRS1* produced a different result.
It seems that the conde in TRS1 and TRS2 use the same search algorithm, but TRS1* use a different algorithm.
2nd experiment:
TRS1
(define listo
(lambda (l)
(conde
((nullo l) succeed)
((pairo l)
(fresh (d)
(cdro l d)
(listo d)))
(else fail))))
(define lolo
(lambda (l)
(conde
((nullo l) succeed)
((fresh (a)
(caro l a)
(listo a))
(fresh (d)
(cdro l d)
(lolo d)))
(else fail))))
(run 5 (x)
(lolo x))
;; => '(() (()) (() ()) (() () ()) (() () () ()))
TRS2
(defrel (listo l)
(conde
((nullo l))
((fresh (d)
(cdro l d)
(listo d)))))
(defrel (lolo l)
(conde
((nullo l))
((fresh (a)
(caro l a)
(listo a))
(fresh (d)
(cdro l d)
(lolo d)))))
(run 5 x
(lolo x))
;; => '(() (()) ((_0)) (() ()) ((_0 _1)))
TRS1*
(define listo
(lambda (l)
(conde
((nullo l) succeed)
((pairo l)
(fresh (d)
(cdro l d)
(listo d)))
(else fail))))
(define lolo
(lambda (l)
(conde
((nullo l) succeed)
((fresh (a)
(caro l a)
(listo a))
(fresh (d)
(cdro l d)
(lolo d)))
(else fail))))
(run 5 (x)
(lolo x))
;; => '(() (()) ((_.0)) (() ()) ((_.0 _.1)))
Notice that, in the 2nd experiment, TRS2 and TRS1* produced the same result, but TRS1 produced a different result.
It seems that the conde in TRS2 and TRS1* use the same search algorithm, but TRS1 use a different algorithm.
These makes me very confusion.
Could someone help me to clarify these different search algorithms in each minikanren implementation above?
Very thanks.
---- ADD A NEW EXPERIMENT ----
3nd experiment:
TRS1
(define (tmp-rel y)
(conde
((== 'c y) )
((tmp-rel-2 y))))
(define (tmp-rel-2 y)
(== 'd y)
(tmp-rel-2 y))
(run 1 (r)
(fresh (x y)
(conde
((== 'a x) (tmp-rel y))
((== 'b x) (conde
((== 'e y) )
((== 'f y)))))
(== `(,x ,y) r)))
;; => '((a c))
However, run 2 or run 3 loops.
If I use condi instead of conde, then run 2 works but run 3 still loop.
TRS2
(defrel (tmp-rel y)
(conde
((== 'c y) )
((tmp-rel-2 y))))
(defrel (tmp-rel-2 y)
(== 'd y)
(tmp-rel-2 y))
(run 3 r
(fresh (x y)
(conde
((== 'a x) (tmp-rel y))
((== 'b x) (conde
((== 'e y) )
((== 'f y)))))
(== `(,x ,y) r)))
;; => '((b e) (b f) (a c))
This is OK, except that the order is not as expected.
Notice that (a c) is at the last now.
TR1*
(define (tmp-rel y)
(conde
((== 'c y) )
((tmp-rel-2 y))))
;;
(define (tmp-rel-2 y)
(== 'd y)
(tmp-rel-2 y))
(run 2 (r)
(fresh (x y)
(conde
((== 'a x) (tmp-rel y))
((== 'b x) (conde
((== 'e y) )
((== 'f y)))))
(== `(,x ,y) r)))
;; => '((a c) (b e))
However, run 3 loops.
After several days of research, I think I have been able to answer this question.
1. Concept clarification
First of all, I'd like to clarify some concepts:
There are two well-known models of non-deterministic computation: the stream model and the two-continuations model. Most of miniKanren implementations use the stream model.
PS. The term "backtracking" generally means depth-first search (DFS), which can be modeled by either the stream model or the two-continuations model. (So when I say "xxx get tried", it doesn't mean that the underlying implementation have to use two-continuations model. It can be implemented by stream model, e.g. minikanren.)
2. Explain the different versions of the
condeorcondi2.1
condeandcondiinTRS1TRS1provides two goal constructors for non-deterministic choice,condeandcondi.condeuses DFS, which be implemented by MonadPlus of stream.The disadvantage of MonadPlus is that it is not fair. When the first alternative offers an infinite number of results, the second alternative is never tried. It making the search incomplete.
To solve this incomplete problem,
TRS1introducedcondiwhich can interleave the two results.The problem of the
condiis that it can’t work well with divergence (I mean dead loop with no value). For example, if the first alternative diverged, the second alternative still cannot be tried.This phenomenon is described in the Frame 6:30 and 6:31 of the book. In some cases you may use
allito rescue, see Frame 6:32, but in general it still can not cover all the diverged cases, see Frame 6:39 or the following case: (PS. All these problems do not exist inTRS2.)Implementation details:
In
TRS1, a stream is a standard stream, i.e. lazy-list.The
condeis implemented bymplus:The
condiis implemented bymplusi2.2
condeinTRS2TRS2removed the above two goal constructors and provided a newconde.The
condelike thecondi, but only interleaving when the first alternative is a return value of a relation which be defined bydefref. So it is actually more like the oldcondeif you won't usedefref.The
condealso fixed the above problem ofcondi.Implementation details:
In
TRS2, a stream is not a standard stream.As the book says that
So in
TRS2, streams are not lazy in every element, but just lazy at suspension points.There is only one place to initially create a suspension, i.e.
defref:This is reasonable because the "only" way to produce infinite results or diverge is recursive relation. It also means that if you use
defineinstead ofdefrelto define a relation, you will encounter the same problem ofcondeinTRS1(It is OK for finite depth-first search).Note that I had to put quotation marks on the "only" because most of the time we will use recursive relations, however you still can produce infinite results or diverge by mixing Scheme's named
let, for example:This diverged because there is no suspension now.
We can work around it by wrapping a suspension manually:
The
condeis implemented byappend-inf:2.3
condeinTRS1*TRS1*originates from the early paper "From Variadic Functions to Variadic Relations A miniKanren Perspective". AsTRS2,TRS1*also removed the two old goal constructors and provided a newconde.The
condelike thecondeinTRS2, but only interleaving when the first alternative itself is aconde.The
condealso fixed the above problem ofcondi.Note that there is no
defrefinTRS1*. Therefore if the recursive relations are not starting fromconde, you will encounter the same problem ofcondiinTRS1. For example,We can work around this problem by wrapping a
condemanually:Implementation details:
In
TRS1*, the stream is the standard stream + suspension.It also means that the named let
loopproblem above does not exist inTRS1*.The
condeis implemented by the interleavingmplus:Note that although the function is named
mplus, it is not a legal MonadPlus because it does not obey MonadPlus law.3. Explain these experiments in the question.
Now I can explain these experiments in the question.
1st experiment
TRS1=>'((a c) (a d) (b e) (b f)), becausecondeinTRS1is DFS.TRS2=>'((a c) (a d) (b e) (b f)), becausecondeinTRS2is DFS if nodefrefinvolved.TRS1*=>'((a c) (b e) (a d) (b f)), becausecondeinTRS1*is interleaving (the outmostcondemake the two innermostcondes interleaving).Note that if we replace
condewithcondiinTRS1, the result will be the same asTRS1*.2nd experiment
TRS1=>'(() (()) (() ()) (() () ()) (() () () ())), becausecondeinTRS1is DFS. The second clause ofcondeinlistois never tried, since when(fresh (d) (cdro l d) (lolo d)isbinded to the first clause ofcondeinlistoit offers an infinite number of results.TRS2=>'(() (()) ((_0)) (() ()) ((_0 _1))), because now the second clause ofcondeinlistocan get tried.listoandlolobeing defined bydefrelmeans that they will potentially create suspensions. Whenappend-infthese two suspensions, each takes a step and then yield control to the other.TRS1*=>'(() (()) ((_.0)) (() ()) ((_.0 _.1)), is the same asTRS2, except that suspensions are created byconde.Note that replacing
condewithcondiinTRS1will not change the result. If you want to get the same result asTRS2orTRS1*, wrapalliat the second clause ofconde.3rd experiment
Note that as @WillNess said in his comment of the question:
Yes, the 3rd experiment about
TRS1andTRS1*has a mistake:Unlike
TRS2,TRS1andTRS1*have no build-indefrel, so thedefineform is from Scheme, not minikaren.We should use a special minikanren form enclosing the two goals.
Therefore,
For
TRS1, we should change the definition toFor
TRS1*, there is noallconstructor, but we can use(fresh (x) ...)to work around itI made this mistake because I was not familiar with minikanren before.
However, this mistake won't affect the final result, and the explanation below for
TRS1andTRS1*are suitable for both the wrong definition and the correct definition.TRS1=>'((a c)), becausecondeinTRS1is DFS. Thetmp-reldiverges attmp-rel-2.Note that replacing
condewithcondiand(run 2 ...), we will get'((a c) (b e)). This becausecondican interleave. However, it still cannot print the third solution(b f)becausecondican’t work well with divergence.TRS2=>'((b e) (b f) (a c)), becauseTRS2can archive complete search if we usedefrelto define relation.Note that the final result is
'((b e) (b f) (a c))instead of'((a c) (b e) (b f))because inTRS2, a suspension only initially be created bydefrel. If we expect'((a c) (b e) (b f)), we can wrap the suspension manually:TRS1*=>'((a c) (b e)), because inTRS1*, suspensions be wrapped atcondes .Note that it still cannot print the third solution
(b f)becausetmp-rel-2does not be wrapped inconde, so no suspension is created here. If we expect'((a c) (b e) (b f)), we can wrap the suspension manually:4. Conclusion
All in all, minikanren is not one language but families of languages. Each minikanren implementation may have its own hack. There may be some corner cases which have slightly different behaviors in different implementations. Fortunately, minikanren is easy to understand. When encountering these corner cases, we can solve them by reading the source code.
5. References
The Reasoned Schemer, First Edition (MIT Press, 2005)
From Variadic Functions to Variadic Relations - A miniKanren Perspective
The Reasoned Schemer, Second Edition (MIT Press, 2018)
µKanren: A Minimal Functional Core for Relational Programming
Backtracking, Interleaving, and Terminating Monad Transformers