When you do computations involving IO, you want to be in IO context.
For instance, let's say we want to count the amount of rows in a file, and we're going to use readFile for this
First, lets look at the types
readFile :: FilePath -> IO String
This is a computation with an IO result, which means we should only access it by IO actions.
computeLines :: String -> Int
computeLines = length . lines
Lines is an example computation for calculating the amount of lines in a file. It's composed of the function lines :: String -> [String] which splits a string into a list of strings, divided by line break.
This is the function we want to use the result of readFile on, but as it's IO, the type system won't let us. However, there's hope still
countLines :: FilePath -> IO Int
countLines fp = do
file <- readFile fp
return $ computeLines file
By the powers of the do notation and IO actions we can compose the functions and still live in the comfortable context of IO actions.
If you're not used to IO computations, this snippet will contain some concepts that probably are new to you:
* The "do" notation; it's a syntactic sugar for doing sequential computations within a type, like IO actions.
* The left pointing arrow (<-) is a type-safe way to extract a contextual value (like IO String) into a computable value (like String).
* return packages a computation (like Int) into a contextual value (like IO Int).
So what we do is fetching the result from readFile with <-, computes it (computeLines file) and package it from an Int to an IO Int.
Best regards,
Jonathan