Day 8: LogicT — A List Transformer
The list monad lets you perform non-deterministic searches for a solution to a problem. At each step of your program, you can provide a choice of possible values to bind to a variable, and the rest of the program will run several times, trying each possible value and collecting all the results from every run into one thing. That is what >>=
is for in []
.
Here’s a pretty contrived problem: we’re going to search all of the integers to see if there is an integer greater than 100.
First we need a list of all the integers, which we’ll get by combining the positives, zero and the negatives.
positives = [1..]
negatives = map (* (-1)) [1..]
the_numbers = positives ++ [0] ++ negatives
Now we can run a simple program to find the first ( head
) solution:
a_solution = head $ do
n <- the_numbers
guard (n > 100)
return n
We’ll go over all of the integers, and for each one, if it matches our search condition, we’ll return it, or otherwise we’ll abort this part of the search with no results.
> a_solution
101
That answer is pretty obvious.
Let’s use the same technique to search for an integer less than 0. Can you think of such an integer? Hopefully. Here’s the program, just modified a little bit with our new condition:
a_solution = head $ do
n <- the_numbers
guard (n < 0)
return n
Alas, it hangs rather than giving an answer:
> a_solution
...infinite wait...
This program checks the_numbers
in order, and we defined it using ++
as first having all the (infinite number of) positive numbers, then 0, then the (infinite number of negatives). We won’t ever consider any negative numbers as possible solutions until we’ve checked all the positive numbers. ++
is unfairly favouring the left hand side over the right hand side.
Fairer Disjunction
How about we use interleave
instead of ++
?
> the_numbers = positives `interleave` [0] `interleave` negatives
> take 10 the_numbers
[1,-1,0,-2,2,-3,3,-4,4,-5]
We’ve lost some of the ordering that we had before: our three lists are now all jumbled up with each other. But that means our search program will find a solution (on the second attempt, even).
List Monad Transformers
Lots of monads have a transfomer version (sometimes, even, the basic version in terms of the transformer and Identity
) — is there is sensible one for []
? So that we can get (for example) ListT IO
which would act sometimes like a list and sometimes like IO
.
Each step in ListT IO
could run an IO action that returns many values, and the rest of the program will be run for each of those. Similarly, there could be mplus
that works like ++
but on ListT IO
actions.
A very simple implementation says a ListT IO a
action is an embedded IO [a]
action which runs and returns a list, and have >>=
and mplus
that work as in a regular list.
Like the earlier pure example, this works badly in the case of infinite lists: if the IO
action that produces a list runs forever (producing values as it goes), the next step of the program is not even attempted once! The action needs to finish before it can return even the start of the result list — we’re far too strict in the first step.
A related problem arises if we try the “interleave
instead of ++
” trick — we would need our three IO actions producing the three parts to have run for infinitely long before we can make the first test. Not only would we not find any negative answers, we wouldn’t find any positive ones either.
LogicT to the rescue
This, broadly, is what LogicT
sets out to do. It introduces a new primitive, msplit
, with roughly this type signature: (very simplified)
msplit :: LogicT m a => LogicT m (Maybe (a, LogicT m a))
This works a little bit like a case
match on a list: given a pure list, you can case match and get either the empty list or a pair: the first element of the list, and the rest of the list.
Given a LogicT m
action a
, runningmsplit a
will either give you Nothing
(like the empty list), or an element and a new action — analogous to the first element and (an action that will provide) the rest of the list. It will run the action for long enough to produce a single element, and then return control back to you.
Now an interleave
can be defined which takes elements alternately from two LogicT
actions and returns them interspersed with each other, without needing either of the supplied actions to fully complete — each one only gets to run long enough to produce one answer before the other one gets a chance. Here’s some pseudo-code based on the LogicT paper listed at the bottom:
interleave :: LogicT m a -> LogicT m a -> LogicT m a
interleave l r = do
s <- msplit l
case s of
Nothing -> r -- we've reached the end of l so all that
-- remains is r
Just (s, rest) -> return s `mplus` interleave r rest
-- we got a value, so return it, and then interleave
-- the rest, starting with r first
There is a pile of other interesting stuff that msplit
facilitates, covered in section 3 of the paper listed at the bottom — for example a fair version of >>=
(did you know it wasn’t fair?) and some search pruning operations.
See Also
Here’s the paper I usually read when I’m trying to understand LogicT. I found it pretty dense reading: LogicT.pdf
Day 6 of this blog, on conduits, also dealt with infinite lists of stuff and how to stick together programs which want to work with them — but the interface was very different to this.