Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

checkpoint logic part 2

  • Loading branch information...
commit c43b8e7d7ad8f15f6c688198370850ef3a58d657 1 parent f7d3a9f
Jake Donham authored
32 _code/scala-logic/Logic.scala
View
@@ -48,57 +48,53 @@ object LogicList extends Logic {
}
object LogicSFK extends Logic {
- // type FK[R] = => R
- type SK[A,R] = (A, => R) => R
+ type FK[R] = () => R
+ type SK[A,R] = (A, FK[R]) => R
- trait T[A] { def apply[R](sk: SK[A,R], fk: => R): R }
+ trait T[A] { def apply[R](sk: SK[A,R], fk: FK[R]): R }
def fail[A] =
new T[A] {
- def apply[R](sk: SK[A,R], fk: => R) = fk
+ def apply[R](sk: SK[A,R], fk: FK[R]) = fk()
}
def unit[A](a: A) =
new T[A] {
- def apply[R](sk: SK[A,R], fk: => R) = sk(a, fk)
+ def apply[R](sk: SK[A,R], fk: FK[R]) = sk(a, fk)
}
def or[A](t1: T[A], t2: => T[A]) =
new T[A] {
- def apply[R](sk: SK[A,R], fk: => R) = t1(sk, t2(sk, fk))
+ def apply[R](sk: SK[A,R], fk: FK[R]) = t1(sk, { () => t2(sk, fk) })
}
def bind[A,B](t: T[A], f: A => T[B]) =
new T[B] {
- def apply[R](sk: SK[B,R], fk: => R) =
+ def apply[R](sk: SK[B,R], fk: FK[R]) =
t(({ (a, fk) => f(a)(sk, fk) }: SK[A,R]), fk)
}
def apply[A,B](t: T[A], f: A => B) =
new T[B] {
- def apply[R](sk: SK[B,R], fk: => R) =
+ def apply[R](sk: SK[B,R], fk: FK[R]) =
t(({ (a, fk) => sk(f(a), fk) }: SK[A,R]), fk)
}
def filter[A](t: T[A], p: A => Boolean) =
new T[A] {
- def apply[R](sk: SK[A,R], fk: => R) =
- t(({ (a, fk) => if (p(a)) sk(a, fk) else fk }: SK[A,R]), fk)
+ def apply[R](sk: SK[A,R], fk: FK[R]) =
+ t(({ (a, fk) => if (p(a)) sk(a, fk) else fk() }: SK[A,R]), fk)
}
def split[A](t: T[A]) = {
- def unsplit(r: Option[(A,T[A])]): T[A] =
- r match {
+ def unsplit(fk: FK[Option[(A,T[A])]]): T[A] =
+ fk() match {
case None => fail
case Some((a, t)) => or(unit(a), t)
}
- def unit[A](a: => A) =
- new T[A] {
- def apply[R](sk: SK[A,R], fk: => R) = sk(a, fk)
- }
def sk : SK[A,Option[(A,T[A])]] =
- { (a, fk) => Some(a, bind(unit(fk), unsplit)) }
- t(sk, None)
+ { (a, fk) => Some((a, bind(unit(fk), unsplit))) }
+ t(sk, { () => None })
}
}
18 _code/scala-logic/LogicState.scala
View
@@ -9,6 +9,9 @@ trait LogicState { L =>
def filter[S,A](t: T[S,A], p: A => Boolean): T[S,A]
def split[S,A](s: S, t: T[S,A]): Option[(S,A,T[S,A])]
+ def get[S]: T[S,S]
+ def set[S](s: S): T[S, Unit]
+
def or[S,A](as: List[A]): T[S,A] =
as.foldRight(fail[S,A])((a, t) => or(unit(a), t))
@@ -26,6 +29,7 @@ trait LogicState { L =>
def withFilter(p: A => Boolean): T[S,A] = L.filter(t, p)
def |(t2: => T[S,A]): T[S,A] = L.or(t, t2)
+ def &[B](t2: => T[S,B]): T[S,B] = L.bind(t, { _: A => t2 })
}
implicit def syntax[S,A](t: T[S,A]) = Syntax(t)
@@ -88,6 +92,16 @@ object LogicStateSFK extends LogicState {
{ (s, a, fk) => Some(s, a, byNameUnit(fk).flatMap(unsplit)) }
t(s, sk, None)
}
+
+ def get[S]: T[S,S] =
+ new T[S,S] {
+ def apply[R](s: S, sk: SK[S,S,R], fk: => R) = sk(s, s, fk)
+ }
+
+ def set[S](s: S): T[S,Unit] =
+ new T[S,Unit] {
+ def apply[R](_s: S, sk: SK[S,Unit,R], fk: => R) = sk(s, (), fk)
+ }
}
object LogicStateSKE extends LogicState {
@@ -129,4 +143,8 @@ object LogicStateSKE extends LogicState {
}
catch { case Fail | Finish => lb.result }
}
+
+ def get[S] = { (s, sk) => sk(s, s) }
+
+ def set[S](s: S) = { (_s, sk) => sk(s, ()) }
}
155 _code/scala-logic/Term.scala
View
@@ -0,0 +1,155 @@
+import scala.collection.immutable.HashMap
+
+class Evar[A](val name: String)
+object Evar { def apply[A](name: String) = new Evar[A](name) }
+
+class Env(m: HashMap[Evar[Any],Term[Any]]) {
+ def apply[A](v: Evar[A]) =
+ m(v.asInstanceOf[Evar[Any]]).asInstanceOf[Term[A]]
+ def get[A](v: Evar[A]): Option[Term[A]] =
+ m.get(v.asInstanceOf[Evar[Any]]).asInstanceOf[Option[Term[A]]]
+ def updated[A](v: Evar[A], t: Term[A]) =
+ Env(m.updated(v.asInstanceOf[Evar[Any]], t.asInstanceOf[Term[Any]]))
+
+ override def toString = {
+ "{ " + m.map({ case (k, v) => k.name + " = " + v.toString }).mkString(", ") + " }"
+ }
+}
+object Env {
+ def apply(m: HashMap[Evar[Any],Term[Any]]) = new Env(m)
+ def empty = new Env(HashMap())
+}
+
+trait Term[A] {
+ def unify(e: Env, t: Term[A]): Option[Env]
+ def occurs[B](e: Env, v: Evar[B]): Boolean
+ def subst(e: Env): Term[A]
+ def ground(e: Env): A
+
+ import LogicStateSKE._
+ def =!=(t2: Term[A]): T[Env, Unit] =
+ for {
+ env <- get
+ env2 <-
+ (unify(env, t2) match {
+ case None => fail[Env,Unit]
+ case Some(e) => set(e)
+ })
+ } yield env2
+}
+
+case class VarTerm[A](v: Evar[A]) extends Term[A] {
+ def unify(e: Env, t: Term[A]) =
+ e.get(v) match {
+ case Some(t2) => t2.unify(e, t)
+ case None =>
+ t match {
+ case VarTerm(v2) if (v2 == v) => Some(e)
+ case _ =>
+ if (t.occurs(e, v)) None
+ else Some(e.updated(v, t))
+ }
+ }
+
+ def occurs[B](e: Env, v2: Evar[B]) =
+ e.get(v) match {
+ case Some(t) => t.occurs(e, v2)
+ case None => v2 == v
+ }
+
+ def subst(e: Env) =
+ e.get(v) match {
+ case Some(t) => t.subst(e)
+ case None => this
+ }
+
+ def ground(e: Env) =
+ e.get(v) match {
+ case Some(t) => t.ground(e)
+ case None => throw new IllegalArgumentException("not ground")
+ }
+
+ override def toString = { v.name }
+}
+
+case class LitTerm[A](a: A) extends Term[A] {
+ def unify(e: Env, t: Term[A]) =
+ t match {
+ case LitTerm(a2) => if (a == a2) Some(e) else None
+ case _: VarTerm[_] => t.unify(e, this)
+ case _ => t.unify(e, this)
+ }
+
+ def occurs[B](e: Env, v2: Evar[B]) = false
+ def subst(e: Env) = this
+ def ground(e: Env) = a
+
+ override def toString = { a.toString }
+}
+
+case class Tuple2Term[A,B](_1: Term[A], _2: Term[B]) extends Term[(A,B)] {
+ def unify(e: Env, t: Term[(A,B)]) =
+ t match {
+ case Tuple2Term(_1_, _2_) =>
+ for (e1 <- _1.unify(e, _1_); e2 <- _2.unify(e1, _2_)) yield e2
+ case _: VarTerm[_] => t.unify(e, this)
+ case _ => None
+ }
+
+ def occurs[C](e: Env, v: Evar[C]) = _1.occurs(e, v) || _2.occurs(e, v)
+ def subst(e: Env) = Tuple2Term(_1.subst(e), _2.subst(e))
+ def ground(e: Env) = (_1.ground(e), _2.ground(e))
+
+ override def toString = { (_1, _2).toString }
+}
+
+case class NilTerm[A]() extends Term[List[A]] {
+ def unify(e: Env, t: Term[List[A]]) =
+ t match {
+ case NilTerm() => Some(e)
+ case _: VarTerm[_] => t.unify(e, this)
+ case _ => None
+ }
+
+ def occurs[B](e: Env, v: Evar[B]) = false
+ def subst(e: Env) = this
+ def ground(e: Env) = Nil
+
+ override def toString = { Nil.toString }
+}
+
+case class ConsTerm[A](hd: Term[A], tl: Term[List[A]]) extends Term[List[A]] {
+ def unify(e: Env, t: Term[List[A]]) =
+ t match {
+ case ConsTerm(hd2, tl2) =>
+ for (e1 <- hd.unify(e, hd2); e2 <- tl.unify(e1, tl2)) yield e2
+ case _: VarTerm[_] => t.unify(e, this)
+ case _ => None
+ }
+
+ def occurs[C](e: Env, v: Evar[C]) = hd.occurs(e, v) || tl.occurs(e, v)
+ def subst(e: Env) = ConsTerm(hd.subst(e), tl.subst(e))
+ def ground(e: Env) = hd.ground(e) :: tl.ground(e)
+
+ override def toString = { hd.toString + " :: " + tl.toString }
+}
+
+object Term {
+ implicit def var2term[A](v: Evar[A]): VarTerm[A] = VarTerm(v)
+ //implicit def lit2term[A](a: A): LitTerm[A] = LitTerm(a)
+ implicit def int2term(a: Int): LitTerm[Int] = LitTerm(a)
+ implicit def tuple2term[A,B](ab: Tuple2[Term[A],Term[B]]): Tuple2Term[A,B] =
+ Tuple2Term(ab._1, ab._2)
+ implicit def list2term[A](l: List[Term[A]]): Term[List[A]] =
+ l match {
+ case Nil => NilTerm[A]
+ case hd :: tl => ConsTerm(hd, list2term(tl))
+ }
+}
+
+object Run {
+ import LogicStateSKE._
+
+ def run[A](t: T[Env,Unit], n: Int, tm: Term[A]): List[Term[A]] =
+ LogicStateSKE.run(Env.empty, t, n).map({ case (e, _) => tm.subst(e) })
+}
10 _code/scala-logic/Test.scala
View
@@ -0,0 +1,10 @@
+object Test {
+ import Term._
+ import LogicStateSKE._
+
+ def member[A](x: Term[A], l: Term[List[A]]): T[Env,Unit] = {
+ val hd = Evar[A]("hd"); val tl = Evar[List[A]]("tl")
+ ConsTerm(hd, tl) =!= l &
+ (x =!= hd | member(x, tl))
+ }
+}
217 _posts/2011-04-14-logic-programming-in-scala-2.markdown
View
@@ -1,67 +1,230 @@
---
layout: blogspot
-title: Logic programming in Scala
+title: Logic programming in Scala part 2, backtracking and state
---
-<b>Implementing the logic monad with success/failure continuations</b>
+In the [previous post](/2011/04/logic-programming-in-scala-part-1.html)
+we saw how to write computations in a logic monad, where a "value" is
+a choice among alternatives, and using a value in a computation means
+using the alternatives, in some sense.
+
+Our first implementation of the logic monad represents a choice among
+alternatives as a list, and using a value in a computation means
+running the computation for each alternative. This is OK for some
+problems, but we run into trouble when there are a large (or infinite)
+number of alternatives. For example, a choice among the natural numbers:
+
+{% highlight scala %}
+scala> import LogicList._
+import LogicList._
+
+scala> val nat: T[Int] = unit(0) | nat.map(_ + 1)
+java.lang.NullPointerException
+ ...
+scala> def nat: T[Int] = unit(0) | nat.map(_ + 1)
+nat: LogicList.T[Int]
+scala> run(nat, 10)
+java.lang.StackOverflowError
+ ...
+{% endhighlight %}
+
+With the `val` version, Scala's implementation of recursive values
+shows through: `nat` is initialized to `null` then assigned after the
+right hand side is evaluated, but the evaluation fails since `nat` is
+`null`. With the `def` version we can construct the value (it is a
+function so the recursive use is delayed), but any attempt to use it
+overflows the stack trying to compute all the natural numbers.
+
+Let's repair this with a fancier implementation of the logic monad,
+translated from Kiselyov et al.'s
+[Backtracking, Interleaving, and Terminating Monad Transformers](http://okmij.org/ftp/Computation/LogicT.pdf).
+
+<b>Success and failure continuations</b>
+
+The idea is to represent a choice of alternatives by a function, which
+takes as arguments two functions: a _success continuation_ and a
+_failure continuation_. (Here a continuation is nothing fancy, just a
+function indicating what to do next.)
+
+The success continuation is called with each alternative in the choice
+until there are no more; then the failure continuation is called. (If
+there are no alternatives of course the failure continuation is called
+immediately.) However, in contrast to the list implementation, the
+success continuation is not called eagerly for each alternative, but
+rather on demand as the alternatives are needed.
+
+The key idea is what happens for `t1 | t2`: we pass `t1` a failure
+continuation which calls the success continuation on `t2`, so the
+resulting value calls the success continuation on each alternative of
+both `t1` and `t2`.
+
+Let's see the code:
{% highlight scala %}
object LogicSFK extends Logic {
- // type FK[R] = => R
- type SK[A,R] = (A, => R) => R
+ type FK[R] = () => R
+ type SK[A,R] = (A, FK[R]) => R
+
+ trait T[A] { def apply[R](sk: SK[A,R], fk: FK[R]): R }
+{% endhighlight %}
+
+The continuations can return a result of some arbitrary type `R`. This
+means that the representation function has a rank-2 polymorphic
+type---it takes functions which are themselves polymorphic---which is
+not directly representable in Scala. But we can encode it by making
+the representation function a method.
- trait T[A] { def apply[R](sk: SK[A,R], fk: => R): R }
+The success continuation takes a value of the underlying type, and
+also a failure continuation. Whatever use the computation makes of the
+value, it may eventually fail (by returning `fail` or by using a guard
+in a for-comprehension which is always false); at that point the
+failure continuation is called.
+{% highlight scala %}
def fail[A] =
new T[A] {
- def apply[R](sk: SK[A,R], fk: => R) = fk
+ def apply[R](sk: SK[A,R], fk: FK[R]) = fk()
}
def unit[A](a: A) =
new T[A] {
- def apply[R](sk: SK[A,R], fk: => R) = sk(a, fk)
+ def apply[R](sk: SK[A,R], fk: FK[R]) = sk(a, fk)
}
+{% endhighlight %}
+
+To fail, just call the failure continuation. To succeed with one
+alternative, call the success continuation with the single alternative
+and the passed-in failure continuation---there are no more
+alternatives to try, so if this branch of the computation fails, do
+whatever the caller tells us to do.
+{% highlight scala %}
def or[A](t1: T[A], t2: => T[A]) =
new T[A] {
- def apply[R](sk: SK[A,R], fk: => R) = t1(sk, t2(sk, fk))
+ def apply[R](sk: SK[A,R], fk: FK[R]) = t1(sk, { () => t2(sk, fk) })
}
+{% endhighlight %}
+
+We want to explore the alternatives in both `t1` and `t2`, so we pass
+the success continuation to `t1` (which calls it on each alternative);
+when `t1` is exhausted we pass the success continuation to `t2`;
+finally we fail with the caller's failure continuation.
+
+Since a choice of alternatives is always built up from `or`, you can
+see that the failure continuation means "go back and try the next
+alternative". When there are no more alternatives in the current
+choice, it means "go back to the previous choice and try the next
+alternative". In the jargon of logic programming, an `or` is called a
+_choice point_, and going back to a previous choice to try the next
+alternative is called _backtracking_.
+{% highlight scala %}
def bind[A,B](t: T[A], f: A => T[B]) =
new T[B] {
- def apply[R](sk: SK[B,R], fk: => R) =
+ def apply[R](sk: SK[B,R], fk: FK[R]) =
t(({ (a, fk) => f(a)(sk, fk) }: SK[A,R]), fk)
}
def apply[A,B](t: T[A], f: A => B) =
new T[B] {
- def apply[R](sk: SK[B,R], fk: => R) =
+ def apply[R](sk: SK[B,R], fk: FK[R]) =
t(({ (a, fk) => sk(f(a), fk) }: SK[A,R]), fk)
}
+{% endhighlight %}
+
+For `bind` we want to call `f` on each alternative, so we pass a
+success continuation which calls `f` on its argument `a`. In turn
+`f(a)` returns a choice of alternatives; we pass it the original
+success continuation, and the failure continuation in force at the
+point `a` was generated.
+For `map` things are somewhat simpler, since `f(a)` returns a single
+value rather than a choice of alternatives, so we succeed immediately
+with the returned value.
+
+{% highlight scala %}
def filter[A](t: T[A], p: A => Boolean) =
new T[A] {
- def apply[R](sk: SK[A,R], fk: => R) =
- t(({ (a, fk) => if (p(a)) sk(a, fk) else fk }: SK[A,R]), fk)
+ def apply[R](sk: SK[A,R], fk: FK[R]) =
+ t(({ (a, fk) => if (p(a)) sk(a, fk) else fk() }: SK[A,R]), fk)
}
+{% endhighlight %}
+To filter a choice of alternatives, every time the choice succeeds
+with a value (that is, for each alternative) we see if the value
+satisfies the predicate `p`. If it does, we succeed with that value;
+otherwise we fail (to backtrack and generate the next alternative).
+
+{% highlight scala %}
def split[A](t: T[A]) = {
- def unsplit(r: Option[(A,T[A])]): T[A] =
- r match {
+ def unsplit(fk: FK[Option[(A,T[A])]]): T[A] =
+ fk() match {
case None => fail
case Some((a, t)) => or(unit(a), t)
}
- def unit[A](a: => A) =
- new T[A] {
- def apply[R](sk: SK[A,R], fk: => R) = sk(a, fk)
- }
def sk : SK[A,Option[(A,T[A])]] =
- { (a, fk) => Some(a, bind(unit(fk), unsplit)) }
- t(sk, None)
+ { (a, fk) => Some((a, bind(unit(fk), unsplit))) }
+ t(sk, { () => None })
}
}
{% endhighlight %}
+The point of `split` is to pull a single alternative from a choice,
+returning along with it a choice of the remaining alternatives. For
+the list implementation we just returned the head and tail of the
+list. In this implementation, the alternatives are computed on demand;
+we want to be careful only to do as much computation as needed to pull
+the first alternative
+
+The failure continuation we pass to `t` just returns `None` when there
+are no more alternatives. The success continuation `sk` returns the
+first alternative and a choice of the remaining alternatives (wrapped
+in `Some`).
+
+The tricky part is computing the choice of remaining alternatives:
+We're given the failure continuation `fk`; calling it calls `sk` on
+the next alternative, which ultimately returns `Some(a, t)` where `a`
+is the next alternative, or `None` if there are no more
+alternatives. We repackage this `Option` as a choice of alternatives
+with `unsplit`. So that we don't call `fk` too soon, we call `unsplit`
+via `bind`, which defers it until the resulting choice of alternatives
+is actually used.
+
+Now we can write infinite choices:
+
+{% highlight scala %}
+scala> import LogicSFK._
+import LogicSFK._
+
+scala> val nat: T[Int] = unit(0) | nat.map(_ + 1)
+nat: LogicSFK.T[Int] = LogicSFK$$anon$3@27aea0c1
+
+scala> run(nat, 10)
+res1: List[Int] = List(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
+{% endhighlight %}
+
+This was a pretty complicated way to generate the natural numbers up
+to 10; the point is that we can use this choice of alternatives in
+computations.
+
+Note that this is not quite the same as the similar-looking infinite
+list in Haskell:
+
+{% highlight haskell %}
+nat = 0 : map (+1) nat
+{% endhighlight %}
+
+Each element in this list is computed just once and memoized, but in a
+choice of alternatives no results are memoized; to compute each
+successive number in `nat` all the previous ones must be
+recomputed. So the running time of `run(nat, N)` is O(N<sup>2</sup>).
+
+{% highlight scala %}
+scala> run(nat, 2000).length
+Java.lang.OutOfMemoryError: Java heap space
+ ...
+{% endhighlight %}
+
<b>Backtracking with exceptions</b>
{% highlight scala %}
@@ -110,9 +273,7 @@ object LogicSKE extends Logic {
}
{% endhighlight %}
-<b>Unification</b>
-
-<b>Threading state through the logic monad</b>
+<b>State</b>
{% highlight scala %}
trait LogicState { L =>
@@ -122,6 +283,9 @@ trait LogicState { L =>
// def split[S,A](s: S, t: T[S,A]): Option[(S,A,T[S,A])]
// as before with extra S parameter
+
+ def get[S]: T[S,S]
+ def set[S](s: S): T[S, Unit]
}
{% endhighlight %}
@@ -150,10 +314,11 @@ object LogicStateSKE extends LogicState {
t(s, { (s, a) => if (p(a)) sk(s, a) else throw Fail })
}
- def split[S,A](s: S, t: T[S,A]) =
- throw new Exception("unimplemented")
+ def get[S] = { (s, sk) => sk(s, s) }
+
+ def set[S](s: S) = { (_s, sk) => sk(s, ()) }
- override def run[S,A](s: S, t: T[S,A], n: Int): List[(S,A)] = {
+ def run[S,A](s: S, t: T[S,A], n: Int): List[(S,A)] = {
if (n <= 0) return Nil
val lb = new scala.collection.mutable.ListBuffer[(S,A)]
def sk(s: S, a: A) = {
Please sign in to comment.
Something went wrong with that request. Please try again.