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/) +==================================================== + +[![Build Status](https://travis-ci.org/aborg0/minikanren-scala.svg?branch=sbt)](https://travis-ci.org/aborg0/minikanren-scala) +[![Coverage Status](https://coveralls.io/repos/github/aborg0/minikanren-scala/badge.svg?branch=sbt)](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.