Think of a value with type (IO String) as "a computation that interacts with the outside world and then returns a value of type String", and similarly (IO ()) is "a computation that interacts with the outside world and then does not return a value. putStrLn has type IO (). getLine has type IO String.
Now, imagine that you want to build up a whole program that interacts with the outside world. One easy way to do this is to sequence together a number of these primitive operations. The monadic bind operator is Haskell's way of sequencing together computations that support the monad interface (which IO does). A simple console echo program could be written as "main = getLine >>= putStrLn", using the >>= operator to feed the result of getLine into putStrLn. The two computations, sequenced together, form the definition of the whole program.
The monadic bind operator gets awkward if you're trying to combine lots of these statements or if you need to assign their results to variables, and so do-notation was added to the language:
main = do
youTyped
This is because a monad functions just like an overridable semicolon: it's a way for you to define how two computations, specified in sequence, will execute. For the IO type, this is specified by the language runtime to be "execute the first, then execute the second", but it doesn't have to be this. For the Maybe type, for example, it's "Execute the first, but only execute the second if the result of the first is not Nothing", and for the Error type, it's "Execute the first, but propagate the error if it returns one."
A key point the article is making but fails to mention is that these values of the IO type are just ordinary values. The "normal" thing to do is execute them in sequence, but you don't have to do that. You can, for example, map putStrLn over a list and then sequence that with mapM:
main = mapM putStrLn [1,2,3,4]
Or you could just map putStrLn over a list, and then pass the resulting list of IO values around to some other filter for further processing before sequencing. A somewhat roundabout way of defining FizzBuzz, for example, would be:
subst (i, a)
| i `mod` 15 == 0 = putStrLn "FizzBuzz"
| i `mod` 5 == 0 = putStrLn "Buzz"
| i `mod` 3 == 0 = putStrLn "Fizz"
| otherwise = a
makePrint i = (i, putStrLn $ show i)
main = sequence_ $ map (subst . makePrint) [1..100]
See what I did there? The numbers 1 through 100 are each first passed through makePrint, which pairs them with an IO action to print their string equivalent. The result of
that is passed to subst, which filters out multiples of 3, 5, and 15 to instead become actions that print "Fizz", "Buzz", and "FizzBuzz". The original putStrLn calls are dropped on the floor, or more accurately, never executed because Haskell is lazy. Finally, all of the resulting actions are sequenced together with sequence_, which is a standard library function to execute a bunch of monads and throw away the result. The result of putStrLn is just a
value, and can be manipulated like any other value.
This is how many of the cooler monadic libraries (eg. stateful CGI programming with WASH, parser combinators in Parsec, or bytecode JIT generation with LLVM) are implemented.