11

I was trying the Cont monad, and discovers the following problem.

  1. First construct a infinite list and lift all the elements to a Cont monad
  2. Use sequence operation to get a Cont monad on the infinite list.
  3. When we try to run the monad, with head, for example, it falls into infinite loop while trying to expand the continuation and the head is never called.

The code looks like this:

let inff = map (return :: a -> Cont r a) [0..]
let seqf = sequence inff
runCont seqf head

So is this a limitation of the Cont monad implementation in Haskell? If so, how do we improve this?

Shiva Wu
  • 1,074
  • 1
  • 8
  • 20
  • 1
    possible duplicate of [Why the Haskell sequence function can't be lazy or why recursive monadic functions can't be lazy](http://stackoverflow.com/questions/14494648/why-the-haskell-sequence-function-cant-be-lazy-or-why-recursive-monadic-functio) – Aadit M Shah Feb 22 '14 at 03:47

2 Answers2

13

The reason is that even though the value of the head element of sequence someList depends only on the first elemenent of someList, the effect of sequence someList can generally depend on all the effects of someList (and it does for most monads). Therefore, if we want to evaluate the head element, we still need to evaluate all the effects.

For example, if we have a list of Maybe values, the result of sequence someList is Just only if all the elements of someList are Just. So if we try to sequence an infinite list, we'd need to examine its infinite number of elements if they're all Just.

The same applies for Cont. In the continuation monad, we can escape any time from the computation and return a result that is different from what has been computed so far. Consider the following example:

test :: (Num a, Enum a) => a
test = flip runCont head $
    callCC $ \esc -> do
        sequence (map return [0..100] ++ [esc [-1]])

or directly using cont instead of callCC:

test' :: (Num a, Enum a) => a
test' = flip runCont head $
            sequence (map return [0..100] ++ [cont (const (-1))])

The result of test is just -1. After processing the first 100 elements, the final element can decide to escape all of this and return -1 instead. So in order to see what is the head element of sequence someList in Cont, we again need to compute them all.

Petr
  • 62,528
  • 13
  • 153
  • 317
7

This is not a flaw with the Cont monad so much as sequence. You can get similar results for Either, for example:

import Control.Monad.Instances ()

xs :: [Either a Int]
xs = map Right [0..]  -- Note: return = Right, for Either

ys :: Either a [Int]
ys = sequence xs

You can't retrieve any elements of ys until it computes the entire list, which will never happen.

Also, note that: sequence (map f xs) = mapM f xs, so we can simplify this example to:

>>> import Control.Monad.Instances
>>> mapM Right [0..]
<Hangs forever>

There are a few monads where mapM will work on an infinite list of values, specifically the lazy StateT monad and Identity, but they are the exception to the rule.

Generally, mapM/sequence/replicateM (without trailing underscores) are anti-patterns and the correct solution is to use pipes, which allows you to build effectful streams that don't try to compute all the results up front. The beginning of the pipes tutorial describes how to solve this in more detail, but the general rule of thumb is that any time you write something like:

example1 = mapM f xs

example2 = sequence xs

You can transform it into a lazy Producer by just transforming it to:

example1' = each xs >-> Pipes.Prelude.mapM f

example2' = each xs >-> Pipes.Prelude.sequence

Using the above example with Either, you would write:

>>> import Pipes
>>> let xs = each [0..] >-> mapM Right :: Producer Int (Either a) ()

Then you can lazily process the stream without generating all elements:

>>> Pipes.Prelude.any (> 10) xs
Right True
Gabriella Gonzalez
  • 34,863
  • 3
  • 77
  • 135
  • 6
    I think calling mapM an anti-pattern in general is a bit harsh. I'd say it's bad only if the list is of unbounded length (for example derived from user input). But there e.g. isn't much wrong with calling mapM on a list of at most 100 elements. – kosmikus Feb 22 '14 at 09:25
  • 5
    The mapM function is not an anti-pattern at all, not any more than effects being an anti-pattern. – augustss Feb 22 '14 at 10:53
  • 3
    Using something with O(N^2) space and time complexity that doesn't stream when a streaming O(N) solution exists is an anti-pattern in my eyes. Also, note that the OP specifically describes this behavior as problematic in their own words, so this statement is relevant in the context of this question. – Gabriella Gonzalez Feb 22 '14 at 17:20
  • Thank you for the information about Pipes, looks great. – Shiva Wu Mar 02 '14 at 02:07