IO is a bad example; its instance of Monad normally exists in native non-Haskell code (or in Haskell-runtime-specific code) rather than in Haskell. Another good reason that talking about the "IO monad" early on doesn't work out well. So, I'll start with a couple of non-IO examples and then provide a suggestion for how IO works.
Let's start with Maybe. The Maybe type looks like this:
data Maybe a = Just a | Nothing
You often use Maybe to represent functions that only handle some inputs:
safeHead :: [a] -> Maybe a
safeHead (x:_) = Just x
safeHead _ = Nothing
> safeHead [1,2,3]
Just 1
> safeHead []
Nothing
The Maybe monad lets you assemble a series of operations of type Maybe into a single compound function of type Maybe. If any of the individual operations produce Nothing, the whole function produces Nothing.
safeAddHeads :: Num a => [a] -> [a] -> Maybe a
safeAddHeads l1 l2 = do
v1 safeAddHeads [1,2,3] [2,4,6]
Just 3
> safeAddHeads [1,2,3] []
Nothing
That do notation get translated to the following uses of bind and return:
safeAddHeads l1 l2 =
safeHead l1 >>= \v1 ->
safeHead l2 >>= \v2 ->
return v1 + v2
Bind (>>=) implements this behavior, of obtaining a value and feeding it into the next combination, or returning Nothing:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Just a >>= f = f a
Nothing >>= _ = Nothing
Return takes a plain value and makes it a monadic value, so it's just the "Just" constructor:
return :: a -> Maybe a
return = Just
(You could also write that as "return a = Just a", but you'll often see Haskell functions written in "point-free style", which means omitting explicit parameter names when not needed.)
So, that's bind and return for Maybe.
For a more unusual example, lists are a monad. The list monad acts like a cross-product: for every value in the list, do the remaining operations. For instance:
pairs :: [a] -> [b] -> [(a,b)]
pairs l1 l2 = do
v1 pairs [1,2,3] [4,5]
[(1,4),(1,5),(2,4),(2,5),(3,4),(3,5)]
If that looks a lot like a list comprehension, that's not a coincidence.
Return, again, is easy:
return :: a -> [a]
return a = [a]
Bind implements the behavior of running the right-hand side for each item on the left and combining the results into a list:
(>>=) :: [a] -> (a -> [b]) -> [b]
(x:xs) >>= f = f x ++ (xs >>= f)
[] >>= _ = []
Note that the list bind operation calls itself recursively here: first call f on the first item, then call it on the rest. This could also be written with a fold or map; for instance:
xs >>= f = concatMap f xs
concatMap just runs concat on the result of map. The map applies f (of type a -> [b]) to each item in the list (type [a]); since f returns a list, that results in a list of lists ([[b]]), which concat combines into a single list ([b]).
You might want to take a look at the Writer, Reader, and State monads for further examples. Note in particular that those three maintain some internal state information inside the monad, and in addition to the bind and return operations, they offer operations that work with that internal state.
Having given those examples, here's a sketch of how IO could work. IO operations act as though they change some state (the outside world), and if you chain them together, the second operation acts on the state resulting from the first (for instance, if you call getLine twice, the first modifies the state of stdin by reading a line from it, and the second starts where the first left off by reading another line). That state isn't actually representable in Haskell, but let's imagine that it is.
So, the IO type (to focus on the terminology suggested by this article) contains a value along with an operation changing the state of the world:
data IO a = IOPrivate (StateOfTheWorld -> StateOfTheWorld) a
Notice that a value of IO type contains an operation changing the state of the world, rather than an actual state of the world. It helps to think of an IO type as representing an operation to be run, and main is the top-level operation that actually gets run when you invoke the program. Continuing that analogy, something outside your program, provided by the Haskell runtime, does this:
run :: IO () -> StateOfTheWorld
run (IOPrivate f ()) = f initialStateOfTheWorld
(Compare this to functions like runState, for instance.)
Return takes a non-IO value and makes it into an IO value that doesn't actually change the state of the world:
return :: a -> IO a
return a = IOPrivate id a
Bind sequences the changes to the state of the world:
(>>=) :: IO a -> (a -> IO b) -> IO b
(IOPrivate sf1 v1) >>= f =
let IOPrivate sf2 v2 = f v1
in IOPrivate (sf2 . sf1) v2
("sf" here stands for "state function".)