As an exercise, I tried to construct an infinite, lazy list, recursively, inside a monad.
Basically, the equivalent of nat
, but monadic, monat
:
nat :: Int -> [Int]
nat n = n : nat (n 1)
monat :: Monad m => Int -> m [Int]
monat n = return n >>= \n -> (n:) <$> monat (n 1)
But monat
isn't really lazily evaluated: it's impossible to get the first elements without computing it all:
ghci> take 5 $ nat 0
[0,1,2,3,4]
ghci> take 5 <$> (monat 0) :: Maybe [Int]
... never ending computation ...
I suspect the issue must lie in fmap
or >>=
, since they're the only extra functions I'm using there but not in nat
. However I looked at their implementation and I don't see why or where laziness is broken:
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
instance Monad Maybe where
(Just x) >>= k = k x
Nothing >>= _ = Nothing
Can you help me understand why the list in
monat
isn't computed lazily?Is there any way to understand why the computation doesn't end, besides analyzing the code? For instance, can I do something like pausing evaluation in GHCi and look at the few latest stack trace frames in order to understand where it's looping? How?
CodePudding user response:
The reason for the infinite loop is simply that fmap
/<$>
for the Maybe
functor needs to know whether the input is Nothing
or Just
. Because it only applies the function if there is a Just
otherwise it will propagate the Nothing
.
Your function monat
doesn't specify whether the result should be Nothing
or Just
. Both are theoretically fixed points of the equation with which you have defined monat
. More practically, you can think of it as monat
postponing the choice between Nothing
and Just
every recursive call. In that way, it never actually makes the choice.
One way to resolve this problem while still keeping the monadic interface is to use the ListT
monad transformer. This monad transformer is like a normal list but interspersed with a monad. You can define monat
like this:
import Control.Monad.Trans.Class (lift)
import qualified ListT -- from the list-t package
import ListT (ListT)
monat :: Monad m => Int -> ListT m Int
monat n = lift (return n) >>= \m -> cons m (monat (m 1))
And you can use that like this:
ghci> ListT.toList (ListT.take 5 (monat 0) :: ListT Maybe Int)
Just [0,1,2,3,4]