Efficient Fair Search for Lazy FLP

At FLOPS 2008 Oleg Kiselyov showed me FBackTrack — a simple instantiation of the MonadPlus class in Haskell which is fair (like breadth-first search) and seems to consume considerably less memory than BFS. I immediately started following an obvious plan: implement a traversal on Curry’s SearchTree datatype treating choices like mplus and values like return.

Although the resulting search outperformed breadth-first search in some examples, I didn’t get the benefit that I expected from the Haskell experiments. A closer look revealed that the main trick to save memory was the implementation of the bind operator, that a search that doesn’t use bind is a worst case for the algorithm still taking exponential space, and that SearchTrees don’t have an equivalent to bind, unfortunately!

The bind operator takes a nondeterministic computation and passes one of its results to a function supplied as second argument, i.e., bind demands the computation of its first argument. The question arises, how we can model lazy functional-logic programming in Haskell, so instead of writing a traversal for SearchTrees, I tried to figure out how to translate Curry programs to Haskell such that under the hood, FBackTrack is used as search engine. The implementation uses the datatype Stream that is an instance of MonadPlus:

The given program will fail to terminate with the implementation given in FBackTrack.hs and I couldn’t come up with another implementation that handles laziness in a sensible way.

Instead, I concluded to apply a different transformation scheme that uses bind only when the program demands evaluation, e.g., during pattern matching. With this transformation scheme the Haskell and Curry versions of the functions given above are identical (apart from type signatures). To see the difference consider the transformed version of the function not:

Data values are constructor rooted and have an arbitrary number of nondeterministic data values as arguments. The type class Nondet provides operations to convert between arbitrary data types and nondeterministic data values:

At run time, values of type FLP_Bool will be indistinguishable from values of type Stream Data — the newtype constructor will be eliminated. Why do we introduce the type FLP_Bool anyway? The type class Nondet has another method unknown which represents a logic variable and is usually implemented differently for different types:

class Nondet a
where
…
unknown :: a

For example, the instance for FLP_Bool implements unknown as follows:

instance Nondet FLP_Bool
where
…
unknown = choice [false,true]

The function choice is used to build the nondeterministic choice of arbitrary values of the same type:

The problem of the presented transformation scheme is that it does not respect shared nondeterministic choices. For example, a call to main in the following Curry program should give the results (True,True) or (False,False) but neither (True,False) nor (False,True):

main = dup (unknown::Bool)
dup x = (x,x)

The restricted set of results corresponds to what would be evaluated with an eager strategy and we need to compute the same results lazily.

which represents all four pairs of booleans. The information that the same choice should be taken in both components is lost.

Until now, I didn’t find a way to improve on this. Even the side-effectish way of associating unique references to choices in order to detect choices that where duplicated by sharing cannot easily be adopted because of the tricky interleavings in the implementation of the monadic operations that constantly restructure the choices.