

readFile :: FilePath -> IO String -- read in a file

-- read in a file

writeFile :: FilePath -> String -> IO () -- write out a file

-- write out a file

getArgs :: IO [String] -- get the command line arguments, from the module System.Environment

-- get the command line arguments, from the module System.Environment

putStrLn :: String -> IO () -- write out a string, followed by a new line, to the console





main :: IO ()

main = do

src <- readFile "file.in"

writeFile "file.out" (operate src)



operate :: String -> String

operate = ... -- your code here



file.in

file.out



main :: IO ()

main = do

x1 <- expr1

x2 <- expr2

...

xN <- exprN

return ()



do

xI <- exprI

return ()

xI <- return (exprI)



main :: IO ()

main = do

[arg1,arg2] <- getArgs

src <- readFile arg1

res <- return (operate src)

_ <- writeFile arg2 res

return ()



_ <-



_ <- x can be rewritten as x .

If the penultimate line doesn't have a binding arrow ( <- ) and is of type IO (), then the return () can be removed.

x <- return y can be rewritten as let x = y (provided you don't reuse variable names).





main :: IO ()

main = do

[arg1,arg2] <- getArgs

src <- readFile arg1

let res = operate src

writeFile arg2 res





title :: String -> IO ()

title str = do

putStrLn str

putStrLn (replicate (length str) '-')

putStrLn ""





main :: IO ()

main = do

title "Hello"

title "Goodbye"



return x



readArgs :: IO (String,String)

readArgs = do

xs <- getArgs

let x1 = if length xs > 0 then xs !! 0 else "file.in"

let x2 = if length xs > 1 then xs !! 1 else "file.out"

return (x1,x2)





main :: IO ()

main = do

(arg1,arg2) <- readArgs

src <- readFile arg1

let res = operate src

writeFile arg2 res





main :: IO ()

main = do

xs <- getArgs

if null xs then do

putStrLn "You entered no arguments"

else do

putStrLn ("You entered " ++ show xs)





main :: IO ()

main = do

let x = title "Welcome"

x

x

x



x <-

x



replicateM_ :: Int -> IO () -> IO ()

replicateM_ n act = do

if n == 0 then do

return ()

else do

act

replicateM_ (n-1) act





main :: IO ()

main = do

let x = title "Welcome"

replicateM_ 3 x





sequence_ :: [IO ()] -> IO ()

sequence_ xs = do

if null xs then do

return ()

else do

head xs

sequence_ (tail xs)



return ()

head xs



replicateM_ :: Int -> IO () -> IO ()

replicateM_ n act = sequence_ (replicate n act)





sequence_ :: [IO ()] -> IO ()

sequence_ xs =

if null xs then

return ()

else do

head xs

sequence_ (tail xs)





sequence_ :: [IO ()] -> IO ()

sequence_ [] = return ()

sequence_ (x:xs) = do

x

sequence_ xs





main :: IO ()

main = do

xs <- getArgs

sequence_ (map operateFile xs)



operateFile :: FilePath -> IO ()

operateFile x = do

src <- readFile x

writeFile (x ++ ".out") (operate src)



operate :: String -> String

operate = ...





Write lots of Haskell code.



Read chapters 8 and 9 of Programming in Haskell by Graham Hutton. You should expect to spend about 6 hours thinking and contemplating on sections 8.1 to 8.4 (I recommend going to a hospital A&E department with a minor injury).



Read Monads as Containers, an excellent introduction to monads.



Look at the documentation on the monad laws, and find where I've used them in this tutorial.



Read through all the functions in Control.Monad, try to define them, and then use them when writing programs.



Implement and use a state monad.



This tutorial explains how to perform IO in Haskell , without attempting to give any understanding of monads. We start with the simplest example of IO, then build up to more complex examples. You can either read the tutorial to the end, or stop at the end of any section - each additional section will let you tackle new problems. We assume basic familiarity with Haskell, such as the material covered in chapters 1 to 6 of Programming in Haskell by Graham Hutton In this tutorial I use four standard IO functions:The simplest useful form of IO is to read a file, do something, then write out a file.This program gets the contents of, runs the operate function on it, then writes the result to. The main function contains all the IO operations, while operate is entirely pure. When writing operate you do not need to understand any details of IO. This pattern of IO was sufficient for my first two years of programming Haskell.If the pattern described in Simple IO is insufficient, the next step is a list of actions. A main function can be written as:The main function starts with, then has a sequence ofstatements, and ends with. Each statement has a pattern on the left of the arrow (often just a variable), and an expression on the right. If the expression is not of type IO, then you must write. The return function takes a value, and wraps it in the IO type.As a simple example we can write a program that gets the command line arguments, reads the file given by the first argument, operates on it, then writes out to the file given by the second argument:As before, operate is a pure function. The first line after the do uses a pattern match to extract the command line arguments. The second line reads the file specified by the first argument. The third line uses return to wrap a pure value. The fourth line provides no useful result, so we ignore it by writingThe action list pattern is very rigid, and people usually simplify the code using the following three rules:With these rules we can rewrite our example as:So far only the main function has been of type IO, but we can create other IO functions, to wrap up common patterns. For example, we can write a utility function to print nice looking titles:We can use this title function multiple times within main:The functions we've written so far have all been of type IO (), which lets us perform IO actions, but not give back interesting results. To give back the value x, we writeas the final line of the do block. Unlike the imperative language return statement, this return must be on the final line.This function returns the first two command line arguments, or supplies default values if fewer arguments are given. We can now use this in the main program from before:Now, if less than two arguments are given, the program will use default file names instead of crashing.So far we've only seen a static list of IO statements, executed in order. Using if, we can choose what IO to perform. For example, if the user enters no arguments we can tell them:For optional IO you make the final statement of the do block an if, then under each branch continue the do. The only subtle point is that the else must be indented by one more space than the if. This caveat is widely considered to be a bug in the definition of Haskell, but for the moment, the extra space before the else is required.If you've gone from understanding no IO to this point in the tutorial, I suggest you take a break (a cookie is recommended). The IO presented above is all that imperative languages provide, and is a useful starting point. Just as functional programming provides much more powerful ways of working with functions by treating them as values, it also allows IO to be treated as values, which we explore in the rest of the tutorial.The next stage is to work with IO as values. Until now, all IO statements have been executed immediately, but we can also create variables of type IO. Using our title function from above we can write:Instead of running the IO with, we have placed the IO value in the variable x, without running it. The type of x is IO (), so we can now writeon a line to execute the action. By writing the x three times we perform the action three times.We can also pass IO values as arguments to functions. In the previous example we ran the IO action three times, but how would we run it fifty times? We can write a function that takes an IO action, and a number, and runs the action that number of times:This definition makes use of optional IO to decide when to stop, and recursion to continue performing the IO. We can now rewrite the previous example as:In an imperative language the replicateM_ function is built in as a for statement, but the flexibility of Haskell allows us to define new control flow statements - a very powerful feature. The replicateM_ function defined in Control.Monad is like ours, but more general, and can be used instead.We've seen IO values being passed as arguments, so it's natural that we can also put IO in structures such as lists and tuples. The function sequence_ takes a list of IO actions, and executes each action in turn:If there are no elements in the list then sequence_ stops, with. If there are elements in the list then sequence_ gets the first action (with) and executes it, then calls sequence_ on the remaining actions. As before, sequence_ is available in Control.Monad , but in a more general form. It is now simple to rewrite replicateM_ in terms of sequence_:A much more natural definition of sequence_, rather than using null/head/tail, is to make use of Haskell's pattern matching. If there is exactly one statement in a do block, you can remove the do. Rewriting sequence_ we can eliminate the do after the equals sign, and the do after the then keyword.Now we can replace the if with pattern matching, without needing to consider the IO:As a final example, imagine we wish to perform some operation on every file given at the command line. Using what we have already learnt, we can write:A Haskell program usually consists of an outer IO shell calling pure functions. In the previous example main and operateFile are part of the IO shell, while operate and everything it uses are pure. As a general design principle, keep the IO layer small. The IO layer should concisely perform the necessary IO, then delegate to the pure part. Use of explicit IO in Haskell is necessary, but should be kept to a minimum - pure Haskell is where the beauty lies.You should now be equipped to do all the IO you need. To become more proficient I recommend any of the following: