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.
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[*].
[*] | If they are (un)lucky, functional comes first |
[Colmerauer92] | The birth of Prolog, Colmerauer and Russell, 1992 |
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 mapping from logical variables to values[1]. It is immutable; extending a substitution with a new key-value pair 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:
(define mplus (lambda (a-inf f) (case-inf a-inf (f) ((a) (choice a f)) ((a f0) (choice a (lambdaf@ () (mplus (f0) f)))))))
(define mplusi (lambda (a-inf f) (case-inf a-inf (f) ((a) (choice a f)) ((a f0) (choice a (lambdaf@ () (mplusi (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)))))))
(define bindi (lambda (a-inf g) (case-inf a-inf (mzero) ((a) (g a)) ((a f) (mplusi (g a) (lambdaf@ () (bindi (f) g)))))))
Two families of conditional constructs are provided -- cond-like constructs and if-like constructs.
(define-syntax ifaux (syntax-rules () ((_ mplusfn g0 g1 g2) (lambdag@ (s) (mplusfn ((all g0 g1) s) (lambdaf@ () (g2 s)))))))
(define-syntax ife (syntax-rules () ((_ g0 g1 g2) (ifaux mplus g0 g1 g2)))) (define-syntax ifi (syntax-rules () ((_ g0 g1 g2) (ifaux mplusi g0 g1 g2))))
(def list? (λ (l) (if (null? l) #t (if (pair? l) (list? (cdr l)) #f))))
A list is either an empty list, or a pair whose tail is a list
(def list° (λ (l) (if-e (null° l) #s (if-e (pair° l) (fresh° (d) (cdr° l d) (list° d)) #u))))
Note the differences:
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 scala> import info.hircus.kanren.MiniKanren._ import info.hircus.kanren.MiniKanren._ scala> val v = make_var('hello) 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
[3] | http://code.google.com/p/scalacheck/ |
[4] | 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 Scala port is available under the BSD license from GitHub[5]. The latest Kanren source is available on Sourceforge[6].
[5] | http://github.com/hircus/minikanren-scala |
[6] | http://kanren.sourceforge.net/ |