Well, at first I thought I’d call this post “Doing Haskell without a condom” but decided it was probably best not to. I can’t really take full credit for this post, I’ve stolen almost all of it from emails after I posted a question on haskell-cafe. All praise should go to the helpful people there!
What I wanted to do was model a finite stream of external events as a list and then map a function on that list. The processing of an event should happen as soon as an event is encoutered (one could say that the list of events should be lazy). Due to the wording “external events” the solution will have to be in the IO monad. It’s all a bit vague at this point, so let’s take it down to a more concrete level and then at the end make the solution more generic again. Let’s read characters until we read a
q and print them.
First attempt, infinite list
Let’s start with an infinite list:
listChars :: [IO Char] listChars = repeat getChars
Then I can map
printChar like this:
mapM_ (\ x -> x >>= printChar) listChars
The result is almost what I want, each character is printed immediately. The only problem is that there is no end to the stream of characters. This means that the processing never terminates, that’s not a good thing.
Solving the problem with the infinite list is easy if we do it recursively:
processChars :: IO () processChars = do c <- getChar printChar c if c == 'q' then return () else processChars
This is easy to make more generic, but I expressively wanted to map a function on a list. So a recursive solution won’t cut it this time. However, the same pattern can be used to create a list inside the IO monad:
listChars :: IO [Char] listChars = do c <- getChar if c == 'q' then return [c] else liftM2 (:) (return c) listChars
putChar on the result of this doesn’t quite produce the desired result.
listChar >>= mapM_ putChar
It doesn’t print characters as soon as they are entered. That is, it’s not lazy.
Monads have an ordering effect on things. Since
listChar executes in the IO monad, and the result then is passed to
putChar, which also executes in the IO monad, we get a strict ordering between the two functions. What we need is to somehow interleave the IO in
listChars with the IO in
putChar. Put interleave into Hoogle and you’re pointed to
System.IO.Unsafe. We can put it to use like this:
listChars :: IO [Char] listChars = unsafeInterleaveIO $ do c <- getChar if c == 'q' then return [c] else liftM2 (:) (return c) listChars
And now we get the required behaviour. Great!
But there is an even stranger operation called ‘unsafeInterleaveIO’ that gets the “official baton”, makes its own pirate copy, and then runs an “illegal” relay-race in parallel with the main one! I can’t talk further about its behavior without causing grief and indignation, so it’s no surprise that this operation is widely used in countries that are hotbeds of software piracy such as Russia and China! Don’t even ask me – I won’t say anything more about this dirty trick I use all the time
So, I suppose it’s worth the effort to think through just how we put it to use here. Basically one can say that
unsafeInterleaveIO creates a second IO monad, and in order to keep some level of determinism in the program the use of the new one should be kept separate from the use of the main IO monad, i.e. the one that
main executes in. In this case the main IO monad is used to print characters to
stdout while the one created by
unsafeInterleaveIO reads characters from
stdin. According to my limited understanding of IO in haskell this should mean we are safe, the two IO monads run no risk of ever “crossing”.
Joachim Breitner suggested another, very cute solution based on cutting the infinite list short.
listChars = let sequence' (x:xs) = do r <- x rs <- unsafeInterleaveIO (sequence' xs) return (r:rs) in do allChars <- sequence' $ repeat getChar let getChars = takeWhile (/= 'q') allChars return getChars
listIO :: IO [a] -> (a -> Bool) -> IO [a] listIO getF testF = unsafeInterleaveIO $ do c <- getF if testF c then return [c] else liftM2 (:) (return c) listIO listChars = listIO getChar ('q' ==)
listIO :: IO [a] -> (a -> Bool) -> IO [a] listIO getF testF= let sequence' (x:xs) = do r <- x rs <- unsafeInterleaveIO (sequence' xs) return (r:rs) in do allChars <- sequence' $ repeat getF let getA = takeWhile testF allChars return getA listChars = listIO getChar ('q' \=)
Could ListT be used in a solution?
This was one of my first thoughts. I mean what I want is the combination of two monads, List and IO. So,
ListT IO Char should offer a solution right?
In theory it could! In practice it doesn’t! Apparantly ListT imposes unnecessary strictness. That means that it doesn’t offer a solution in this case.