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:
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:
caseclass 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 {
caseNone=> Stream.empty
caseSome(subst) => Unification.unify(subst(y), subst(y1)) match {
caseNone=> Stream.empty
caseSome(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:
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:
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:
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 ofs.flatMap(f)
as non-deterministically selecting an element of the streams
to pass tof
.For example the following is a stream of numbers divisible by 3:
The line
i <- ...
selects an arbitrary number, butif i % 3 == 0
backtracks if the choseni
is not divisible by three.The
for
notation above is syntactic sugar for the expression:You may realize that
Stream.from(1)
returns numbers in ascending order. Here is a variant that truly picks a random number instead number:Logic Variables
The following stream defines a
(child, parent)
relationship between pairs of persons:We can query the stream with the following functions:
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:
If
x
is bound to aPerson
, then its value is fixed in the result stream. But, if it is bound to aVar
, then its value may vary:But,
rel
itself is very cumbersome to write. TheStream
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:
To run a query, you'll just apply
parent
to concrete values and logic variables: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.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: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.