diff --git a/.gitignore b/.gitignore
index fea75c6..3cbb5d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,10 @@ bin
*~
*.\#*
*\#*\#
+.idea
+/project/project/target/
+/project/target/
+/js/
+/jvm/target/
+project/project/
+target/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..bc89b84
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,34 @@
+# See http://about.travis-ci.org/docs/user/build-configuration/
+# use Docker-based container (instead of OpenVZ)
+sudo: false
+
+cache:
+ directories:
+ - $HOME/.m2/repository
+ - $HOME/.sbt
+ - $HOME/.ivy2
+
+before_cache:
+ # Cleanup the cached directories to avoid unnecessary cache updates
+ - find $HOME/.ivy2/cache -name "ivydata-*.properties" -print -delete
+ - find $HOME/.sbt -name "*.lock" -print -delete
+
+language: scala
+jdk:
+ - oraclejdk8
+scala:
+ - 2.10.6
+ - 2.11.8
+ - 2.12.1
+notifications:
+ email: false
+# webhooks:
+# urls:
+# on_success: always # options: [always|never|change] default: always
+# on_failure: always # options: [always|never|change] default: always
+# on_start: false # default: false
+script:
+ - sbt ++$TRAVIS_SCALA_VERSION clean coverage test miniKanrenJVM/tut coverageReport &&
+ sbt coverageAggregate
+after_success:
+ - sbt coveralls
\ No newline at end of file
diff --git a/AUTHORS b/AUTHORS
index e580548..4584b61 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -2,3 +2,4 @@ AUTHORS
=======
Michel Alexandre Salim
+Gabor Bakos
\ No newline at end of file
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 9cf7c02..0000000
--- a/Makefile
+++ /dev/null
@@ -1,26 +0,0 @@
-BASEDIR=info/hircus/kanren
-TITLE="Mini Kanren"
-
-all: bin bin/${BASEDIR}/examples
-
-bin: src/${BASEDIR}/*.scala src/${BASEDIR}/tests/*.scala
- $(shell [ -d bin ] && touch -m bin || mkdir -p bin)
- scalac -d bin src/${BASEDIR}/*.scala src/${BASEDIR}/tests/*.scala
-
-bin/${BASEDIR}/examples: bin src/${BASEDIR}/examples/*.scala
- scalac -d bin src/${BASEDIR}/examples/*.scala
-
-check:
- ./check.sh
-
-api: src/${BASEDIR}/*.scala src/${BASEDIR}/examples/*.scala
- $(shell [ -d api ] && touch -m api || mkdir -p api)
- scaladoc -doctitle ${TITLE} -windowtitle ${TITLE} -d api \
- src/${BASEDIR}/*.scala src/${BASEDIR}/examples/*.scala
-
-clean:
- -rm -rf api bin
-
-publish: api
- make -C docs
- ./publish.sh
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cea133d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,32 @@
+A Scala port of [miniKanren](http://minikanren.org/)
+====================================================
+
+[](https://travis-ci.org/aborg0/minikanren-scala)
+[](https://coveralls.io/github/aborg0/minikanren-scala?branch=sbt)
+
+Based on https://github.com/michel-slm/minikanren-scala
+
+Please check the documentation in [docs/presentation.rst](docs/presentation) for details.
+
+Using REPL with SBT:
+
+ > miniKanrenJVM/console
+
+...
+
+ scala> time(run(1, x)(solve_puzzle(x)))
+ res0: (Long, Any) = (10044,List(List(9567, 1085, 10652)))
+
+Interpretation of the result: took `10044` milliseconds to solve the problem, a result is
+
+ SEND 9567
+ +MORE +1085
+ ----- -----
+ MONEY 10652
+
+Another example (palindromes with six-digit numbers that are the product of two three-digit numbers), this time with `maprun` as it is much faster:
+
+ time(maprun(1, x)(palprod_o(x)))
+ 100001
+ 101101
+ res1: (Long, Any) = (40837,List((1,(1,(1,(0,(0,(1,(1,(1,(1,(1,(0,(0,(0,(1,List()))))))))))))))))
diff --git a/build.sbt b/build.sbt
new file mode 100644
index 0000000..51c4161
--- /dev/null
+++ b/build.sbt
@@ -0,0 +1,90 @@
+enablePlugins(ScalaJSPlugin)
+
+name := "Scala MiniKanren root project"
+crossScalaVersions := Seq("2.10.6", "2.11.8", "2.12.1")
+scalaVersion in ThisBuild := "2.12.1" // or any other Scala version >= 2.10.2 for Scala.js
+
+// This is an application with a main method
+scalaJSUseMainModuleInitializer := true
+
+lazy val root = project.in(file(".")).
+ aggregate(miniKanrenJS, miniKanrenJVM).
+ settings(
+ publish := {},
+ publishLocal := {}
+ )
+
+lazy val miniKanren = crossProject.in(file(".")).
+ settings(
+ name := "Scala MiniKanren",
+ version := "0.1-SNAPSHOT",
+ libraryDependencies += "org.scalacheck" %%% "scalacheck" % "1.13.4" % "test",
+
+ scalacOptions ++= Seq(
+ "-deprecation",
+ "-encoding", "UTF-8",
+ "-unchecked",
+ "-feature",
+ //"-language:implicitConversions",
+ //"-language:postfixOps",
+ //"-language:higherKinds",
+ //"-language:reflectiveCalls",
+ "-Xlint",
+ // "-Xfatal-warnings",
+ //"-Yno-adapted-args",
+ "-Ywarn-dead-code",
+ "-Ywarn-numeric-widen",
+ "-Ywarn-value-discard",
+ "-Xfuture"
+ )
+
+ ).jvmSettings(
+ coverageEnabled := true,
+ initialCommands := """
+ |import info.hircus.kanren.MiniKanren._
+ |import info.hircus.kanren.Prelude._
+ |import info.hircus.kanren.MKMath._
+ |import info.hircus.kanren.examples.PalProd._
+ |import info.hircus.kanren.examples.SendMoreMoney._
+ |
+ |var x = make_var('x)
+ |var y = make_var('y)
+ |var z = make_var('z)
+ |
+ |def time(block: => Any) = {
+ | val start = System currentTimeMillis ()
+ | val res = block
+ | val stop = System currentTimeMillis ()
+ | ((stop-start), res)
+ |}
+ |
+ |def ntimes(n: Int, block: => Any) = {
+ | // folding a list of Longs is cumbersome
+ | def adder(x:Long,y:Long) = x+y
+ | val zero : Long = 0
+ |
+ | // compute only once!
+ | val res = (for (i <- 0 until n) yield (time(block) _1)).toList
+ | println("Elapsed times: " + res)
+ | println("Avg: " + (res.foldLeft(zero)(adder) / n))
+ |}
+ |""".stripMargin,
+ tutSettings
+ ).jsSettings(
+ coverageEnabled := false
+ )
+
+lazy val miniKanrenJVM = miniKanren.jvm
+
+lazy val miniKanrenJS = miniKanren.js
+
+//tutSettings
+
+//tutSourceDirectory := baseDirectory.value / "shared" / "src" / "main" / "tut"
+
+LaikaPlugin.defaults
+
+inConfig(LaikaKeys.Laika)(Seq(
+// sourceDirectories := Seq(baseDirectory.value / "docs"),
+ LaikaKeys.encoding := "UTF-8"
+))
\ No newline at end of file
diff --git a/check.sh b/check.sh
deleted file mode 100755
index 54d6fcf..0000000
--- a/check.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/sh
-cd bin
-for tst in info.hircus.kanren.tests.{Subst,Unify,Run,Branching,Math}Specification; do
- scala $tst
-done
-cd ..
diff --git a/docs/.gitignore b/docs/.gitignore
deleted file mode 100644
index a11c7ee..0000000
--- a/docs/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-ui
-*.html
diff --git a/docs/Makefile b/docs/Makefile
deleted file mode 100644
index 0203c3c..0000000
--- a/docs/Makefile
+++ /dev/null
@@ -1,11 +0,0 @@
-all: presentation.html handouts.html
-
-presentation.html: presentation.rst
- rst2s5 presentation.rst presentation.html
-
-handouts.html: presentation.rst
- rst2html presentation.rst handouts.html
-
-clean:
- -rm -rf presentation.html ui
-
diff --git a/jvm/src/main/tut/io_livecode_ch_learn_webyrd_webmk.md b/jvm/src/main/tut/io_livecode_ch_learn_webyrd_webmk.md
new file mode 100644
index 0000000..b0cee39
--- /dev/null
+++ b/jvm/src/main/tut/io_livecode_ch_learn_webyrd_webmk.md
@@ -0,0 +1,348 @@
+# miniKanren: an interactive Tutorial
+
+
+## Core miniKanren
+
+Core miniKanren extends Scheme with three operations:
+`==` (here in scala: `unify`/`mkEqual`/`===`), `fresh` (here in scala: `make_var`), and `conde`.
+There is also `run`, which serves as an interface between
+Scheme and miniKanren, and whose value is a list.
+
+`==` unifies two terms. `fresh`, which
+syntactically looks like `lambda`, introduces lexically-scoped
+Scheme variables that are bound to new logic variables; `fresh`
+also performs conjunction of the relations within its body. Thus
+`(fresh (x y z) (== x z) (== 3 y))`
+
+```tut:silent
+import info.hircus.kanren._
+import info.hircus.kanren.MiniKanren._
+import info.hircus.kanren.MKMath._
+val x = make_var('x)
+val y = make_var('y)
+val z = make_var('z)
+all(mkEqual(x, z), mkEqual(3, y))
+```
+
+would introduce logic variables `x`, `y`, and `z`,
+then associate `x` with `z` and `y`
+with `3`. This, however, is not a legal miniKanren
+program---we must wrap a `run` around the entire expression.
+`(run 1 (q) (fresh (x y z) (== x z) (== 3 y)))`
+
+ ```tut
+val q = make_var('q)
+run(1, q)(all(mkEqual(make_var('x), make_var('z)), mkEqual(build_num(3), make_var('y))))
+ ```
+
+The value returned is a list containing the single
+value `(_.0)`; we
+say that `_.0` is
+the *reified value* of the unbound query variable `q` and thus
+represents any value. `q` also remains unbound in
+`(run 1 (q) (fresh (x y) (== x q) (== 3 y)))`
+
+ ```tut
+run(1, q)(all(mkEqual(make_var('x), q), mkEqual(build_num(3), make_var('y))))
+ ```
+
+We can get back more interesting values by unifying the query variable with another term.
+
+ (run 1 (y)
+ (fresh (x z)
+ (== x z)
+ (== 3 y)))
+
+```tut
+val ex3_x = make_var('x)
+val ex3_z = make_var('z)
+val three = build_num(3)
+run(1, y)(all(mkEqual(ex3_x, ex3_z), mkEqual(build_num(3), y)))
+```
+> Note that numbers are represented in binary form, so `3` (`three`) is `11` (`(1, (1, List()))`, similar to `HList`s, these are encoded as tuples).
+
+ (run 1 (q)
+ (fresh (x z)
+ (== x z)
+ (== 3 z)
+ (== q x)))
+
+```tut
+val ex4_x = make_var('x)
+val ex4_z = make_var('z)
+run(1, q)(all(mkEqual(ex4_x, ex4_z), mkEqual(build_num(3), ex4_z), mkEqual(q, ex4_x)))
+```
+
+ (run 1 (y)
+ (fresh (x y)
+ (== 4 x)
+ (== x y))
+ (== 3 y))
+
+```tut
+val ex5_x = make_var('x)
+run(1, y)(all(mkEqual(build_num(4), ex5_x), mkEqual(ex5_x, make_var('y)), mkEqual(build_num(3), y)))
+```
+
+Each of these examples returns `(3)`; in the
+rightmost example, the `y` introduced by `fresh` is
+different from the `y` introduced by `run`.
+
+
\ No newline at end of file
diff --git a/mk-scheme b/mk-scheme
deleted file mode 100755
index d4b1d4c..0000000
--- a/mk-scheme
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/sh
-pushd ${HOME}/checkouts/upstream/kanren/mini
-#petite mybook.ss
-petite palindrom-fixed.scm
-popd
diff --git a/project/build.properties b/project/build.properties
new file mode 100644
index 0000000..cf19fd0
--- /dev/null
+++ b/project/build.properties
@@ -0,0 +1 @@
+sbt.version=0.13.15
\ No newline at end of file
diff --git a/project/plugins.sbt b/project/plugins.sbt
new file mode 100644
index 0000000..53360d3
--- /dev/null
+++ b/project/plugins.sbt
@@ -0,0 +1,24 @@
+resolvers += Resolver.url(
+ "bintray-laika-sbt-plugin-releases",
+ url("http://dl.bintray.com/content/jenshalm/sbt-plugins/"))(
+ Resolver.ivyStylePatterns)
+
+resolvers += Resolver.url(
+ "bintray-scala.js-sbt-plugin-releases",
+ url("https://dl.bintray.com/content/scala-js/scala-js-releases"))(
+ Resolver.ivyStylePatterns)
+
+resolvers += Resolver.url(
+ "bintray-tut-sbt-plugin-releases",
+ url("https://dl.bintray.com/content/tpolecat/sbt-plugins"))(
+ Resolver.ivyStylePatterns)
+
+addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.15")
+
+addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.8")
+
+addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.0")
+
+addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.1.0")
+
+addSbtPlugin("org.planet42" % "laika-sbt" % "0.6.0")
\ No newline at end of file
diff --git a/publish.sh b/publish.sh
deleted file mode 100755
index 5b83e04..0000000
--- a/publish.sh
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/sh
-if [ -z "$1" ]; then
- HOST=hircus@iceland.freeshell.org
-else
- HOST=$1
-fi
-
-if [ -z "$2" ]; then
- PUBDIR=html/kanren/
-else
- PUBDIR=$2
-fi
-
-rsync -avz --delete -e ssh api docs/{handouts,presentation}.html docs/ui \
- $HOST:$PUBDIR
-
diff --git a/src/info/hircus/kanren/MKMath.scala b/shared/src/main/scala/info/hircus/kanren/MKMath.scala
similarity index 100%
rename from src/info/hircus/kanren/MKMath.scala
rename to shared/src/main/scala/info/hircus/kanren/MKMath.scala
diff --git a/src/info/hircus/kanren/MiniKanren.scala b/shared/src/main/scala/info/hircus/kanren/MiniKanren.scala
similarity index 93%
rename from src/info/hircus/kanren/MiniKanren.scala
rename to shared/src/main/scala/info/hircus/kanren/MiniKanren.scala
index 1bbff9c..7ed73b7 100644
--- a/src/info/hircus/kanren/MiniKanren.scala
+++ b/shared/src/main/scala/info/hircus/kanren/MiniKanren.scala
@@ -244,9 +244,9 @@ object MiniKanren {
def bind_i(a_inf: Stream[Subst], g: Goal): Stream[Subst] =
a_inf match {
- case Stream.empty => a_inf
+ case Stream.Empty => a_inf
case Stream.cons(a, f) => f match {
- case Stream.empty => g(a)
+ case Stream.Empty => g(a)
case _ => mplus_i(g(a), bind(f, g))
}
}
@@ -273,9 +273,9 @@ object MiniKanren {
*/
def mplus_i(a_inf: Stream[Subst],
f: => Stream[Subst]): Stream[Subst] = a_inf match {
- case Stream.empty => f
+ case Stream.Empty => f
case Stream.cons(a, f0) => f0 match {
- case Stream.empty => Stream.cons(a, f)
+ case Stream.Empty => Stream.cons(a, f)
case _ => Stream.cons(a, mplus_i(f, f0))
}
@@ -307,8 +307,8 @@ object MiniKanren {
}
}
- def all = all_aux(bind) _
- def all_i = all_aux(bind_i) _
+ def all(gs: Goal*) = all_aux(bind)(gs: _*)
+ def all_i(gs: Goal*) = all_aux(bind_i)(gs: _*)
/**
@@ -351,16 +351,16 @@ object MiniKanren {
s: Subst => {
val s_inf = testg(s)
s_inf match {
- case Stream.empty => altg(s)
+ case Stream.Empty => altg(s)
case Stream.cons(s_1, s_inf_1) => s_inf_1 match {
- case Stream.empty => conseqg(s_1)
+ case Stream.Empty => conseqg(s_1)
case _ => bind(s_inf, conseqg) } }
} }
def if_u(testg: Goal, conseqg: =>Goal, altg: =>Goal): Goal = {
s: Subst => {
testg(s) match {
- case Stream.empty => altg(s)
+ case Stream.Empty => altg(s)
case Stream.cons(s_1, s_inf) => conseqg(s_1) }
} }
@@ -373,10 +373,10 @@ object MiniKanren {
cond_aux(ifer)(gs2: _*))
} } }
- def cond_e = cond_aux(if_e _) _
- def cond_i = cond_aux(if_i _) _
- def cond_a = cond_aux(if_a _) _
- def cond_u = cond_aux(if_u _) _
+ def cond_e(gs: (Goal, Goal)*) = cond_aux(if_e _)(gs: _*)
+ def cond_i(gs: (Goal, Goal)*) = cond_aux(if_i _)(gs: _*)
+ def cond_a(gs: (Goal, Goal)*) = cond_aux(if_a _)(gs: _*)
+ def cond_u(gs: (Goal, Goal)*) = cond_aux(if_u _)(gs: _*)
class Unifiable(a: Any) {
def ===(b: Any): Goal = mkEqual(a, b)
@@ -423,9 +423,9 @@ object MiniKanren {
* @param v the variable to be inspected
* @param g0 a goal; multiple goals might be specified
*/
- def run(n: Int, v: Var) = run_aux(n, v, empty_s) _
- def crun(n: Int, v: Var) = run_aux(n, v, empty_cs) _
- def maprun(n: Int, v: Var) = run_aux(n, v, empty_msubst) _
+ def run(n: Int, v: Var)(g0: Goal, gs: Goal*) = run_aux(n, v, empty_s)(g0, gs: _*)
+ def crun(n: Int, v: Var)(g0: Goal, gs: Goal*) = run_aux(n, v, empty_cs)(g0, gs: _*)
+ def maprun(n: Int, v: Var)(g0: Goal, gs: Goal*) = run_aux(n, v, empty_msubst)(g0, gs: _*)
def cljrun(n: Int, v: Var) = run_aux(n, v, empty_cljsubst) _
private def run_aux(n: Int, v: Var, subst: Subst)(g0: Goal, gs: Goal*): List[Any] = {
diff --git a/src/info/hircus/kanren/Prelude.scala b/shared/src/main/scala/info/hircus/kanren/Prelude.scala
similarity index 100%
rename from src/info/hircus/kanren/Prelude.scala
rename to shared/src/main/scala/info/hircus/kanren/Prelude.scala
diff --git a/src/info/hircus/kanren/Subst.scala b/shared/src/main/scala/info/hircus/kanren/Subst.scala
similarity index 84%
rename from src/info/hircus/kanren/Subst.scala
rename to shared/src/main/scala/info/hircus/kanren/Subst.scala
index 09a187b..d97cd23 100644
--- a/src/info/hircus/kanren/Subst.scala
+++ b/shared/src/main/scala/info/hircus/kanren/Subst.scala
@@ -171,33 +171,47 @@ object Substitution {
* causes heap OOM exception.
*/
case class MSubst(m: Map[Var, Any]) extends Subst {
- def extend(v: Var, x: Any) = Some(MSubst(m(v) = x))
+ def extend(v: Var, x: Any) = Some(MSubst(m + (v -> x)))
def lookup(v: Var) = m.get(v)
def length = m.size
}
val empty_msubst = MSubst(Map())
- import clojure.lang.IPersistentMap
- import clojure.lang.PersistentHashMap
+// import clojure.lang.IPersistentMap
+// import clojure.lang.PersistentHashMap
+//
+// /**
+// * A substitution based on Clojure's PersistentHashMap
+// * (earlier based on Odersky's colleague's work at EPFL!)
+// *
+// * Requires a modified Clojure, because right now the
+// * MapEntry interface exposes a val() getter which clashes
+// * with the Scala keyword
+// */
+// case class CljSubst(m: IPersistentMap) extends Subst {
+// def extend(v: Var, x: Any) = Some(CljSubst(m.assoc(v, x)))
+// def lookup(v: Var) = {
+// val res = m.entryAt(v)
+// if (res != null) Some(res.`val`)
+// else None
+// }
+// def length = m.count
+// }
+//
+// val empty_cljsubst = CljSubst(PersistentHashMap.EMPTY)
+ import scala.collection.immutable.Map
/**
- * A substitution based on Clojure's PersistentHashMap
- * (earlier based on Odersky's colleague's work at EPFL!)
- *
- * Requires a modified Clojure, because right now the
- * MapEntry interface exposes a val() getter which clashes
- * with the Scala keyword
- */
- case class CljSubst(m: IPersistentMap) extends Subst {
- def extend(v: Var, x: Any) = Some(CljSubst(m.assoc(v, x)))
+ * A substitution based on Scala's immutable hashmap (which now looks like Clojure's PersistentHashMap)
+ */
+ case class CljSubst(m: Map[Any, Any]) extends Subst {
def lookup(v: Var) = {
- val res = m.entryAt(v)
- if (res != null) Some(res.`val`)
- else None
+ m.get(v)
}
- def length = m.count
+ def extend(v: Var, x: Any) = Some(CljSubst(m + (v -> x)))
+ def length = m.size
}
- val empty_cljsubst = CljSubst(PersistentHashMap.EMPTY)
+ val empty_cljsubst = CljSubst(Map())
}
diff --git a/src/info/hircus/kanren/examples/PalProd.scala b/shared/src/main/scala/info/hircus/kanren/examples/PalProd.scala
similarity index 100%
rename from src/info/hircus/kanren/examples/PalProd.scala
rename to shared/src/main/scala/info/hircus/kanren/examples/PalProd.scala
diff --git a/src/info/hircus/kanren/examples/SendMoreMoney.scala b/shared/src/main/scala/info/hircus/kanren/examples/SendMoreMoney.scala
similarity index 100%
rename from src/info/hircus/kanren/examples/SendMoreMoney.scala
rename to shared/src/main/scala/info/hircus/kanren/examples/SendMoreMoney.scala
diff --git a/src/info/hircus/kanren/tests/BranchingSpecification.scala b/shared/src/test/scala/info/hircus/kanren/tests/BranchingSpecification.scala
similarity index 100%
rename from src/info/hircus/kanren/tests/BranchingSpecification.scala
rename to shared/src/test/scala/info/hircus/kanren/tests/BranchingSpecification.scala
diff --git a/src/info/hircus/kanren/tests/MathSpecification.scala b/shared/src/test/scala/info/hircus/kanren/tests/MathSpecification.scala
similarity index 100%
rename from src/info/hircus/kanren/tests/MathSpecification.scala
rename to shared/src/test/scala/info/hircus/kanren/tests/MathSpecification.scala
diff --git a/src/info/hircus/kanren/tests/RunSpecification.scala b/shared/src/test/scala/info/hircus/kanren/tests/RunSpecification.scala
similarity index 100%
rename from src/info/hircus/kanren/tests/RunSpecification.scala
rename to shared/src/test/scala/info/hircus/kanren/tests/RunSpecification.scala
diff --git a/src/info/hircus/kanren/tests/SubstSpecification.scala b/shared/src/test/scala/info/hircus/kanren/tests/SubstSpecification.scala
similarity index 97%
rename from src/info/hircus/kanren/tests/SubstSpecification.scala
rename to shared/src/test/scala/info/hircus/kanren/tests/SubstSpecification.scala
index e09859a..000a243 100644
--- a/src/info/hircus/kanren/tests/SubstSpecification.scala
+++ b/shared/src/test/scala/info/hircus/kanren/tests/SubstSpecification.scala
@@ -40,7 +40,7 @@ object SubstSpecification extends Properties("Substitution") {
/* Utility function */
def remove_right_dups[A](s: List[A]): List[A] = {
if (s.isEmpty) s
- else s.head :: remove_right_dups(s.tail.remove({_ == s.head}))
+ else s.head :: remove_right_dups(s.tail.filterNot({_ == s.head}))
}
property("freshvar") = forAll { (vstr: String) =>
diff --git a/src/info/hircus/kanren/tests/UnifySpecification.scala b/shared/src/test/scala/info/hircus/kanren/tests/UnifySpecification.scala
similarity index 97%
rename from src/info/hircus/kanren/tests/UnifySpecification.scala
rename to shared/src/test/scala/info/hircus/kanren/tests/UnifySpecification.scala
index 5bcee68..8cb7c4a 100644
--- a/src/info/hircus/kanren/tests/UnifySpecification.scala
+++ b/shared/src/test/scala/info/hircus/kanren/tests/UnifySpecification.scala
@@ -43,7 +43,7 @@ object UnifySpecification extends Properties("Unification") {
/* Utility function */
def remove_right_dups[A](s: List[A]): List[A] = {
if (s.isEmpty) s
- else s.head :: remove_right_dups(s.tail.remove({_ == s.head}))
+ else s.head :: remove_right_dups(s.tail.filterNot({_ == s.head}))
}
property("bindonce") = forAll { n: Int =>
diff --git a/shell b/shell
deleted file mode 100755
index e1122b0..0000000
--- a/shell
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-scala -cp bin -i src/shell.scala
diff --git a/src/docs/presentation.rst b/src/docs/presentation.rst
new file mode 100644
index 0000000..6d90816
--- /dev/null
+++ b/src/docs/presentation.rst
@@ -0,0 +1,878 @@
+Logical JVM: Implementing the Mini-Kanren logic system in Scala
+===============================================================
+
+:Author: Michel Alexandre Salim
+
+.. image:: http://i.creativecommons.org/l/by-sa/3.0/us/88x31.png
+ :height: 31px
+ :width: 88px
+ :alt: Creative Commons License
+ :align: center
+
+Navigation
+----------
+
+* Use arrow keys, PgUp/PgDn, and mouse clicks to navigate
+* Press "**C**" for controls, and click the "|mode|" button to switch
+ between presentation and handout/outline modes
+
+.. |mode| unicode:: U+00D8 .. capital o with stroke
+
+
+Abstract
+--------
+
+.. class:: incremental
+
+Mini-Kanren is a simplified implementation of Kanren, a declarative
+logic system, embedded in a pure functional subset of Scheme.
+
+.. class:: incremental
+
+This presentation describes a port to Scala, written for the graduate
+programming language course at Indiana University.
+
+
+Outline
+-------
+
+This presentation is in three sections:
+
+1. `The Mini-Kanren logic system`_
+2. `An overview of Scala`_
+3. `The port`_
+
+The Mini-Kanren logic system
+----------------------------
+
+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
+
+
+Mini-Kanren: References
+-----------------------
+
+ 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
+
+Mini-Kanren: Substitution
+-------------------------
+
+A *substitution* is a set of mappings from logical variables to values\
+[#]_. It is immutable; extending a substitution with a new key-value
+mapping produces a new substitution, with the old substitution remaining
+unchanged\ [#]_.
+
+.. [#] including logical variables
+.. [#] Satisfied by Scheme association lists, or Clojure persistent maps
+
+Mini-Kanren: Goals
+------------------
+
+A *goal* is a function that, given a substitution, returns a stream of
+substitutions. There are two basic goals:
+
+.. class:: incremental
+
+- **succeed** (**#s**) returns a stream containing only the input substitution
+- **fail** (**#u**) returns an empty stream
+
+
+Mini-Kanren: Conditionals
+-------------------------
+
+Four basic conditional constructs:
+
+.. class:: incremental
+
+- cond\ :sup:`e` -- each goal can succeed
+- cond\ :sup:`i` -- each goal can succeed, output is interleaved
+- cond\ :sup:`a` -- a single line, cf. soft-cut. only one goal can succeed
+- cond\ :sup:`u` -- uni-. like cond\ :sup:`a`, but the successful
+ *question* only succeeds once
+
+.. class:: incremental
+
+We'll stick with cond\ :sup:`e` first, and discuss the others in a bit
+
+List predicate (Scheme)
+-----------------------
+::
+
+ (def list?
+ (lambda (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
+
+
+List predicate (Kanren)
+-----------------------
+
+::
+
+ (def list°
+ (lambda (l)
+ (conde
+ ((null° l))
+ ((pair° l)
+ (fresh (d)
+ (cdr° l d)
+ (list° d)))
+ (else #u))))
+
+List predicates
+---------------
+
+Note the differences:
+
+- cond\ :sup:`e` instead of cond
+- cdr\ :sup:`o` instead of cdr
+- relations cannot be nested
+- non-boolean relations take an extra argument
+- relations return goals, not values
+
+Mini-Kanren: infinite goals
+---------------------------
+
+::
+
+ (define any°
+ (lambda (g)
+ (ife g #s
+ (any° g))))
+
+ (define always° (any° #s))
+ (define never° (any° #u))
+
+
+
+An overview of Scala
+--------------------
+
+ Scala is a concise, elegant, type-safe programming language that
+ integrates object-oriented and functional features.\ [#]_
+
+
+.. [#] http://www.scala-lang.org/
+
+Scala: the name
+---------------
+
+ 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.\ [#]_
+
+.. [#] *Scala: A Scalable Language*, by Martin Odersky, Lex Spoon, and Bill Venners
+
+Scala: the authors
+------------------
+
+Scala is developed by the `LAMP group`_ at EPFL, led by Prof. Martin
+Odersky, who previously worked on `Pizza`_ and `Generic Java`_
+
+.. _LAMP group: http://lamp.epfl.ch/
+.. _Pizza: http://pizzacompiler.sourceforge.net/
+.. _Generic Java: http://www.cis.unisa.edu.au/~pizza/gj/
+
+Scala: Pros
+-----------
+
+.. class:: incremental
+
+- runs on the JVM
+- interoperates well with Java
+- and thus with other JVM languages
+- provides functional programming constructs
+- pattern-matching
+- powerful type system
+
+
+Scala: Tail-Call Optimization
+-----------------------------
+
+.. class:: incremental
+
+- function calls in tail position should not grow call stack
+- JVM does not have tailcall instruction
+- JVM functional languages work around this to differing extents
+
+Scala: TCO: self-recursion
+--------------------------
+
+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)
+ }
+
+Scala: TCO: mutual recursion
+----------------------------
+
+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)
+ }
+
+.. class:: incremental
+
+- no mutual TCO (blame Sun)
+- No macros
+- call-by-name provides same power (but not conciseness)
+
+Scala: Objects
+--------------
+
+Objects serve two purposes:
+
+.. class:: incremental
+
+- as a code container (cf. Python modules)
+- in Java, this will be a class with static fields
+- as singletons
+- an object is automatically instantiated exactly once
+
+.. class:: incremental
+
+Let's look at a concrete example
+
+Scala: Objects (cont.)
+----------------------
+
+::
+
+ 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: REPL
+-----------
+
+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: REPL (cont.)
+-------------------
+
+REPL
+~~~~
+
+::
+
+ scala> val v = make_var('hello)
+ v: info.hircus.kanren.MiniKanren.Var = Var('hello,2)
+
+ scala> v = make_var('world)
+ :7: error: reassignment to val
+ v = make_var('world)
+
+.. class:: incremental
+
+Values cannot be reassigned -- use variables for that.
+
+Scala: Pattern matching
+-----------------------
+
+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.
+
+.. class:: incremental
+
+Contrast an implementation of a list-summing function in the three languages:
+
+.. class:: incremental
+
+::
+
+ lsum :: (Num t) => [t] -> t -- this line is optional
+ lsum [] = 0
+ lsum (h:tl) = h + lsum tl
+
+
+Scala: Pattern matching
+-----------------------
+
+.. class:: incremental
+
+::
+
+ # let rec sum list = match list with
+ | [] -> 0
+ | head::tail -> head + sum tail;;
+ val sum : int list -> int =
+ #
+
+.. class:: incremental
+
+::
+
+ scala> def sum(l: List[Int]): Int = l match {
+ | case Nil => 0
+ | case h::tl => h + sum(tl)
+ | }
+ sum: (List[Int])Int
+
+ scala>
+
+
+Scala: scalacheck
+-----------------
+
+*scalacheck*\ [#]_ is a tool for random testing of program properties, with
+ automatic test case generation. It was initially a port of Haskell's
+ *QuickCheck*\ [#]_ library.
+
+.. [#] http://code.google.com/p/scalacheck/
+.. [#] http://hackage.haskell.org/package/QuickCheck-2.1.0.2
+
+Scala: scalacheck examples
+--------------------------
+
+::
+
+ 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 port
+--------
+
+The initial port was done over the course of several weeks; the
+current implementation is a rewrite\ [#]_. 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.
+
+.. [#] original code is lost. moral story: backup (and share online...)
+
+Substitution
+------------
+
+Several choices for substitution:
+
+.. class:: incremental
+
+- List[(Var, Any)] --> equivalent to ((Var,Any),Subst)
+- linked triples: (Var, Any, Subst)
+- immutable maps
+
+Substitution (cont.)
+--------------------
+
+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.
+
+.. class:: incremental
+
+- consider memory usage
+- in Scala, triples are more than twice faster
+- immutable maps ==> heap OOM
+
+
+Constraints
+-----------
+
+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).
+
+Constraints (cont.)
+-------------------
+
+.. class:: incremental
+
+- simple substitutions have no-op constraint methods
+- constraint substitutions delegate to the simple substitution methods when
+ possible, and layer constraint checking on top
+
+Constraints: code
+-----------------
+
+::
+
+ 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))
+
+Constraints: code
+-----------------
+
+::
+
+ def lookup(v: Var) = s.lookup(v)
+ override def constraints(v: Var) = c_lookup(v, c)
+ def length: Int = s.length
+ }
+
+
+Monadic operator: mplus (Scheme)
+--------------------------------
+
+::
+
+ (define mplus
+ (lambda (a-inf f)
+ (case-inf a-inf
+ (f)
+ ((a) (choice a f))
+ ((a f0) (choice a
+ (lambdaf@ () (mplus (f0) f)))))))
+
+Monadic operator: mplus (Scala)
+-------------------------------
+
+::
+
+ def mplus(a_inf: Stream[Subst],
+ f: => Stream[Subst]): Stream[Subst] =
+ a_inf append f
+
+.. class:: handout
+
+**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.
+
+Monadic operator: mplus\ :sup:`i` (Scheme)
+------------------------------------------
+
+::
+
+ (define mplusi
+ (lambda (a-inf f)
+ (case-inf a-inf
+ (f)
+ ((a) (choice a f))
+ ((a f0) (choice a
+ (lambdaf@ () (mplusi (f) f0)))))))
+
+**mplus**\ :sup:`i` *interleaves* two streams
+
+Monadic operator: mplus\ :sup:`i` (Scala)
+-----------------------------------------
+
+::
+
+ 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))
+ }
+ }
+
+
+Monadic operator: bind (Scheme)
+-------------------------------
+
+::
+
+ (define bind
+ (lambda (a-inf g)
+ (case-inf a-inf
+ (mzero)
+ ((a) (g a))
+ ((a f) (mplus (g a)
+ (lambdaf@ () (bind (f) g)))))))
+
+Monadic operator: bind (Scala)
+------------------------------
+
+::
+
+ def bind(a_inf: Stream[Subst], g: Goal): Stream[Subst] =
+ a_inf flatMap g
+
+.. class:: handout
+
+**bind** is flatMap: it first maps *g* over the stream, and then append the
+resulting streams together.
+
+Monadic operator: bind\ :sup:`i` (Scheme)
+-----------------------------------------
+
+::
+
+ (define bindi
+ (lambda (a-inf g)
+ (case-inf a-inf
+ (mzero)
+ ((a) (g a))
+ ((a f) (mplusi (g a)
+ (lambdaf@ () (bindi (f) g)))))))
+
+Monadic operator: bind\ :sup:`i` (Scala)
+----------------------------------------
+
+::
+
+ 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))
+ }
+ }
+
+Syntax: equality
+----------------
+
+.. |identicals| unicode:: U+003D .. identical sign
+
+In Scheme, (|identicals| x y) is the goal that unifies *x* and *y*; (|notidentical| x y)
+constrains them from being unifiable. The syntax looks natural in
+Scheme, as everything is infix.
+
+.. |notidentical| unicode:: U+00B1 .. not identical sign
+
+.. class:: incremental
+
+In Scala, however, the equivalent looks ugly: *mkEqual(x,y)*;
+*neverEqual(x,y)*. We can introduce infix operations by using implicit
+conversions
+
+Syntax: equality
+----------------
+
+::
+
+ 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)
+
+|identicals| and |notidentical| 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.
+
+Macros
+------
+
+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.
+
+Macros: run
+---------------------
+
+::
+
+ > (run #f (q) (member° q '(a b c d e)))
+ (a b c d e)
+ >
+
+.. class:: handout
+
+ - first arg is number of desired results (#f == all)
+ - specifying the number of results is a Scheme-ism, in a language with
+ more idiomatic support for lazy sequences, **run** can be composed with
+ **take**
+
+Macros: run
+-----------
+
+::
+
+ (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))
+ ())))))
+
+Macros: Run
+-----------
+
+::
+
+ 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
+ }
+
+.. class:: handout
+
+ - not a macro: *v* must be already defined
+ - We use the **map** method of a stream, which produces a lazy stream
+ - It's not idiomatic outside Lisp to have functions that take either
+ #f or some other type. Instead, a negative number is used to
+ collect all results
+
+Macros: fresh
+-------------
+
+::
+
+ (def list°
+ (lambda (l)
+ (conde
+ ((null° l))
+ ((pair° l)
+ (fresh (d)
+ (cdr° l d)
+ (list° d))))))
+
+.. class:: incremental
+
+This differs slightly from the first appearance of *list°*: the (else #u) line is removed,
+as cond\ :sup:`e` fails by default
+
+Macros: fresh
+-------------
+
+::
+
+ 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) } }))
+ }
+
+.. class:: incremental
+
+- unlike a macro, *cond_e* is evaluated at runtime.
+- each line is required to have strictly 2 goals (thus **succeed** is inserted)
+- the **fresh** goal is replaced by a closure. Note *s* is passed to **both**
+
+Macros: project
+---------------
+
+::
+
+ > (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)
+ >
+
+.. class:: handout
+
+ - within the body of the projection, the logic variable *x* is
+ replaced by its bound value
+ - cond\ :sup:`e` successively bind *x* to 7 and 42
+ - the second **project** expression fails after printing 42, thus 42
+ is not in the result list
+
+
+Macros: project
+---------------
+
+::
+
+ 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) }})))
+
+
+
+Debugging
+---------
+
+.. class:: incremental
+
+- property specification allows for easy declaration of test cases
+- can stress-test individual functions, and narrow down possible culprits
+- stack overflow bug found in a combination of elimination and having comments
+
+Debugging (cont.)
+-----------------
+
+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 = {
+ ...
+
+Common pitfalls
+---------------
+
+- when translating a Scheme **fresh** or **project** goal, forgetting
+ to apply the created goal to the input substitution
+- higher-order functions: functional parameter must be followed by *_*
+- Variadic functions: if arg array is converted internally to arg list,
+ must convert back to arg array when recurring
+
+
+Benchmarks: Petite Chez Scheme
+----------------------------------------
+
+::
+
+ > (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))
+
+
+Benchmarks: Scala (association list)
+----------------------------------------------
+
+::
+
+ 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()...
+
+Benchmarks: Scala (case class)
+----------------------------------------
+
+::
+
+ 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()...
+
+Conclusion
+----------
+
+TODO list
+---------
+
+.. class:: incremental
+
+- parallelization: cf. pmap\ [#]_
+- the problem is that we don't want to precompute too many answers, so
+ unlike a list pmap, a stream pmap will have to precompute only a
+ fixed number of elements
+- Prolog benchmarks from the full Kanren
+
+.. [#] Erlang implementation: http://lukego.livejournal.com/6753.html
+
+Clojure
+-------
+
+.. class:: incremental
+
+- MK Scala already uses Clojure's implementation of persistent maps
+- Scala-native implementation scheduled to be available in version 2.8
+- Using Clojure will allow measurement of the performance hit entailed in
+ using functions over macros
+
+The port: Downloads
+-------------------
+
+The Scala port is available under the BSD license from GitHub\ [#]_.
+The latest Kanren source is available on Sourceforge\ [#]_.
+
+.. [#] http://github.com/hircus/minikanren-scala
+.. [#] http://kanren.sourceforge.net/
+
+Q & A
+---------
+
+Your questions, suggestions, etc. are welcome! The project bug tracker is
+at the GitHub address.