| Author: | Michel Alexandre Salim |
|---|

Mini-Kanren is a simplified implementation of Kanren, a declarative logic system, embedded in a pure functional subset of Scheme.
This presentation describes a port to Scala, written for the graduate programming language course at Indiana University.
This presentation is in three sections:
To many ears, the term logic programming is virtually synonymous with Prolog (see [Colmerauer92] for a historical treatment). Outside the domain of Artificial Intelligence, computer science practicioners tend not to be exposed to the field -- in most cases, students are first exposed to procedural, then object-oriented, then functional languages[*].
| [Colmerauer92] | The birth of Prolog, Colmerauer and Roussel, 1992 |
| [*] | If they are (un)lucky, functional comes first |
The goal of The Reasoned Schemer is to help the functional programmer think logically and the logic programmer think functionally. -- [Friedman05]
This presentation uses material sourced from the book, beta-tested by several classes of IU computer science students.
| [Friedman05] | The Reasoned Schemer, by Daniel P. Friedman, William E. Byrd and Oleg Kiselyov |
A substitution is a set of mappings from logical variables to values[1]. It is immutable; extending a substitution with a new key-value mapping produces a new substitution, with the old substitution remaining unchanged[2].
| [1] | including logical variables |
| [2] | Satisfied by Scheme association lists, or Clojure persistent maps |
A goal is a function that, given a substitution, returns a stream of substitutions. There are two basic goals:
Four basic conditional constructs:
We'll stick with conde first, and discuss the others in a bit
(def list?
(λ (l)
(cond
((null? l))
((pair? l)
(list? (cdr l)))
(else #f))))
A list is either an empty list, or a pair whose tail is a list
(def list°
(λ (l)
(conde
((null° l))
((pair° l)
(fresh (d)
(cdr° l d)
(list° d)))
(else #u))))
Note the differences:
(define any°
(λ (g)
(ife g #s
(any° g))))
(define always° (any° #s))
(define never° (any° #u))
anyo is a higher-order goal: given a goal, it generates an infinite stream. Depending on the input goal, it will either succeed an infinite number of times, or fail indefinitely (i.e. bottom)
Scala is a concise, elegant, type-safe programming language that integrates object-oriented and functional features.[3]
| [3] | http://www.scala-lang.org/ |
The name Scala stands for “scalable language.” The language is so named because it was designed to grow with the demands of its users. You can apply Scala to a wide range of programming tasks, from writing small scripts to building large systems.[4]
| [4] | Scala: A Scalable Language, by Martin Odersky, Lex Spoon, and Bill Venners |
Scala is developed by the LAMP group at EPFL, led by Prof. Martin Odersky, who previously worked on Pizza and Generic Java
This is safe:
def even_or_odd(check_even: Boolean, n: Int) = n match {
case 0 => check_even
case _ => even_or_odd(!check_even, n-1)
}
This is not:
def is_even(n: Int) = n match {
case 0 => true
case _ => is_odd(n-1)
}
def is_odd(n: Int) = n match {
case 0 => false
case _ => is_even(n-1)
}
Objects serve two purposes:
Let's look at a concrete example
package info.hircus.kanren
object MiniKanren {
import java.util.HashMap
case class Var(name: Symbol, count: Int)
private val m = new HashMap[Symbol, Int]()
def make_var(name: Symbol) = {
val count = m.get(name)
m.put(name, count+1)
Var(name, count)
} /* more code */
}
Scala provides a read-evaluate-print-loop interpreter, familiar to users of functional and scripting languages
scala> import info.hircus.kanren.MiniKanren._
import info.hircus.kanren.MiniKanren._
scala> val v = make_var('hello)
v: info.hircus.kanren.MiniKanren.Var = Var('hello,0)
scala> val w = make_var('hello)
w: info.hircus.kanren.MiniKanren.Var = Var('hello,1)
scala> val v = make_var('hello)
v: info.hircus.kanren.MiniKanren.Var = Var('hello,2)
scala> v = make_var('world)
<console>:7: error: reassignment to val
v = make_var('world)
Values cannot be reassigned -- use variables for that.
Those familiar with either OCaml or Haskell will be right at home with Scala's pattern-matching construct. Unlike Haskell, there is no pattern matching on function definitions.
Contrast an implementation of a list-summing function in the three languages:
lsum :: (Num t) => [t] -> t -- this line is optional lsum [] = 0 lsum (h:tl) = h + lsum tl
# let rec sum list = match list with | [] -> 0 | head::tail -> head + sum tail;; val sum : int list -> int = <fun> #
scala> def sum(l: List[Int]): Int = l match {
| case Nil => 0
| case h::tl => h + sum(tl)
| }
sum: (List[Int])Int
scala>
| [5] | http://code.google.com/p/scalacheck/ |
| [6] | http://hackage.haskell.org/package/QuickCheck-2.1.0.2 |
import org.scalacheck._
object StringSpecification extends Properties("String") {
property("startsWith") = Prop.forAll((a: String, b: String) =>
(a+b).startsWith(a))
// Is this really always true?
property("concat") = Prop.forAll((a: String, b: String) =>
(a+b).length > a.length && (a+b).length > b.length )
property("substring") = Prop.forAll((a: String, b: String) =>
(a+b).substring(a.length) == b )
}
The initial port was done over the course of several weeks; the current implementation is a rewrite[7]. The initial implementation had a stack-overflow bug that was reëncountered during the rewrite, which I'll discuss in a bit.
The new codebase is better tested, and utilizes more Scala features to make the syntax look natural.
| [7] | original code is lost. moral story: backup (and share online...) |
Several choices for substitution:
Scheme Kanren uses association lists, i.e. a linked list of linked lists, but that could be partly because that's the only native recursive data structure in Scheme.
Kanren does not natively understand numbers, so the most natural constraint is inequality. (This is proposed by Prof. Friedman and is not part of the official Kanren codebase, probably due to performance cost)
This implementation led to the shift in the Scala port from an exact translation of Scheme's substitution to a more OOP implementation (cf. Haskell typeclass).
case class ConstraintSubstN(s: SimpleSubst,
c: Constraints) extends Subst {
def extend(v: Var, x: Any) =
if (this.constraints(v) contains x) None
else Some(ConstraintSubstN(SimpleSubst(v,x,s), c))
override def c_extend(v: Var, x: Any) =
ConstraintSubstN(s, c_insert(v,x,c))
def lookup(v: Var) = s.lookup(v) override def constraints(v: Var) = c_lookup(v, c) def length: Int = s.length }
(define mplus
(lambda (a-inf f)
(case-inf a-inf
(f)
((a) (choice a f))
((a f0) (choice a
(lambdaf@ () (mplus (f0) f)))))))
def mplus(a_inf: Stream[Subst],
f: => Stream[Subst]): Stream[Subst] =
a_inf append f
mplus is simply stream append. It is kept as a separate function because, as can be seen in the next slide, other variants do not have built-in Scala implementations.
(define mplusi
(lambda (a-inf f)
(case-inf a-inf
(f)
((a) (choice a f))
((a f0) (choice a
(lambdaf@ () (mplusi (f) f0)))))))
mplusi interleaves two streams
def mplus_i(a_inf: Stream[Subst],
f: => Stream[Subst]): Stream[Subst] = a_inf match {
case Stream.empty => f
case Stream.cons(a, f0) => f0 match {
case Stream.empty => Stream.cons(a, f)
case _ => Stream.cons(a, mplus_i(f, f0))
}
}
(define bind
(lambda (a-inf g)
(case-inf a-inf
(mzero)
((a) (g a))
((a f) (mplus (g a)
(lambdaf@ () (bind (f) g)))))))
def bind(a_inf: Stream[Subst], g: Goal): Stream[Subst] = a_inf flatMap g
bind is flatMap: it first maps g over the stream, and then append the resulting streams together.
(define bindi
(lambda (a-inf g)
(case-inf a-inf
(mzero)
((a) (g a))
((a f) (mplusi (g a)
(lambdaf@ () (bindi (f) g)))))))
def bind_i(a_inf: Stream[Subst], g: Goal): Stream[Subst] =
a_inf match {
case Stream.empty => a_inf
case Stream.cons(a, f) => f match {
case Stream.empty => g(a)
case _ => mplus_i(g(a), bind(f, g))
}
}
In Scheme, (≡ x y) is the goal that unifies x and y; (≢ x y) constrains them from being unifiable. The syntax looks natural in Scheme, as everything is infix.
In Scala, however, the equivalent looks ugly: mkEqual(x,y); neverEqual(x,y). We can introduce infix operations by using implicit conversions
class Unifiable(a: Any) {
def ===(b: Any): Goal = mkEqual(a, b)
def =/=(b: Any): Goal = neverEqual(a, b)
}
implicit def unifiable(a: Any) = new Unifiable(a)
≡ and ≢ are now methods of the class Unifiable, and because an implicit conversion function is in scope, attempting to call it on any value will autobox it to a Unifiable with the same value.
Most macros in the original code can be completely replaced by functions, apart from the ones that introduce new names.
Drawbacks -- the use of macros is equivalent to compiler inlining, in that the expansion is computed at compile time, rather than at runtime. There is a performance hit that has not been quantified yet; more later.
On the other hand, macros are harder to compose -- not first-class values.
> (run #f (q) (member° q '(a b c d e))) (a b c d e) >
(define-syntax run
(syntax-rules ()
((_ n^ (x) g ...)
(let ((n n^) (x (var x)))
(if (or (not n) (> n 0))
(map-inf n
(lambda (s)
(reify (walk* x s)))
((all g ...) empty-s))
())))))
def run(n: Int, v: Var)(g0: Goal, gs: Goal*) = {
val g = gs.toList match {
case Nil => g0
case gls => all((g0::gls): _*)
}
val allres = g(empty_s) map {s: Subst => reify(walk_*(v, s)) }
(if (n < 0) allres else (allres take n)) toList
}
(def list°
(λ (l)
(conde
((null° l))
((pair° l)
(fresh (d)
(cdr° l d)
(list° d))))))
This differs slightly from the first appearance of list°: the (else #u) line is removed, as conde fails by default
def list_o(l: Any): Goal = {
cond_e((null_o(l), succeed),
(pair_o(l), { s: Subst => {
val d = make_var('d)
both(cdr_o(l, d), list_o(d))(s) } }))
}
> (run 2 (x)
(conde
((== x 7) (project (x) (begin (printf "~s~n" x) succeed)))
((== x 42) (project (x) (begin (printf "~s~n" x) fail)))))
7
42
(7)
>
run(2, x)(cond_e((mkEqual(x,7), { s: Subst => {
val x1 = walk_*(x, s)
println(x1)
succeed(s) }}),
(mkEqual(x,42), { s: Subst => {
val x1 = walk_*(x, s)
println(x1)
fail(s) }})))
When computing with streams, eagerness is bad
$ git diff 5bc7a839ae9db cc596e43b465c
/**
- * While we could use call-by-name here,
- * since the goals are functions anyway, delaying evaluation is
- * unnecessary
...
- def if_e(g0: Goal, g1: Goal, g2: Goal): Goal = {
+ def if_e(testg: Goal, conseqg: Goal, altg: => Goal): Goal = {
...
> (time (run 1 (q) (palprod2 q)))
100001
101101
(time (run 1 ...))
315 collections
37916 ms elapsed cpu time, including 156 ms collecting
38858 ms elapsed real time, including 161 ms collecting
1330081488 bytes allocated, including 1325728560 bytes reclaimed
((1 1 1 0 0 1 1 1 1 1 0 0 0 1))
scala> time(run(1,x)(palprod_o(x))) 100001 101101 Elapsed: 114344 ms res2: Any = List((1,(1,(1,(0,(0,(1,(1,(1,(1,(1,(0,(0,(0,(1,List()...
scala> time(run(1,x)(palprod_o(x))) 100001 101101 Elapsed: 44277 ms res2: Any = List((1,(1,(1,(0,(0,(1,(1,(1,(1,(1,(0,(0,(0,(1,List()...
| [8] | Erlang implementation: http://lukego.livejournal.com/6753.html |
The Scala port is available under the BSD license from GitHub[9]. The latest Kanren source is available on Sourceforge[10].
| [9] | http://github.com/hircus/minikanren-scala |
| [10] | http://kanren.sourceforge.net/ |
Your questions, suggestions, etc. are welcome! The project bug tracker is at the GitHub address.