I havent 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, were never really done learning, are we?) that these statements caused me a lot of anxiety.
Like any good computer scientist, if I didnt 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 youre learning a new language its 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 youre at least vaguely familiar with the concept of monad already) and why its 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 its 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 cant 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 youve 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 lets 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 wont always be the case, but if youre 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 havent quite gotten to that monad tutorial yet (which will make this bit clearer), but think of it this way: once youre in IO, youre stuck in IO. In particular, you cannot return a non-IO value from an IO monad (ok, I lie a little bit– but its 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 were doing here in our example.
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 IOing!comments powered by Disqus