edu.umass.cs.cs691f.hw

backtrack

package backtrack

Introduction

This assignment revisits unification. Earlier, you'd implemented a specialized version of unification for type inference. In this assignment, you'll use a more general implementation of unification to build a backtracking search procedure, which forms a monad.

Backtracking

You've seen some sophisticated monads in class, such as parser combinators. Streams are a very simple monad that can be used to implement backtracking search. If s is a stream, you should think of s.flatMap(f) as non-deterministically selecting an element of the stream s to pass to f.

For example the following is a stream of numbers divisible by 3:

val divByThree: Stream[Int] = for {
  i <- Stream.from(1)
  if i % 3 == 0
} yield i

The line i <- ... selects an arbitrary number, but if i % 3 == 0 backtracks if the chosen i is not divisible by three.

The for notation above is syntactic sugar for the expression:

Stream.from(1).filter(i => i % 3 == 0).map(i)

You may realize that Stream.from(1) returns numbers in ascending order. Here is a variant that truly picks a random number instead number:

val randomGen = new scala.util.Random()
val randomInts : Stream[Int] = Stream.continually(randomGen.nextInt)
val divByThree1: Stream[Int] = for {
  i <- randomInts
  if i % 2 == 0
} yield i
Logic Variables

The following stream defines a (child, parent) relationship between pairs of persons:

sealed abstract class Root
case class Person(str: String) extends Root

val parent: Stream[(Person, Person)] =
 Stream((Person("Vito"), Person("Dom")),
        (Person("Sonny"), Person("Vito")),
        (Person("Michael"), Person("Vito")),
        (Person("Fredo"), Person("Vito")),
        (Person("Sophia"), Person("Micheal")),
        (Person("Tony"), Person("Michael")))

We can query the stream with the following functions:

def parentIs(x: Person) = for {
  (x1, y1) <- parent
  if x == x1
} yield (x1, y1)

def childIs(y: Person) = for {
  (x1, y1) <- parent
  if y == y1
} yield (x1, y1)

However, these functions are not composable and do not scale--if we had a three-place relation, we would have to write at least six different queries to follow the pattern above.

Instead, we can define a single query that consumes logic variables as arguments:

case class Var(sym : Sym) extends Root with HasSym

// Study this definition carefully.
def rel(x: Root, y: Root): Stream[(Root, Root)] = parent.flatMap {
  case (x1, y1) => Unification.unify(x, x1) match {
    case None => Stream.empty
    case Some(subst) => Unification.unify(subst(y), subst(y1)) match {
      case None => Stream.empty
      case Some(subst1) => {
        val subst2 = subst1.compose(subst)
        Stream((subst2(x), subst2(y)))
      }
    }
  }
}

If x is bound to a Person, then its value is fixed in the result stream. But, if it is bound to a Var, then its value may vary:

def childIs2(x: Person) = rel(Var(Sym.fresh("child")), x)
def parentIs2(x: Person) = rel(x, Var(Sym.fresh("parent")))

But, rel itself is very cumbersome to write. The Stream monad makes backtracking easy, but we have to manually manage the substitution.

Backtracking + Substitution

In this assignment, you'll build (a portion of) a monad that packages backtracking and substitution. Using this monad, the person relation will just be the following:

def parent(p1: Root, p2: Root): Search[Unit] =
(constrain(p1, Person("Vito")) && constrain(p2, Person("Dom"))) ||
(constrain(p1, Person("Sonny")) && constrain(p2, Person("Vito"))) ||
(constrain(p1, Person("Michael")) && constrain(p2, Person("Vito"))) ||
(constrain(p1, Person("Fredo")) && constrain(p2, Person("Vito"))) ||
(constrain(p1, Person("Sophia")) && constrain(p2, Person("Micheal"))) ||
(constrain(p1, Person("Tony")) && constrain(p2, Person("Michael")))

To run a query, you'll just apply parent to concrete values and logic variables:

val x = Var(Sym.fresh("x"))
val exp = ancestor(x, Person("Vito")).map(_ => x)
assert(Set(exp.take(10) : _*) ==
       Set(Person("Fredo"), Person("Michael"), Person("Sonny"), Person("Tony")))
Template Code

To get started, you should use fill in the following template. The Search constructor takes a single function, f as an argument. This function consumes an input substitution and produces a stream of substitutions-result pairs.

import edu.umass.cs.cs691f._

class Search[A](f: Subst => Stream[(Subst, A)]) {

  def apply(subst : Subst): Stream[(Subst, A)]

  def flatMap[B](k: A => Search[B]): Search[B]

  def map[B](g: A => B) : Search[B]

  def || (other: => Search[A]): Search[A]

  def && [B](other: Search[B]): Search[B]

  def take(n: scala.Int): List[A]
}

object Search {

  def unit[A](x: A): Search[A]

  // Use unification here (and only here).
  def constrain[T](x: T, y: T): Search[Unit]

}
Learning Scala

You'll have to become familar with Scala to do this assignment. There are several resources on the Web that you can use.

I recommend using SBT to build your project. You should add the following lines to your build.sbt file to fetch dependencies:

resolvers += "CS691F repository" at "http://www.cs.umass.edu/~arjun/courses/cs691f/pubjars"
libraryDependencies += "edu.umass.cs.cs691f" % "support" % "1.0"

Alternatively, you can include the .jar file manually.

Reimplement Type Inference

Type inference is nearly trivial to implement using the Search monad. You should try to do so for a small language.

Linear Supertypes
AnyRef, Any
Ordering
  1. Alphabetic
  2. By inheritance
Inherited
  1. backtrack
  2. AnyRef
  3. Any
  1. Hide All
  2. Show all
Learn more about member selection
Visibility
  1. Public
  2. All

Type Members

  1. trait SearchLike[A] extends AnyRef

Inherited from AnyRef

Inherited from Any

Ungrouped