I got a new job where I am hacking some Scala. I thought I would learn something by translating some functional code into Scala, and a friend had recently pointed me to Kiselyov et al.’s Backtracking, Interleaving, and Terminating Monad Transformers, which provides a foundation for Prolog-style logic programming. Of course, a good translation should use the local idiom. So in this post (and the next) I want to explore an embedded domain-specific language for logic programming in Scala.
A search problemHere is a problem I sometimes give in interviews:
Four people need to cross a rickety bridge, which can hold only two people at a time. It’s a moonless night, so they need a light to cross; they have one flashlight with a battery which lasts 60 minutes. Each person crosses the bridge at a different speed: Alice takes 5 minutes, Bob takes 10, Candace takes 20 minutes, and Dave 25. How do they get across?
I’m not interested in the answer—I’m interviewing programmers, not law school applicants—but rather in how to write a program to find the answer.
The basic shape of the solution is to represent the state of the world (where are the people, where is the flashlight, how much battery is left), write a function to compute from any particular state the set of possible next states, then search for an answer (a path from the start state to the final state) in the tree formed by applying the next state function transitively to the start state. (Here is a paper describing solutions in Prolog and Haskell.)
Here is a first solution in Scala:
object Bridge0 {
object Person extends Enumeration {
type Person = Value
val Alice, Bob, Candace, Dave = Value
val all = List(Alice, Bob, Candace, Dave) // values is broken
}
import Person._
val times = Map(Alice -> 5, Bob -> 10, Candace -> 20, Dave -> 25)
case class State(left: List[Person],
lightOnLeft: Boolean,
timeRemaining: Int)
We define an enumeration of people (the Enumeration
class is a bit broken in Scala 2.8.1), a map of the time each takes to cross, and a case class to store the state of the world: the list of people on the left side of the bridge (the right side is just the complement); whether the flashlight is on the left; and how much time remains in the flashlight.
def chooseTwo(list: List[Person]): List[(Person,Person)] = {
val init: List[(Person, Person)] = Nil
list.foldLeft(init) { (pairs, p1) =>
list.foldLeft(pairs) { (pairs, p2) =>
if (p1 < p2) (p1, p2) :: pairs else pairs
}
}
}
This function returns the list of pairs of people from the input list. We use foldLeft
to do a double loop over the input list, accumulating pairs (p1, p2)
where p1 < p2
; this avoids returning (Alice, Bob)
and also (Bob, Alice)
. The use of foldLeft
is rather OCamlish, and if you know Scala you will complain that foldLeft
is not idiomatic—we will repair this shortly.
In Scala, Nil
doesn’t have type 'a list
like in OCaml and Haskell, but rather List[Nothing]
. The way local type inference works, the type variable in the type of foldLeft
is instantiated with the type of the init
argument, so you have to ascribe a type to init
(or explicitly instantiate the type variable with foldLeft[List[(Person,
Person)]]
) or else you get a type clash between List[Nothing]
and List[(Person, Person)]
.
def next(state: State): List[State] = {
if (state.lightOnLeft) {
val init: List[State] = Nil
chooseTwo(state.left).foldLeft(init) {
case (states, (p1, p2)) =>
val timeRemaining =
state.timeRemaining - math.max(times(p1), times(p2))
if (timeRemaining >= 0) {
val left =
state.left.filterNot { p => p == p1 || p == p2 }
State(left, false, timeRemaining) :: states
}
else
states
}
} else {
val right = Person.all.filterNot(state.left.contains)
val init: List[State] = Nil
right.foldLeft(init) { (states, p) =>
val timeRemaining = state.timeRemaining - times(p)
if (timeRemaining >= 0)
State(p :: state.left, true, timeRemaining) :: states
else
states
}
}
}
Here we compute the set of successor states for a state. We make a heuristic simplification: when the flashlight is on the left (the side where everyone begins) we move two people from the left to the right; when it is on the right we move only one. I don’t have a proof that an answer must take this form, but I believe it, and it makes the code shorter.
So when the light is on the left we fold over all the pairs of people still on the left, compute the time remaining if they were to cross, and if it is not negative build a new state where they and the flashlight are moved to the right and the time remaining updated.
If the light is on the right we do the same in reverse, but choose only one person to move.
def tree(path: List[State]): List[List[State]] =
next(path.head).
map(s => tree(s :: path)).
foldLeft(List(path)) { _ ++ _ }
def search: List[List[State]] = {
val start = List(State(Person.all, true, 60))
tree(start).filter { _.head.left == Nil }
}
}
A list of successive states is a path (with the starting state at the end and the most recent state at the beginning); the state tree is a set of paths. The tree rooted at a path is the set of paths with the input path as a suffix. To compute this tree, we find the successor states of the head of the path, augment the path with each state in turn, recursively find the tree rooted at each augmented path, then append them all (including the input path).
Then to find an answer, we generate the state tree rooted at the path consisting only of the start state (everybody and the flashlight on the left, 60 minutes remaining on the light), then filter out the paths which end in a final state (everybody on the right).
For-comprehensionsTo make the code above more idiomatic Scala (and more readable), we would of course use for-comprehensions, for example:
def chooseTwo(list: List[Person]): List[(Person,Person)] =
for { p1 <- list; p2 <- list; if p1 < p2 } yield (p1, p2)
Just as before, we do a double loop over the input list, returning pairs where p1 < p2
. (However, under the hood the result list is constructed by appending to a ListBuffer
rather than with ::
, so the pairs are returned in the reverse order.)
The for-comprehension syntax isn’t specific to lists. It’s syntactic sugar which translates to method calls, so we can use it on any objects which implement the right methods. The methods we need are
def filter(p: A => Boolean): T[A]
def map[B](f: A => B): T[B]
def flatMap[B](f: A => T[B]): T[B]
def withFilter(p: A => Boolean): T[A]
where T
is some type constructor, like List
. For List
, filter
and map
have their ordinary meaning, and flatMap
is a map
(where the result type must be a list) which concatenates the resulting lists (that is, it flattens the list of lists).
WithFilter
is like filter
but should be implemented as a “virtual” filter for efficiency—for List
it doesn’t build a new filtered list, but instead just keeps track of the filter function; this way multiple adjacent filters can be combined and the result produced with a single pass over the list.
The details of the translation are in the Scala reference manual, section 6.19. Roughly speaking, <-
becomes flatMap
, if
becomes filter
, and yield
becomes map
. So another way to write chooseTwo
is:
def chooseTwo(list: List[Person]): List[(Person,Person)] =
list.flatMap(p1 =>
list.filter(p2 => p1 < p2).map(p2 => (p1, p2)))
So far we have taken a concrete view of the choices that arise in searching the state tree, by representing a choice among alternatives as a list. For example, in the chooseTwo
function we returned a list of alternative pairs. I want now to take a more abstract view, and define an abstract type T[A]
to represent a choice among alternatives of type A
, along with operations on the type, packaged into a trait:
trait Logic { L =>
type T[A]
def fail[A]: T[A]
def unit[A](a: A): T[A]
def or[A](t1: T[A], t2: => T[A]): T[A]
def apply[A,B](t: T[A], f: A => B): T[B]
def bind[A,B](t: T[A], f: A => T[B]): T[B]
def filter[A](t: T[A], p: A => Boolean): T[A]
def split[A](t: T[A]): Option[(A,T[A])]
A fail
value is a choice among no alternatives. A unit(a)
is a choice of a single alternative. The value or(t1, t2)
is a choice among the alternatives represented by t1
together with the alternatives represented by t2
.
The meaning of apply
ing a function to a choice of alternatives is a choice among the results of applying the function to each alternative; that is, if t
represents a choice among 1
, 2
, and 3
, then apply(t, f)
represents a choice among f(1)
, f(2)
, and f(3)
.
Bind
is the same except the function returns a choice of alternatives, so we must combine all the alternatives in the result; that is, if t
is a choice among 1
, 3
, and 5
, and f
is { x => or(unit(x), unit(x + 1)) }
, then bind(t, f)
is a choice among 1
, 2
, 3
, 4
, 5
, and 6
.
A filter
of a choice of alternatives by a predicate is a choice among only the alternatives which pass the the predicate.
Finally, split
is a function which returns the first alternative in a choice of alternatives (if there is at least one) along with a choice among the remaining alternatives.
def or[A](as: List[A]): T[A] =
as.foldRight(fail[A])((a, t) => or(unit(a), t))
def run[A](t: T[A], n: Int): List[A] =
if (n <= 0) Nil else
split(t) match {
case None => Nil
case Some((a, t)) => a :: run(t, n - 1)
}
As a convenience, or(as: List[A])
means a choice among the elements of as
. And run
returns a list of the first n
alternatives in a choice, picking them off one by one with split
; this is how we get answers out of a T[A]
.
case class Syntax[A](t: T[A]) {
def map[B](f: A => B): T[B] = L.apply(t, f)
def filter(p: A => Boolean): T[A] = L.filter(t, p)
def flatMap[B](f: A => T[B]): T[B] = L.bind(t, f)
def withFilter(p: A => Boolean): T[A] = L.filter(t, p)
def |(t2: => T[A]): T[A] = L.or(t, t2)
}
implicit def syntax[A](t: T[A]) = Syntax(t)
}
Here we hook into the for-comprehension notation, by wrapping values of type T[A]
in an object with the methods we need (and |
as an additional bit of syntactic sugar), which methods just delegate to the functions defined above. We arrange with an implicit conversion for these wrappers to spring into existence when we need them.
Now we can rewrite the solution in terms of the Logic
trait:
class Bridge(Logic: Logic) {
import Logic._
We pass an implementation of the logic monad in, then open it so the implicit conversion is available (we can also use T[A]
and the Logic
functions without qualification).
The Person
, times
, and State
definitions are as before.
private def chooseTwo(list: List[Person]): T[(Person,Person)] =
for { p1 <- or(list); p2 <- or(list); if p1 < p2 }
yield (p1, p2)
As we saw, we can write chooseTwo
more straightforwardly using a for-comprehension. In the previous version we punned on list
as a concrete list and as a choice among alternatives; here we convert one to the other explicitly.
private def next(state: State): T[State] = {
if (state.lightOnLeft) {
for {
(p1, p2) <- chooseTwo(state.left)
timeRemaining =
state.timeRemaining - math.max(times(p1), times(p2))
if timeRemaining >= 0
} yield {
val left =
state.left.filterNot { p => p == p1 || p == p2 }
State(left, false, timeRemaining)
}
} else { // ...
This is pretty much as before, except with for-comprehensions instead of foldLeft
and explicit consing. (You can easily figure out the branch for the flashlight on the right.)
private def tree(path: List[State]): T[List[State]] =
unit(path) |
(for {
state <- next(path.head)
path <- tree(state :: path)
} yield path)
def search(n: Int): List[List[State]] = {
val start = List(State(Person.all, true, 60))
val t =
for { path <- tree(start); if path.head.left == Nil }
yield path
run(t, n)
}
}
In tree
we use |
to adjoin the input path (previously we gave it in the initial value of foldLeft
). In search
we need to actually run the Logic.T[A]
value rather than returning it, because it’s an abstract type and can’t escape the module (see the Postscript for an alternative); this is why the other methods must be private
.
We can recover the original solution by implementing Logic
with lists:
object LogicList extends Logic {
type T[A] = List[A]
def fail[A] = Nil
def unit[A](a: A) = a :: Nil
def or[A](t1: List[A], t2: => List[A]) = t1 ::: t2
def apply[A,B](t: List[A], f: A => B) = t.map(f)
def bind[A,B](t: List[A], f: A => List[B]) = t.flatMap(f)
def filter[A](t: List[A], p: A => Boolean) = t.filter(p)
def split[A](t: List[A]) =
t match {
case Nil => None
case h :: t => Some(h, t)
}
}
A choice among alternatives is just a List
of the alternatives, so the semantics we sketched above are realized in a very direct way.
The downside to the List
implementation is that we compute all the alternatives, even if we only care about one of them. (In the bridge problem any path to the final state is a satisfactory answer, but our program computes all such paths, even if we pass an argument to search
requesting only one answer.) We might even want to solve problems with an infinite number of solutions.
Next time we’ll repair this downside by implementing the backtracking monad from the paper by Kiselyov et al.
See the complete code here.
Postscript: modules in ScalaI got the idea of implementing the for-comprehension methods as an implict wrapper from Edward Kmett’s functorial library. It’s nice that T[A]
remains completely abstract, and the for-comprehension notation is just sugar. I also tried an implementation where T[A]
is bounded by a trait containing the methods:
trait Monadic[T[_], A] {
def map[B](f: A => B): T[B]
def filter(p: A => Boolean): T[A]
def flatMap[B](f: A => T[B]): T[B]
def withFilter(p: A => Boolean): T[A]
def |(t: => T[A]): T[A]
def split: Option[(A,T[A])]
}
trait Logic {
type T[A] <: Monadic[T, A]
// no Syntax class needed
This works too but the type system hackery is a bit ugly, and it constrains implementations of Logic
more than is necessary.
Another design choice is whether T[A]
is an abstract type (as I have it) or a type parameter of Logic
:
trait Logic[T[_]] { L =>
// no abstract type T[A] but otherwise as before
}
Neither alternative provides the expressivity of OCaml modules (but see addendum below): with abstract types, consumers of Logic
cannot return values of T[A]
(as we saw above); with a type parameter, they can, but the type is no longer abstract.
In OCaml we would write
module type Logic =
sig
type 'a t
val unit : 'a -> 'a t
(* and so on *)
end
module Bridge(L : Logic) =
struct
type state = ...
val search : state list L.t
end
and get both the abstract type and the ability to return values of the type.
Addendum
Jorge Ortiz points out in the comments that it is possible to keep T[A]
abstract and also return its values from Bridge
, by making the Logic
argument a (public) val
. We can then remove the private
s, and write search
as just:
def search: T[List[State]] = {
val start = List(State(Person.all, true, 60))
for { path <- tree(start); if path.head.left == Nil }
yield path
}
instead of baking run
into it. Now, if we write val b = new Bridge(LogicList)
then b.search
has type b.Logic.T[List[b.State]]
, and we can call b.Logic.run
to evaluate it.
This is only a modest improvement; what’s still missing, compared to the OCaml version, is the fact that LogicList
and b.Logic
are the same module. So we can’t call LogicList.run(b.search)
directly. Worse, we can’t compose modules which use the same Logic
implementation, because they each have their own incompatibly-typed Logic
member.
I thought there might be a way out of this using singleton types—the idea is that a match of a value v
against a typed pattern where the type is w.type
succeeds when v eq w
(section 8.2 in the reference manual). So we can define
def run[A](
Logic: Logic,
b: Bridge,
t: b.Logic.T[A],
n: Int): List[A] =
{
Logic match {
case l : b.Logic.type => l.run(t, n)
}
}
which is accepted, but sadly
scala> run[List[b.State]](LogicList, b, b.search, 2)
<console>:8: error: type mismatch;
found : b.Logic.T[List[b.State]]
required: b.Logic.T[List[b.State]]
run[List[b.State]](LogicList, b, b.search, 2)
^
Addendum addendum
Some further advice from Jorge Ortiz: the specific type of Logic
(not just Logic.type
) can be exposed outside Bridge
either through polymorphism:
class Bridge[L <: Logic](val Logic: L) {
...
}
val b = new Bridge(LogicList)
or by defining an abstract value (this works the same if Bridge
is a trait):
abstract class Bridge {
val Logic: Logic
...
}
So we can compose uses of T
but it remains abstract.
A great post, thanks very much! The one thing I found confusing at the end was this comment: "with a type parameter, they can, but the type is no longer abstract."
ReplyDeleteDon't the clients have the choice whether the type can remain abstract or not? It seems to me that a client could have the form:
class Bridge[T[_]](l: Logic[T]) {...}
and so up the chain of clients; the type only needs to be fixed at the last possible moment.
You are right, but at the top of the chain somebody (other than the implementation of Logic) needs to know the type, which should be hidden from everybody inside the implementation of Logic.
ReplyDeleteExcellent post! This was a lot of fun.
ReplyDeleteThe abstract type can indeed escape the module (as in OCaml), but in this example the abstract type is also a dependent type: it depends on Bridge's "Logic" value. For the abstract type to escape the module, the "logic" value can't be private. If you make Bridge's Logic public:
class Bridge(val Logic: Logic) { ... }
then Bridge's methods can have return types of Logic.T[List[State]]
Hey, nice, thanks for the tip! I will amend the post.
ReplyDelete