Staying out of the IO Monad

Dennis J. McWherter, Jr. bio photo By Dennis J. McWherter, Jr. Comment

I haven€™t had a chance to get to my monad post yet (though I assure you that it is coming) so this may seem a little out-of-order for anyone who is still not entirely familiar with what a monad is. In any case, anyone looking to learn Haskell has probably heard several times, €œtry to stay out of the IO monad€ or €œthe IO monad is impure€ or €œOh noez. IO is evil. Pure all the things!€ Ok, maybe the last one is a bit of an exaggeration, but you know what I mean. I remember when I was learning Haskell (well, we€™re never really done learning, are we?) that these statements caused me a lot of anxiety.

Like any good computer scientist, if I didn€™t understand something I quickly took to Google. Unfortunately, I was unable to come across a good explanation of what this meant. I saw many examples, but when you€™re learning a new language it€™s a bit difficult to parse the nuances in the examples to understand the concept being demonstrated. This article aims to provide a short explanation of what staying out of the IO monad exactly is (with the assumption you€™re at least vaguely familiar with the concept of monad already) and why it€™s useful.

I heard the IO monad is evil (i.e. what is the IO monad?)

No, the IO monad is not evil nor is it inherently malicious. It is, however, impure from a functional sense. What that means to us is that writing code in the IO monad is more complicated to €œget right€ than any bit of pure code. Why is this the case exactly? Well, the whole reason it€™s more complicated is that code in the IO monad can fail in any way. For instance, the application can throw unchecked exceptions, fail to retrieve proper input, or even just exit your application prematurely. The point is, the IO monad can simply fail in any manner possible and you have to be ready to account for this.

Why use the IO monad?

This begs the question: why even use the IO monad if it makes our lives more difficult? I mean, with strong type-checking, our pure code is going to behave in exactly the way we expect (or at least the way we have written in the case of bugs). While this is true, the IO monad is the only thing in Haskell which provides us proper facilities to interact with the outside world. That is, if you want to take input from a source external to the compiled application (i.e. file on disk, data stream over network, etc.) then you use the IO monad. Similarly, if you want to extract any useful information from your application (i.e. print to the screen, write to a file, write to a socket, etc.) this also must be done within the context of the IO monad. As you can see, if you want your machine to do more than some fixed computation (where you can€™t even observe the result), then you must use the IO monad.

In short, the IO monad is what makes Haskell applications useable.

In many ways– although it seems like an unnecessary obstacle at first– the IO monad is a very elegant solution to €œdealing€ with the outside world. I argue that the IO monad does not actually add any additional complexity to your program compared to writing it in some other language without the IO monad. In fact, it may make things simpler. That is, in Java or C++ exceptions can be thrown from any context; disks can be full and network devices may fail, but you must always be aware of these problems occurring at any point in your application. The result is that error-handling persists throughout your entire application. However, the IO monad forces your awareness to these problems (if you understand it). You can write the majority of your code in a pure context (i.e. no undesigned failures) while minimizing the amount of code necessary to be run in the IO monad. As a result, the number of ways your application can unexpectedly fail is vastly decreased and you can simply handle these cases directly in the IO monad. This is €œstaying out€ of the IO monad.

How do I stay out?

If you€™ve actually read the previous paragraph, you will notice that I have introduced this concept of €œstaying out€ of the IO monad. That said, it is worthwhile to reiterate: staying out of the IO monad is simply minimizing the amount of code being run in it. That is, whenever possible use pure types (i.e. no €œIO a€ wrapping). Again, this affords us the luxury of minimizing unexpected failures. But what does this mean exactly? The best way to show this, I believe, is through an example.

Suppose we are writing an application which simply fetches a webpage and trims the response to 100 characters or less starting from an offset of 100 characters. This sounds simple enough, so let€™s see the code:

module Main where
import Control.Monad
import Network.HTTP
import System.Environment

getUri :: [String] -> Maybe String
getUri args = if length args /= 1 then Nothing else Just (head args)

trimResult :: Int -> Int -> String -> String
trimResult offset len body = take len $ drop offset body

main :: IO ()
main = do
  args <- getArgs
  uri <- case getUri args of
          Nothing -> getProgName >>= \x -> fail $ "Usage: " ++ x ++ " <uri_to_fetch>"
          Just uri -> return uri
  let request = getRequest uri
  result <- simpleHTTP request
  case result of
   Left err -> putStrLn $ "An error occurred: " ++ (show err)
   Right response -> putStrLn $ trimResult 100 100 (rspBody response)

The first thing I want to draw your attention to is the fact that the only bit of IO you see in my code is in the main function. This won€™t always be the case, but if you€™re not writing new IO functions or wrappers yourself, this is usually a common pattern (aside from using helper functions to improve readability). For instance, the Network.HTTP library function simpleHTTP must use the IO monad so that it can issue a network request (and thus, it must be called from inside another IO monad). However, since I will just be performing manipulation on the result, I can consolidate this functionality to a single location while manipulating the result in pure contexts.

Similarly, you will recognize that my error handling is pure (i.e. use of Maybe) for checking user input and the related monad fail is also performed within the context of the IO monad. Likewise, according to its signature, our trimResult function only takes in real data. What I mean to say is that it is impossible to compile an application which calls this function with anything that could generate a null pointer exception or some other weird failure; such issues are commonplace in other languages. This allows the core functionality of our application to be easily tested and verified. 

The final takeaway here is that the IO monad should always be at the outer-most layer possible in your application. I know I haven€™t quite gotten to that monad tutorial yet (which will make this bit clearer), but think of it this way: once you€™re in IO, you€™re stuck in IO. In particular, you cannot return a non-IO value from an IO monad (ok, I lie a little bit– but it€™s effectively the same) and this makes it such that everything is in the IO monad. On the other hand, you can easily go the other way. That is, from within the IO monad, you can always pass in a non-IO value to a function which does not take the IO monad. This is exactly what we€™re doing here in our example.

Conclusion

The IO monad is a seemingly mystical and complicated beast when learning to write Haskell applications. People are always claiming you need to €œstay out€ of the IO monad, but this is perplexing since the type of main already starts inside of IO. However, we have discussed precisely what it means to stay out of IO here and how exactly to do that. Similarly, we have explored the utility and elegance of the IO monad in practice and– most importantly– why we should never fear the IO monad. Instead, we respect it and handle potential problems accordingly. Happy IO€™ing!

comments powered by Disqus