There may have been some misconceptions due to the way I started this series in part one. The concept of monads is not the key to performing IO in a purely functional way, they are what makes it practical. IO in Haskell is performed by the IO System, which performs one big IO Action (Main.main) when the Program is run. This IO Action is build up of smaller IO Actions, much like a pure function can be build out of smaller functions. IO Actions are represented by the IO Type.
A value of type IO a is an IO Action that, when performed, does some IO and returns a value of type a. The IO type is completely abstract, so there is no way to create a value of type IO a in pure code. You have to build it out of other values by using the power of monads.
getChar :: IO Char putChar :: Char -> IO ()
These are two very basic functions: getChar is an IO Action that, when performed, does some IO and returns a Char (e. g. reading from stdin) and putChar takes a Char, returns an IO Action that, when performed, does some IO with the char (e. g. writing it to stdout) and returns () (unit). An IO action is just a value in the Haskell Typesystem so we can pass it around from function to function in a purely functional way. The only way to actually perform an IO action is to bind it to Main.main.
module Main where main :: IO () main = getChar >>= putChar
We use (>>=) to build one big IO Action out of two smaller ones exactly like we did in the State Monad. In fact, you can think of the IO Monad as a State Monad which passes around the state of the realworld.
What are the benefits of this approach to IO?
- We don't loose the great modularity that functional programming gives us. We build IO Actions out of small parts, pass them around and bind one big IO Action representing the application that we want to write to main to perform it when the program is run.
- The Typesystem makes it impossible to have sideeffects ruin our pure code. IO Actions are values that represent nonpure computations. Functions like getChar and putChar return IO Actions, they are not themselves IO Actions. They are pure functions because they return always the same IO Action (given the same arguments). This IO Action may give different results depending on the context they are performed in.
This is a rather brief explanation of the IO Monad but I think it is enough to get the point. There are a lot of much more detail explanations if you are interested.