Thanks for the replies from you two - Rick and Will.
I understand the arguments for not using "leto" instead of "fresh", i.e., that "let" traditionally is a "let new variable = value" construct.
But in my opinion, a term like "leto" would have been - and is - a better choice anyway. I put more weight on the first property of "let": It introduces new variables in the code.The fact that the variables aren't assigned any values at the time of declaration in a Kanren program is not something which makes me feel uncomfortable with the term "leto".
As I see it, it would feel perfectly natural to read and interpret "leto" as having the meaning "let these new variables be used in the following code block".
The Kanren-family of languages has already stretched the meaning of another core term borrowed from the lisp-family: "cond". In Kanren, it comes in the three flavors conde, conda, and condu, and these terms must have been deliberately chosen to make them easier to understand because they resemble the familiar "cond" term borrowed in Lisp.
In the same vein, "leto" is a better choice than "fresh" because of the resemblance between "leto" and the "let" - the latter traditionally being used in other languages for the same purpose, i.e., to introduce new variables in a code block.
Seen this way "leto" would not be misleading (as Will uttered). Perhaps "let" would have been problematic, but with the "o"-suffix, the "leto" construct is properly distinguishable from "let", just like conde, conda, and condu are distinguishable from "cond".
Another objection I have to the term "fresh" has to do with "musicality". As I see it, the term "fresh" is so much out of of tune, as it possible can be, with the otherwise very refreshing terminology used in the Kanren language family.
If not "leto, then the term for the "fresh" language construct should at least be another neologism adhering to the overall language design. A term like "varo" comes to mind as a splendid candidate, with its reference to the Algol-language family (e.g., Pascal, Modula, Oberon) "var" construct, which works the same way as "fresh": It introduces new variables, but without initializing them at the same time.
All that said, let me end this post by telling how much I've enjoyed getting acquainted with Micro/Mini-Kanren. The implementation is truly mind-blowing (at least for me - quoting Winnie the Pooh: "I'm a bear of very little brain :-) It's marvelous that a language with such extraordinary features can be implemented so elegantly and so cleverly in just about 200 lines of Lisp/Scheme code. There is so much intelligence and cleverness engraved in these 200 lines that it turns into "logical poetry and beauty" - something the original programmers and inventors can be very, very proud of.
Based on the original source code version from the literature, I wrote a micro/mini-kanren implementation so it runs on my own Lisp system - a pet-project of mine. It's probably never going to be published anywhere, so just for my own pleasure, I've listed it below. Comments, corrections, and suggestions on how to improve the code and the comments (English is not my first language) are welcome.
Even though my source is utterly uninteresting to others, I have, nevertheless, a few things to say about it, which might have a minuscule general interest:
1.
This code shows how to implement the necessary macros with quasiquotes only, i.e., without a pattern-based macro system, like Scheme has.
2.
I use the term "bindings" everywhere in the code, where the reference implementations in the literature always talk about a "substitution". The latter is misleading. The program has data for a substitution. A substitution can be made, based on the data. In simple Kanren implementations like mine, and the reference implementations in the literature, the available data is a simple, so-called "assoc-list", a list of key-value bindings.
3.
Even though I kept the term "fresh", I offer three other name improvements at the bottom of the code, in form of symbol-macros:
* "rl" for define-relation.
There is no way I as a programmer will put up with writing "define-relation" each time I define a relation.
* "oro" for disj
* "ando" for conj
I think that "oro" and "ando" are quicker to grasp when reading code. I don't know how other people feel, but my theory is that it takes more brain-power to grasp words like "disj" and "conj", i.e., that more brain-circuitry is involved in deciphering the meaning of such words, whereas basic words like "or" and "and" are decoded by the brain on a fast lane.
I guess this is similar to the interesting observation that the human brain can grasp that there are 4 items in view, but as soon as there are 5 items, the brain changes much change to some different circuitry and do the actual counting!
- bd
(mc variable-tag (variable) `(first ,variable))
(mc variable-name (variable) `(rest ,variable))
(mc make-variable (name) `(cons micro-kanren-variable-tag ,name))
(mc variable? (u) `(and (pair? ,u) (same-object? (variable-tag ,u) micro-kanren-variable-tag)))
(mc bindings->stream (bindings) `(list ,bindings)) ; elevates a set of bindings to a stream of sets of bindings
(const empty-stream nil) ; must be nil
(const fail empty-stream) ; must be nil
(const succeed (fn (bindings) (bindings->stream bindings)))
(const micro-kanren-initial-bindings (list nil)) ; must be non-nil
(const micro-kanren-variable-tag (list nil)) ; must be unique
(fn extend-bindings (variable value bindings) ; associates "variable" with "value" in a new set of bindings
(if (not (occurs-in? variable value bindings))
(acons variable value bindings))) ; using built-in "acons" function
(fn occurs-in? (variable value bindings) ; checks whether "variable" occurs in "value"
(let value (walk value bindings)
(if (variable? value)
(same-object? variable value)
(pair? value)
(or (occurs-in? variable (first value) bindings)
(occurs-in? variable (rest value) bindings)))))
(fn == (u v) ; tries to unify the two terms "u" and "v", using the given bindings
(fn (bindings)
(if (set bindings (unify u v bindings))
(bindings->stream bindings)
empty-stream)))
(fn walk (value bindings) ; dereferences the value, using the given bindings
(let binding _
(while (and (variable? value)
(set binding (assoc value bindings same-object?))) ; compare: identity, i.e., "eq" in Common Lisp
(set value (rest binding)))
value))
(fn unify (u v bindings) ; tries to unify the two terms "u" and "v", using the given bindings
(let u (walk u bindings)
v (walk v bindings)
(if (same-object? u v)
bindings
(variable? u)
(extend-bindings u v bindings)
(variable? v)
(extend-bindings v u bindings)
(and (pair? u) (pair? v))
(and (set bindings (unify (first u) (first v) bindings))
(unify (rest u) (rest v) bindings))
(=? u v)
bindings)))
(fn call/fresh (variable-name function) ; runs the function with a newly created variable as argument
(fn (bindings)
((function (make-variable variable-name)) bindings)))
(mc disj2 (goal1 goal2) `(fn (bindings) (append-streams (,goal1 bindings) (,goal2 bindings))))
(mc conj2 (goal1 goal2) `(fn (bindings) (append-map-streams ,goal2 (,goal1 bindings))))
(fn append-streams (stream1 stream2)
(if (nil? stream1)
stream2
(function? stream1)
(fn () (append-streams stream2 (stream1)))
(cons (first stream1) (append-streams (rest stream1) stream2))))
(fn append-map-streams (goal stream) ; applies "goal" to every member of "stream"
(if (nil? stream)
empty-stream
(function? stream)
(fn () (append-map-streams goal (stream)))
(append-streams (goal (first stream)) (append-map-streams goal (rest stream)))))
(fn stream-pull (stream)
(while (function? stream)
(set stream (stream-pull (stream))))
stream)
(fn stream-take (count stream)
(if (nil? stream)
nil
(=? count 1)
(list (first stream))
(cons (first stream) (stream-take (and count (1- count)) (stream-pull (rest stream))))))
(fn call/initial-state (count goal)
(stream-take count (stream-pull (goal micro-kanren-initial-bindings))))
(mc define-relation (name-and-arguments . goals)
(let name (first name-and-arguments)
arguments (rest name-and-arguments)
bindings (new-symbol)
`(fn ,name ,arguments
(fn (,bindings) (fn () ((conj ,@goals) ,bindings))))))
(fn ifte (condition_ then_ else_) ; if-condition-then-else
(fn (bindings)
(let (fn loop (condition_)
(if (nil? condition_)
(else_ bindings)
(function? condition_)
(fn () (loop (condition_)))
(append-map-streams then_ condition_)))
in (loop (condition_ bindings)))))
(fn once (goal)
(fn (bindings)
(let (fn loop (stream)
(if (nil? stream)
empty-stream
(function? stream)
(fn () (loop (stream)))
(list (first stream))))
in (loop (goal bindings)))))
(mc project (variables . goals)
`(fn (bindings)
(let ,@(mappend (fn (variable) `(,variable (walk* ,variable bindings))) variables)
in ((conj ,@goals) bindings))))
(mc disj (? . goals) ; zero or more goals
(case (length goals)
0 fail
1 (first goals)
(foldr (fn (goal rest) `(disj2 ,goal ,rest)) (last goals) (but-last goals))))
(mc conj (? . goals) ; zero or more goals
(case (length goals)
0 succeed
1 (first goals)
(foldr (fn (goal rest) `(conj2 ,goal ,rest)) (last goals) (but-last goals))))
(mc fresh (variables . goals) ; runs goals with newly created variables
(foldr (fn (variable rest) `(call/fresh ',variable (fn (,variable) ,rest)))
`(conj ,@goals)
variables))
(fn walk* (value bindings)
(let value (walk value bindings)
(if (variable? value)
value
(pair? value)
(cons (walk* (first value) bindings) (walk* (rest value) bindings))
value)))
(fn reify (value)
(fn (bindings)
(let (fn make-reification-bindings (value bindings)
(let (fn make-reification-name (index)
(coerce (format "_.~d" index) 'symbol))
in (if (variable? (set value (walk value bindings)))
(extend-bindings value (make-reification-name (length bindings)) bindings)
(pair? value)
(make-reification-bindings
(rest value)
(make-reification-bindings (first value) bindings))
bindings)))
value (walk* value bindings)
in (walk* value (make-reification-bindings value nil)))))
(mc run (count goal-variables . goals) ; "goal-variables" may be a list of symbols, or a single symbol
(let goal-variables (listify goal-variables) ; e.g., (x y z)
(if (=? (length goal-variables) 1)
`(let ,(first goal-variables) (make-variable ',(first goal-variables))
(map (reify ,(first goal-variables))
(call/initial-state ,count (conj ,@goals))))
(let goal-variable (new-symbol)
`(run ,count
,goal-variable
(fresh ,goal-variables (== ,goal-variable (list ,@goal-variables)) ,@goals))))))
(mc run* (goal-variables . goals)
`(run nil ,goal-variables ,@goals))
(mc conde (? . goals) ; each goal is a goal-sequence. make the goals a disjunction of conjunctions
`(disj ,@(map (fn (goal-sequence) `(conj ,@(listify goal-sequence))) goals)))
(mc conda (? . goals) ; committed choices
; each goal is a goal-sequence. just perform the first goal-sequence whose first goal succeeds
(case (length goals)
0 fail
1 `(conj ,@goals)
(foldr (fn (goal-sequence else_)
`(ifte ,(first goal-sequence) (conj ,@(rest goal-sequence)) ,else_))
`(conj ,@(last goals))
(but-last goals))))
(mc condu (? . goals) ; committed choices, where the condition-clause succeeds only once
`(conda ,@(map (fn (sequence) `((once ,(first sequence)) ,@(rest sequence))) goals)))
; symbol-macros
(sm rl define-relation) ; for convenience
(sm oro disj) ; more readable
(sm ando conj) ; more readable