2 First Edition
| (require typed/miniKanren/trs1) | package: miniKanren-typed |
This module implements the miniKanren language used in the first edition of The Reasoned Schemer (Friedman, Byrd, and Kiselyov, MIT Press, 2005). It keeps the operators of that edition, including the interleaving forms condi and alli and the committed-choice forms conda and condu. The types make the structure of the implementation visible: terms, substitutions, goals, and the lazy streams that carry answers all have names, and every internal operator is exported so that it can be inspected and tested in isolation.
2.1 Implementation Basis
The implementation follows the one described in the first edition: unification and reification are those of Chapter 9 (“Under the Hood”), and the stream operators and goal constructors are those of the appendix “Connecting the Wires”. The second edition is a different language (see Second Edition and Comparing the Editions).
The differences from the first edition are:
Stream representation. The book uses #f for the empty stream and the answer itself for exactly one answer. Here the empty stream is '() and a stream of exactly one answer is a one-element list, which supports arbitrary answer types. This preserves the book’s suspension points and search strategies. See Streamof for the two representations and the book’s restrictions on answers. Streams are accessed only through a small set of operations (stream-first, stream-rest, and so on; see Streams). The book’s case∞ is defined in terms of them, and its mzero, unit, and choice are written empty-stream, (stream a), and (stream-cons a e), where e is the rest itself rather than a thunk that produces it.
Occurs check. The book’s ext-s never checks for circularity; a separate checked variant is introduced in Chapter 9. Here ext-s always performs the occurs check (skipping it when the value contains no variables), so == is the checked unification and there is no separate checked variant.
Spelling. succeed and fail stand for the book’s #s and #u; λG and λF are typed versions of the book’s goal and thunk lambdas. Superscripts become suffixes: conde, condi, alli, ife, and so on.
Omissions. The helper forms that the book introduces in passing to explain a point (such as the bounded lambda of Chapter 5 and project of Chapter 9) are not part of the library.
Throughout this section, “Chapter n” refers to the first edition.
2.2 Types
type
type
type
type
type constructor
(Streamof a)
type constructor
(Suspension a)
(define-type (Streamof a) (∪ Null (Pair a (∪ Null (Suspension a))))) (define-type (Suspension a) (→ (Streamof a)))
A stream is empty (Null), or an answer followed by its rest. The rest is either known to be empty, so that the stream is the one-element list (list a), or a suspension that produces the rest when called. The three cases are built by empty-stream, stream, and stream-cons. A suspension is a thunk that produces a stream when called.
The book uses a different representation: #f for the empty stream, the answer itself for exactly one answer, and an answer paired with a suspension for the remaining stream. Expressed in Typed Racket notation, this is:
(define-type (Streamof a) (∪ False a (Pair a (Suspension a))))
The book requires that an answer be neither #f nor a pair whose cdr is a procedure, so that the three cases do not overlap. Substitutions satisfy this restriction.
The Streamof defined above has no such restriction: the three cases are told apart by null? on the stream and on its cdr, so Typed Racket can distinguish them for any answer type without inspecting the answers.
Read element by element, a stream is a list whose answers are all computed when they are reached and whose rest is delayed: the rest of a stream-cons is produced by a suspension, and only a (stream a) is known to end without calling one. The operations of Streams present it in exactly these terms.
type
2.3 Running Programs
Unassociated variables in an answer are reified as the symbols '.0, '.1, and so on, in order of first appearance.
2.4 Goals
> (run* (q) (fresh (x) (== q (cons x 2)) (== x 1))) - : (Listof Term)
'((1 . 2))
> (run* (q) (== q (list q))) - : (Listof Term)
'()
syntax
(fresh (x ...) goal ...)
all explores the answers of the first goal one at a time: for the first answer it exhausts the rest of the conjunction before moving to the second answer. alli (“all, interleaved”) alternates between the answers produced for different answers of the earlier goals, so a later answer of the first goal is reached even when an earlier one leads to infinitely many results. (all, Chapter 6; alli, Chapter 6.)
In the examples below, the first goal chooses 'L or 'R, and counto then relates n to a list of 'Is of any length. The first choice already has infinitely many answers, so all never reaches 'R; alli alternates between the two choices.
> (: counto (→ Term Goal))
> (define (counto n) (conde [(== n '())] [(fresh (m) (== n (cons 'I m)) (counto m))]))
> (run 6 (q) (fresh (n side) (all (conde [(== side 'L)] [(== side 'R)]) (counto n)) (== q (cons side n)))) - : (Listof Term)
'((L) (L I) (L I I) (L I I I) (L I I I I) (L I I I I I))
> (run 6 (q) (fresh (n side) (alli (conde [(== side 'L)] [(== side 'R)]) (counto n)) (== q (cons side n)))) - : (Listof Term)
'((L) (R) (L I) (R I) (L I I) (R I I))
conde is depth-first: it produces all the answers of the first clause before any answer of the second, so a clause with infinitely many answers hides every clause after it. condi (“cond, interleaved”) alternates between the clauses. (conde, Chapter 1; condi, Chapter 6.)
> (: repeato (→ Term Term Goal))
> (define (repeato x l) (conde [(== l '())] [(fresh (d) (== l (cons x d)) (repeato x d))])) > (run 6 (q) (conde [(repeato 'a q)] [(repeato 'b q)])) - : (Listof Term)
'(() (a) (a a) (a a a) (a a a a) (a a a a a))
> (run 6 (q) (condi [(repeato 'a q)] [(repeato 'b q)])) - : (Listof Term)
'(() () (a) (b) (a a) (b b))
In conda (“soft cut”), every answer of the chosen test is passed to the rest of its clause. In condu (“committed choice”), only the first answer of the test is kept. Neither form is relational in general: the result can depend on how much is already known about the variables. (Chapter 10.)
2.5 Substitutions and Unification
These are the operations of Chapter 9.
value
procedure
s : Substitution
procedure
(ext-s x v s) → Substitution
x : Var v : Data s : Substitution (ext-s x v s) → (Option Substitution) x : Var v : Term s : Substitution
> (define x (var 'x)) > (ext-s x '(1 2) empty-s) - : Substitution
(list (list (var 'x) 1 2))
> (ext-s x (list 1 x) empty-s) - : (U False Substitution)
#f
procedure
x : Var v : Term s : Substitution
procedure
v : Term s : Substitution
> (define x (var 'x)) > (define y (var 'y)) > (walk x (list (cons x y) (cons y 'end))) - : Term
'end
> (walk (cons x x) (list (cons x 1))) - : Term
(cons (var 'x) (var 'x))
procedure
v : Term s : Substitution
procedure
(unify u v s) → (Option Substitution)
u : Term v : Term s : Substitution
> (define x (var 'x)) > (define y (var 'y)) > (unify (cons x 2) (cons 1 y) empty-s) - : (U False Substitution)
(list (cons (var 'y) 2) (cons (var 'x) 1))
> (unify (cons x 2) (cons 1 3) empty-s) - : (U False Substitution)
#f
procedure
(reify-name n) → Symbol
n : Index
procedure
(reify-s v s) → Substitution
v : Term s : Substitution
procedure
v : Term
2.6 Streams
The operators of the appendix “Connecting the Wires” are written here with a few stream operations, which are the only code that knows how Streamof is represented: the book’s mzero, unit, and choice become empty-stream, (stream a), and (stream-cons a e), and its case∞ is defined with them. A reader who wonders what case∞ does with a stream can read it off its definition below. Everything is exported so that goals can be written and examined at the level of streams.
2.6.1 Constructing Streams
value
syntax
(stream a ...)
syntax
(stream-cons a d)
2.6.2 Reading Streams
procedure
(stream-empty? a∞) → Boolean
a∞ : (Streamof a)
procedure
(stream-singleton? a∞) → Boolean
a∞ : (Pair a (∪ Null (Suspension a)))
procedure
(stream-first a∞) → a
a∞ : (Pair a (∪ Null (Suspension a)))
procedure
(stream-rest a∞) → (Streamof a)
a∞ : (Pair a (∪ Null (Suspension a)))
> (define a∞ (stream-cons 1 (begin (displayln "rest") (stream 2)))) > (stream-first a∞) - : Integer [more precisely: Positive-Byte]
1
> (stream-singleton? a∞) - : Boolean
#f
> (stream-rest a∞)
rest
- : (Streamof Positive-Byte)
'(2)
> (stream-singleton? (stream 2)) - : Boolean
#t
2.6.3 Lambdas and case∞
syntax
(case∞ e [() on-zero] [(a) on-one] [(a f) on-choice])
(let ([a∞ e]) (cond [(stream-empty? a∞) on-zero] [(stream-singleton? a∞) (define a (stream-first a∞)) on-one] [else (define a (stream-first a∞)) (define (f) (stream-rest a∞)) on-choice]))
so f is a suspension that, when called, produces the rest of the stream. The single answer is told apart with stream-singleton? rather than by looking at (stream-rest a∞), because that would force the rest at once: then a branch that never produces an answer could hold back the answers before it.
2.6.4 The Operators of the Appendix
procedure
a∞ : (Streamof a) f : (Suspension a)
procedure
a∞ : (Streamof a) g : (→ a (Streamof a))
procedure
a∞ : (Streamof a) f : (Suspension a)
procedure
a∞ : (Streamof a) g : (→ a (Streamof a))