Difference between revisions of "Introduction to Haskell IO/Actions"

From HaskellWiki
Jump to navigation Jump to search
 
(still entering content for first draft.)
Line 70: Line 70:
 
You may be wondering how any Haskell program can do anything useful if it
 
You may be wondering how any Haskell program can do anything useful if it
 
can only run a single IO action. As we saw earlier, IO actions can be
 
can only run a single IO action. As we saw earlier, IO actions can be
very complex. Complicated actions can be built up of many simpler actions.
+
very complex. We can combine many simple actions together to form more
  +
complicated actions. To combine actions together we use a '''do-block'''.
We will see how shortly.
 
  +
  +
A do-block combines together two or more actions into a single action.
  +
When two IO actions are combined the result is an IO action that, when
  +
invoked, performs the first action and then performs the second action.
  +
Here's a simple example.
  +
<haskell>
  +
main :: IO ()
  +
main = do
  +
putStrLn "hello"
  +
putStrLn "world"
  +
</haskell>
  +
Main is an action that prints a line "hello" and then prints a line
  +
"world".
  +
If the first action had any side effects, those effects are visible
  +
to the second action when it is performed. For example, if a file is
  +
written in the first action and read in the second action, the change
  +
to the file will be visible to the read. Remember that IO actions can
  +
return results to the program. The result of a do-block is the result
  +
of the last action in the do block. In our example above, the last
  +
action (<hask>putStrLn "hello"</hask>) doesn't provide a useful result
  +
and so the type of the entire do-block is <hask>IO ()</hask>.
  +
  +
Do-blocks can also make use of the result of one action when constructing
  +
another action. For example:
  +
<haskell>
  +
main:: IO ()
  +
main = do
  +
line <- getLine
  +
putStrLn ("you said: " ++ line)
  +
</haskell>
  +
This example uses the action <hask>getLine</hask> (<hask>getLine :: IO String</hask>) which reads a line of input from the console. The do-block
  +
makes an action that, when invoked, invokes the <hask>getLine</hask>,
  +
takes its result and invokes
  +
the action <hask>putStrLn ("you said: " ++ line)</hask> with the previous
  +
result bound to <hask>line</hask>.
  +
  +
Notice that
  +
an arrow (<hask><-</hask>) is used in the binding and not an equal sign
  +
(as is done when binding with <hask>let</hask> or <hask>where</hask>).
  +
The arrow indicates that the result of an action is being bound. The
  +
type of <hask>getLine</hask> is <hask>IO String</hask>, and the arrow
  +
binds the result of the action to <hask>line</hask> which will be of
  +
type <hask>String</hask>.
  +
  +
We've used do-blocks to combine two actions together. This provides enough
  +
power to combine more actions together:
  +
<haskell>
  +
main :: IO ()
  +
main = do
  +
putStrLn "Enter two lines"
  +
do
  +
line1 <- getLine
  +
do
  +
line2 <- getLine
  +
putStrLn ("you said: " ++ line1 ++ " and " ++ line2)
  +
</haskell>
  +
Since the innermost do-block is an action, it can be combined with
  +
<hask>getLine</hask> to make another action, which can be combined with
  +
<hask>putStrLn "Enter two lines"</hask> to make another more complicated
  +
action. Luckily we don't have to go through all this trouble. Do-blocks
  +
allow multiple actions to be specified in a single block. The meaning
  +
of these multi-action blocks is identical to the nested example above: the bindings are made visible to all successive actions. The previous
  +
example can be rewritten more compactly as
  +
<haskell>
  +
main :: IO ()
  +
main = do
  +
putStrLn "Enter two lines"
  +
line1 <- getLine
  +
line2 <- getLine
  +
putStrLn ("you said: " ++ line1 ++ " and " ++ line2)
  +
</haskell>
  +
  +
Of course we are free to use other Haskell language features when writing
  +
our program. Instead of putting all of our actions in <hask>main</hask>
  +
we may want to factor some common operations out as separate actions or
  +
functions that build actions. For example, we may want to combine
  +
prompting and user input:
  +
<haskell>
  +
promptLine :: String -> IO String
  +
pormptLine prompt = do
  +
putStr prompt
  +
getLine
  +
  +
main :: IO ()
  +
main = do
  +
line1 <- promptLine "Enter a line: "
  +
line2 <- promptLine "And another: "
  +
putStrLn ("you said: " ++ line1 ++ " and " ++ line2)
  +
</haskell>
  +
Here we made a function <hask>promptLine</hask> which returns an action.
  +
The action prints a prompt (using <hask>putStr :: IO ()</hask>, which prints a
  +
string without a newline character) and reads a line from the console.
  +
The result of the action is the result of the last action, <hask>getLine</hask>.
  +
  +
Let's try to write a slightly more helper function that reads two lines
  +
and returns both of them concatenated together:
  +
<haskell>
  +
promptTwoLines :: String -> String -> IO String
  +
promptTwoLines prompt1 prompt2 = do
  +
line1 <- promptLine prompt1
  +
line2 <- promptLine prompt2
  +
line1 ++ " and " ++ line2 -- ??
  +
</haskell>
  +
There's a problem here. We know how to prompt for and read in both
  +
lines of input, and we know how to combine those lines of input, but
  +
we don't have an action that results in the combined string. Remember,
  +
do-blocks combine together actions and the result of the do-block
  +
is the result of the last action. <hask>line1 ++ " and " ++ line2</hask>
  +
is a string, not an action resulting in a string and so cannot be
  +
used as the last line of the do-block. What we need is a way to
  +
make an action that results in a particular value. This is exactly
  +
what the <hask>return</hask> function does. Return is a function that
  +
takes any type of value and makes an action that results in that value.
  +
We can now complete our helper:
  +
<haskell>
  +
promptTwoLines :: String -> String -> IO String
  +
promptTwoLines prompt1 prompt2 = do
  +
line1 <- promptLine prompt1
  +
line2 <- promptLine prompt2
  +
return (line1 ++ " and " ++ line2)
  +
  +
main :: IO ()
  +
main = do
  +
both <- promptTwoLines "First line: " "Second line: "
  +
putStrLn ("you said " ++ both)
  +
</haskell>
  +
In this example <hask>return (line1 ++ " and " ++ line2)</hask> is an
  +
action of type <hask>IO String</hask> that doesn't affect the outside world in any way, but results in a string that combines <hask>line1</hask> and <hask>line2</hask>.
  +
  +
Here's a very important point that many beginners get confused
  +
about: "return" does ''not'' affect the control flow of the program!
  +
Return does ''not'' break the execution of the do-block. Return may
  +
occasionally be used in the middle of a do-block where it doesn't
  +
directly contribute to the result of the do-block.
  +
Return is simply a function that makes
  +
an action whose result is a particular value. In a sense it
  +
''wraps'' up a value into an action.
  +
  +
XXX - let's in do blocks
  +
  +
XXX - using if/case/etc in do-blocks, nested do-blocks
  +
  +
XXX - there's no escape
  +
  +
== Summary ==
  +
* IO actions are used to affect the world outside of the program.
  +
* Actions take no arguments but have a result value.
  +
* Actions are inert until run. Only one IO action in a Haskell program is run (<hask>main</hask>).
  +
* Do-blocks combine multiple actions together into a single action.
  +
* Combined IO actions are executed sequentially with observable side-effects.
  +
* Arrows are used to bind action results in a do-block.
  +
* Return is a function that builds actions. It is ''not'' a form of control flow!

Revision as of 20:21, 16 December 2006

When we're programming in Haskell and we want to do something that has a side effect, something that affects the world in some way, we use actions. Actions are values in the Haskell language, much like the number three, the string "hello world", or the function map. They can be bound to variable names, passed into a function as an argument or be the result of a function. Like all other Haskell values, every action has a type. There are many kinds of actions but we'll start with a very important one called an IO action. These are the actions that can change the world outside of the programming. Here are some examples of IO actions:

  • Print the string "hello" to the console.
  • Read a line of input from the console.
  • Establish a network connection to www.google.com on port 80.
  • Read two lines of input from the terminal, interpret them as numbers, add them together and print out the result.
  • A first-person shooter game that uses mouse movements as input and renders graphics to the screen.

As you can see, IO actions range from the very simple (printing a string) to very comlex (a video game). You may have also noticed that IO actions can also result in a value that can be used by the Haskell program. The point of reading a line of input from the console is to provide data to the program. The type of an action reflects the kind of action (IO) as well as the type of value that it provides as a result (for example String). We say that the action that reads a line of input from the console has the type IO String. In fact, all IO actions will have a type IO a for some result type a. When an action doesn't provide any useful data back to the program the unit type (written ()) is used to denote the result. For programmers familiar with C, C++ or Java, this is similar to the return type of "void" in those languages. The IO actions mentioned above have the following types:

  • Print the string "hello" to the console: IO ()
  • Read a line of input from the console: IO String
  • Establish a network connection to www.google.com on port 80: IO Socket
  • Read two lines of input from the terminal, interpret them as numbers, add them together and print out the result: IO Int
  • A first-person shooter game that uses mouse movements as input and renders graphics to the screen: IO ()

While actions can result in values that are used by the program, they do not take any arguments. Consider putStrLn. It has the following type:

putStrLn :: String -> IO ()

PutStrLn takes an argument, but it is not an action. It is a function that takes one argument (a string) and returns an action of type IO (). So putStrLn is not an action, but putStrLn "hello" is. The distiction is subtle but important. All IO actions are of type IO a for some type a. They will never require additional arguments, although a function which makes the action (such as putStrLn) may.


Actions are like directions. They specify something that can be done. They are not active in and of themselves. They need to be "run" to make something happen. Simply having an action lying around doesnt make anything happen. For example, putStrLn "hello" is an action in haskell that prints the line "hello". It has type IO (). We can write a Haskell program that contains the definition

x = putStrLn "hello"

but that doesn't cause the haskell program to print out "hello"! Haskell only runs one IO action in a program, the action called main. This action should have the type IO (). The following haskell program will print out "hello":

module Main where

main :: IO ()
main = putStrLn "hello"

Do Notation

You may be wondering how any Haskell program can do anything useful if it can only run a single IO action. As we saw earlier, IO actions can be very complex. We can combine many simple actions together to form more complicated actions. To combine actions together we use a do-block.

A do-block combines together two or more actions into a single action. When two IO actions are combined the result is an IO action that, when invoked, performs the first action and then performs the second action. Here's a simple example.

main :: IO ()
main = do
    putStrLn "hello"
    putStrLn "world"

Main is an action that prints a line "hello" and then prints a line "world". If the first action had any side effects, those effects are visible to the second action when it is performed. For example, if a file is written in the first action and read in the second action, the change to the file will be visible to the read. Remember that IO actions can return results to the program. The result of a do-block is the result of the last action in the do block. In our example above, the last action (putStrLn "hello") doesn't provide a useful result and so the type of the entire do-block is IO ().

Do-blocks can also make use of the result of one action when constructing another action. For example:

main:: IO ()
main = do
    line <- getLine
    putStrLn ("you said: " ++ line)

This example uses the action getLine (getLine :: IO String) which reads a line of input from the console. The do-block makes an action that, when invoked, invokes the getLine, takes its result and invokes the action putStrLn ("you said: " ++ line) with the previous result bound to line.

Notice that an arrow (<-) is used in the binding and not an equal sign (as is done when binding with let or where). The arrow indicates that the result of an action is being bound. The type of getLine is IO String, and the arrow binds the result of the action to line which will be of type String.

We've used do-blocks to combine two actions together. This provides enough power to combine more actions together:

main :: IO ()
main = do
    putStrLn "Enter two lines"
    do
        line1 <- getLine
        do
            line2 <- getLine
            putStrLn ("you said: " ++ line1 ++ " and " ++ line2)

Since the innermost do-block is an action, it can be combined with getLine to make another action, which can be combined with putStrLn "Enter two lines" to make another more complicated action. Luckily we don't have to go through all this trouble. Do-blocks allow multiple actions to be specified in a single block. The meaning of these multi-action blocks is identical to the nested example above: the bindings are made visible to all successive actions. The previous example can be rewritten more compactly as

main :: IO ()
main = do
    putStrLn "Enter two lines"
    line1 <- getLine
    line2 <- getLine
    putStrLn ("you said: " ++ line1 ++ " and " ++ line2)

Of course we are free to use other Haskell language features when writing our program. Instead of putting all of our actions in main we may want to factor some common operations out as separate actions or functions that build actions. For example, we may want to combine prompting and user input:

promptLine :: String -> IO String
pormptLine prompt = do
    putStr prompt
    getLine

main :: IO ()
main = do
    line1 <- promptLine "Enter a line: "
    line2 <- promptLine "And another: "
    putStrLn ("you said: " ++ line1 ++ " and " ++ line2)

Here we made a function promptLine which returns an action. The action prints a prompt (using putStr :: IO (), which prints a string without a newline character) and reads a line from the console. The result of the action is the result of the last action, getLine.

Let's try to write a slightly more helper function that reads two lines and returns both of them concatenated together:

promptTwoLines :: String -> String -> IO String
promptTwoLines prompt1 prompt2 = do
    line1 <- promptLine prompt1
    line2 <- promptLine prompt2
    line1 ++ " and " ++ line2    -- ??

There's a problem here. We know how to prompt for and read in both lines of input, and we know how to combine those lines of input, but we don't have an action that results in the combined string. Remember, do-blocks combine together actions and the result of the do-block is the result of the last action. line1 ++ " and " ++ line2 is a string, not an action resulting in a string and so cannot be used as the last line of the do-block. What we need is a way to make an action that results in a particular value. This is exactly what the return function does. Return is a function that takes any type of value and makes an action that results in that value. We can now complete our helper:

promptTwoLines :: String -> String -> IO String
promptTwoLines prompt1 prompt2 = do
    line1 <- promptLine prompt1
    line2 <- promptLine prompt2
    return (line1 ++ " and " ++ line2)

main :: IO ()
main = do
    both <- promptTwoLines "First line: " "Second line: "
    putStrLn ("you said " ++ both)

In this example return (line1 ++ " and " ++ line2) is an action of type IO String that doesn't affect the outside world in any way, but results in a string that combines line1 and line2.

Here's a very important point that many beginners get confused about: "return" does not affect the control flow of the program! Return does not break the execution of the do-block. Return may occasionally be used in the middle of a do-block where it doesn't directly contribute to the result of the do-block. Return is simply a function that makes an action whose result is a particular value. In a sense it wraps up a value into an action.

XXX - let's in do blocks

XXX - using if/case/etc in do-blocks, nested do-blocks

XXX - there's no escape

Summary

  • IO actions are used to affect the world outside of the program.
  • Actions take no arguments but have a result value.
  • Actions are inert until run. Only one IO action in a Haskell program is run (main).
  • Do-blocks combine multiple actions together into a single action.
  • Combined IO actions are executed sequentially with observable side-effects.
  • Arrows are used to bind action results in a do-block.
  • Return is a function that builds actions. It is not a form of control flow!