We know that in Prolog – non-deterministic predicates are a feature used to whittle down combinatorial problems.
In Haskell we see some similar non-deterministic behaviour to Prolog in the List Monad.
In Haskell we also see non-determinism in choosing thunk evaluation order:
But there is nothing telling GHC which of those thunks should be evaluated first, and therefore GHC is at full liberty to choose whichever thunk to evaluate first.
This is fascinating – and somewhat liberating. I'm wondering (apart from noodling with logic problems like eight-queens) what principle this serves. Was there some big idea or big problem they were trying to solve with non-determinism?
My question is: What is the reasoning behind making non-determinism a feature of Haskell?
Best Answer
While it is true that both aspects cited in the questions appear as forms of non-determinism, they are indeed quite different both in how they work and in their goals. Hence any answer has to be necessarily split in two parts.
Evaluation order
Haskell mandates no particular execution order in the evaluation of thunks essentially because of two reasons.
unsafePerformIO
& co.). This means that the evaluation of any expression, e.g.f x
will result in the same outcome no matter how many times it is evaluated and no matter in which part of the program it is evaluated (assumingf
andx
bind to the same values in the considered scopes, of course). Hence mandating any particular order of execution would have no purpose, because changing it would not produce any observable effects in the outcome of the program. In this regard, this is not really a form of nondeterminism, at least not any form of observable one, since the different possible executions of the program are all semantically equivalent.Changing order of execution can, however, have an effect on the performance of the program, and leaving the compiler the freedom of manipulating the order at its will is fundamental to achieve the amazing performance that a compiler like GHC can obtain compiling such an high-level language. As an example, think about a classic stream-fusion transformation:
This equality simply means that applying two functions to a list with
map
has the same outcome than applying once the composition of the two functions instead. This is only true because of referential transparency, and is a kind of transformation that the compiler can always apply, no matter what. If changing the order of execution of the three functions had effects on the result of the expression, this would not be possible. On the other hand, compiling it in the second form instead of the first may have a huge performance impact, because it avoids the construction of one temporary list and only traverses the list once. The fact that GHC can automatically apply such transformation is a direct consequence of referential transparency and the non-fixed order of execution and it is one of the key aspects of the great performance that Haskell can achieve.To see what I mean by ease of composition, consider an example when you have a function
producer :: Int -> [Int]
that performs some complex task to compute a list of some sort of data from an input argument, andconsumer :: [Int] -> Int
which is another complex function computing a result from a list of input data. You have separately written them, tested them, optimized them very carefully, and used them in isolation in different projects. Now in the next project it happens that you have to callconsumer
on the result ofproducer
. In a non-lazy language this may be non-optimal, since it may be the case that the combined task may be most efficiently implemented without building a temporary list structure. To obtain an optimized implementation you would have to reimplement the combined task from scratch, re-test it, and re-optimize it.In haskell this is not needed, and calling the composition of the two functions
consumer . producer
is perfectly fine. The reason is that the program is not required to actually produce the whole result ofproducer
before giving it toconsumer
. In fact, as soon asconsumer
will need the first element of its input list, then the corresponding code fromproducer
will run as far as needed to produce it, and no more. When the second element is needed, it will be computed. If some element will not be needed byconsumer
, it will not be computed at all, effectively saving useless computations. The execution ofconsumer
andproducer
will effectively be interleaved, not only avoiding the memory usage of the intermediate list structure, but also possibly avoiding useless computations, and the execution would probably be similar to the hand-written combined version that you had to write yourself otherwise. This is what I meant by composition. You had two well-tested and performant pieces of code and you could compose them obtaining for free a well-tested and performant piece of code.Nondeterministic monads
The use of nondeterministic behavior provided by the List monads and similar ones is instead totally different. Here the point is not that of providing the compiler with means of optimizing your program, but clearly and concisely express computations that are inherently nondeterministic.
An example of what I mean is provided by the interface of the
Data.Boolean.SatSolver
library. It provides a very simple DPLL SAT solver implemented in Haskell. As you may know, solving the SAT problem involves finding an assignment of boolean variables that satisfies a boolean formula. There may be more than one such assignments, however, and one may need to find any of them, or iterate over all of them, depending on the application. Hence, many libraries will have two different functions likegetSolution
andgetAllSolutions
. This library instead has only one function,solve
, with the following type:Here, the result is a
SatSolver
value wrapped inside a monad of unspecified type, which however is constrained to implement theMonadPlus
type class. This type class is the one that represent the kind of nondeterminism provided by the list monad, and in fact lists are instances. All the functions that operate onSatSolver
values returns their results wrapped into aMonadPlus
instance. So suppose you have the formulap || !q
and you want to solve it by restricting on the results that setq
true, then the usage is the following (variables are numbered instead of being identified by name):Note how the monad instance and the do notation mask all the low-level details of how the functions manages the
SatSolver
data structure, and lets us express clearly our intent.Now, if you want to obtain all the results, you simply use
solve
in a context where its result has to be a list. The following will print all the results on screen (assuming aShow
instance forSatSolver
, which does not exist, but forgive me this point).However, lists are not the only instances of
MonadPlus
.Maybe
is another instance. So if you just need one solution, no matter which one, you can just usesolve
as if it returned aMaybe SatSolver
value:Now suppose that you have two tasks so built,
task
andtask2
, and you want to obtain a solution to either one. Once again everything comes together to let us compose our pre-existing building blocks:where
<|>
is a binary operation provided by theAlternative
typeclass, which is a super class ofMonadPlus
. This again let us clearly express our intent, reusing code without changes. The nondeterminism is clearly expressed in code, not buried under the details of how the nondeterminism is actually implemented. I suggest you to take a look at the combinators built on top of theAlternative
type class to get further examples.The nondeterministic monads like lists are not just a way to express nice exercises but offers a way to design elegant and reusable code that clearly expresses intent in the implementation of tasks which are inherently nondeterministic.