Playing unsafe haskell

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.

Recursive (non-)solution

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

Now mapping 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.

Adding laziness

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 unsafeInterleaveIO in 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!

IO inside on the haskell wiki has the following comment on unsafeInterleaveIO:

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”.

Another solution

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

Generalisations

Recursive solution:

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' ==)

Joachim’s solution:

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.

3 Comments

  1. If your event stream is actually the characters of stdin, then getContents already hides the details of the unsafeInterleaveIO for you. As in:

    getContents >>= mapM_ yourFunc . takeWhile (/= ‘q’)

    However, understanding the internals makes it possible to generalize this approach to one where the event stream contains richer data (such as GUI events).

    Perhaps there is more that can be learned from FRP (functional reactive programming)

  2. Alan,

    That’s a very good point. I’ve found that quite a few standard functions are “lazy enough”, just like getContents.

    Thanks for the link to FRP!

  3. Pingback: Is There a Such Thing as Real World Haskell? « The Other Librarian

Comments are closed.