Haskell IO
Input/Output vs. Pure Functions
Haskell is a pure functional language.
- The only effect of a function call is to return a result value.
- There are no side effects.
- Global variables cannot be changed.
- Data structures cannot be modified.
But, IO naturally changes the state of the world.
- Contents of files are modified.
- File pointers are advanced.
- Different results are produced by each call to an IO action.
The Haskell IO Action Concept
In order to model IO using a functional approach, Haskell focuses on the idea of IO actions as values.
IO a
represents IO actions that return a value of typea
.IO String
is the type of the IO actiongetLine
.IO ()
represents IO actions that return an empty tuple (avoid
value in other languages).
Stdio Functions
For example, here are the type signatures of some standard Haskell IO functions.
putChar :: Char -> IO ()
Write a character to the standard output device (same as hPutChar stdout
).
putStr :: String -> IO ()
Write a string to the standard output device (same as hPutStr stdout
).
putStrLn :: String -> IO ()
The same as putStr
, but adds a newline character.
print :: Show a => a -> IO ()
Print to standard output a value of a showable type a
.
getChar :: IO Char
Read a character from the standard input device (same as hGetChar stdin
).
getLine :: IO String
Read a line from the standard input device (same as hGetLine stdin
).
Sequencing IO Actions
Haskell IO actions can be composed using the operators >>
and
>>=
.
(>>) :: IO a -> IO b -> IO b
(>>=) :: IO a -> (a -> IO b) -> IO b
Using the >>
operator takes two IO actions and returns a new IO action.
action1 >> action2
is an IO action that means:- first perform
action1
, ignoring the result - then perform
action2
, returning the result.
- first perform
How is >>=
different? The first result is not ignored; it is passed as an argument to the second action.
Executing IO actions
- IO actions are not executed during evaluation of Haskell function calls.
- Instead, Haskell executes only the IO action defined by the
main
program.
main :: IO ()
So, a Haskell Hello World
program could be written:
main = putStrLn "Hello" >> putStrLn "World"
Passing Results of IO Actions Along
The second operator >>=
(also called bind
) is more interesting.
(>>=) :: IO a -> (a -> IO b) -> IO b
This operator lets us take the result of an IO action and pass it into a function which produces another IO action.
Using >>=
we can build a simple interactive IO action.
main = putStrLn "Before we get started, what is your name?"
>> getLine
>>= \name -> putStrLn ("Hello, " ++ name ++ "!")
Why can't we write:
putStrLn ("Hello, " ++ getLine ++ "!")
Because the result type of getLine
is IO String
, not String
.
Do Notation
In order to simplify programming with the operators (>>
) and (>>=
), we can use do
notation instead.
For example, the interactive program above can be translated to use
do
syntax as follows.
main = do
putStrLn "Before we get started, what is your name?"
name <- getLine
putStrLn ("Hello, " ++ name ++ "!")
This use of do
syntax allows you to use an imperative programming style in a purely functional language!
Programming with IO Actions
The standard function readFile
produces an IO action reads and returns the contents of a file given its name.
*Main> :t readFile readFile :: FilePath -> IO String
Here FilePath
is just the file name as a String
.
Now consider the task of reading and appending together the contents from a list of files. Gow do we write a function readFiles
to do this?
First, what is the type of readFiles
? It is a function that takes a list of file names (strings) as its arguments and returns an IO action.
readFiles :: [String] -> IO String
If the list of filenames is empty, then the appended file contents that should be produced is just the empty string.
readFiles [] = return ""
Here return
is just the function that wraps a value into a monadic type.
*Main> :t return "" return "" :: Monad m => m [Char]
Now consider the recursive case.
readFiles (file1: moreFiles) =
... process file1
... call readFiles moreFiles
How do we put things together? We cannot use append
directly, but must use the >>=
operator to pass
results to a function that produces the composite IO action.
readFiles [] = return ""
readFiles (file1: moreFiles) =
readFile file1 >>=
\contents1 -> readFiles moreFiles >>=
\moreContents -> return (contents1 ++ moreContents)
Another way of programming this is to use the do notation:
readFiles [] = return ""
readFiles (file1: moreFiles) =
do contents1 <- readFile file1
do moreContents <- readFiles moreFiles
return (contents1 ++ moreContents)
Alternatively:
readFiles [] = return ""
readFiles (file1: moreFiles) =
do contents1 <- readFile file1
moreContents <- readFiles moreFiles
return (contents1 ++ moreContents)
Summary
- As a pure functional language, a Haskell function cannot have the side effect commonly associated with input-output operations.
- Instead, Haskell functions can produce IO actions as values.
- IO actions can be chained together with (
>>
) and (>>=
) to give composite IO actions. - Haskell programs are executed by defining one
main
IO action and performing that action. - The
do
syntax allows all of this to be expressed in a familiar imperative programming style.