CS 331 Spring 2013  >  Lecture Notes for Wednesday, February 27, 2013

CS 331 Spring 2013
Lecture Notes for Wednesday, February 27, 2013

Haskell: I/O

String Conversion

Strings & I/O

In many languages, conversion to & from a string is mixed up together with I/O. This makes sense, because when we do text I/O, the values we send and receive must, in the end, be composed of characters. For example, in C++:


int x;
cout << x;

The second line above converts the value of x to a string, and then sends it to cout.

In Haskell, string conversion and I/O are kept a bit more separate. We look first at string conversion

Function show

Function show converts pretty much anything to a String. This function is overloaded for any type in class Show.

[Interactive Haskell]

> show 345
> length (show 345)  -- length of String
> length 345         -- 345 is not a String

Function read

Function read can convert a String to pretty much anything. This function is overloaded for any type in class Read.

[Interactive Haskell]

> read "345"

We may need to indicate the type to convert to. Often the compiler can determine this by looking at what is done with the return value of read.

[Interactive Haskell]

> 2 + read "345"    -- Integer context
> 2.0 + read "345"  -- Double context

An interesting way to specify a type requirement is to use function asTypeOf. This is defined as follows.


asTypeOf :: a -> a -> a
asTypeOf x _ = x

So asTypeOf returns its first argument, but requires this to be the same type as its second argument.

[Interactive Haskell]

> (read "345") `asTypeOf` 12    -- Convert to Integer
> (read "345") `asTypeOf` 12.0  -- Convert to Double

Simple Output

I/O Actions

An I/O action is a type of value. We do I/O by returning an I/O action to the outside world.


sayHowdyNewLine = putStrLn "Howdy!"

[Interactive Haskell]

> :t sayHowdyNewLine
sayHowdyNewLine :: IO ()
> sayHowdyNewLine

Function putStr takes a String parameter and putStr returns an I/O action. When returned to the outside world, this I/O action causes the string to be printed. Function putStrLn is the same, except that it adds a newline on the end.

[Interactive Haskell]

> :t putStr
putStr :: String -> IO ()

Saying print x is the same as putStrLn (show x).

do Blocks

A do block combines multiple I/O actions into a single I/O action.


printMessages = do
    putStrLn "Hello"
    putStr "Here is a number: "
    print (7*8 + 15)
    putStrLn "Bye!"

[Interactive Haskell]

> :t printMessages
printMessages :: IO ()
> printMessages
Here is a number: 71

We can nest do blocks.


printMessages2 = do
    putStrLn "My"
        putStrLn "dog"
        putStrLn "has"
    putStrLn "fleas."

[Interactive Haskell]

> :t printMessages2
printMessages2 :: IO ()
> printMessages2

Simple Input

An I/O action is essentially a side effect wrapping a value. The above I/O actions all wrap “nothing” values. We do input using an I/O action that wraps a non-trivial value.

[Interactive Haskell]

> :t getChar
getChar :: IO Char
> :t getLine
getLine :: IO String

We can bind a variable name to the wrapped value by using “<-”.


reverseIt = do
    putStr "Type something: "
    line <- getLine
    putStr "You typed (backwards): "
    putStrLn (reverse line)

Above, the name line is bound to the String wrapped by the I/O action that getLine returns. This String is read from the standard input.

Two important points:

Using return in a do Block

All of the above I/O actions involved a non-trivial side effect. We can wrap a value in a do-nothing side effect using return. For example, “return x” produces a do-nothing I/O action that wraps the value x. And “return ()” produces a do-nothing I/O action that wraps a “nothing” value—essentially a null I/O action.


-- myGetLine
-- Same as getLine, but showing how to write it using getChar.
myGetLine = do
    c <- getChar
    if c == '\n'
        then return ""
        else do
            rest <- myGetLine
            return (c:rest)

Note that each expression in a do block needs to return an I/O action. However, these expressions can be complicated. For example, from the above if to the end of the do block is a single expression.

Also note that Haskell’s “returndoes not return. It simply creates a do-nothing I/O action. There is nothing to prevent us from doing several return expressions in a row, following by—say—a putStrLn. (Try it!) However, in practice a return is almost always the last I/O action performed. Thus the name “return” makes sense.

Using let in a do Block

One last bit of syntax remains. We can use “let ... = ...” in a do block to bind a name to a non-I/O value, for the remainder of the do block.


-- squareEm
-- Repeatedly input a number from the user. If 0, then quit; otherwise
--  print its square, and repeat.
squareEm = do
    putStr "Type a number (0 to quit): "
    line <- getLine    -- Bind name to I/O-wrapped value
    let n = read line  -- Bind name to non-I/O value
                       -- Compiler knows n is a number by how it is used
    if n == 0
        then return () -- Must have I/O action here, so make it null
        else do
            putStr "Squaring, we get: "
            print (n*n)
            squareEm   -- repeat

Above, the “then” portion of the expression must return an I/O action; otherwise we have a type error. However, at this point we simply want to leave, so there is nothing for this I/O action to do. Thus we use return () to create a null I/O action.

Quick Summary of Haskell I/O

See haskell_io.hs for Haskell source code related to today’s lecture.

CS 331 Spring 2013: Lecture Notes for Wednesday, February 27, 2013 / Updated: 27 Feb 2013 / Glenn G. Chappell / ggchappell@alaska.edu