CS 331 Spring 2013 > Lecture Notes for Wednesday, February 27, 2013 |
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++:
[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
show
Function show
converts pretty much anything to
a String
.
This function is overloaded for any type in class Show
.
[Interactive Haskell]
> show 345 "345" > length (show 345) -- length of String 3 > length 345 -- 345 is not a String ERROR ...
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" ERROR ...
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 347 > 2.0 + read "345" -- Double context 347.0
An interesting way to specify a type requirement is to use
function asTypeOf
.
This is defined as follows.
[Haskell]
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 345 > (read "345") `asTypeOf` 12.0 -- Convert to Double 345.0
An I/O action is a type of value. We do I/O by returning an I/O action to the outside world.
[Haskell]
sayHowdyNewLine = putStrLn "Howdy!"
[Interactive Haskell]
> :t sayHowdyNewLine sayHowdyNewLine :: IO () > sayHowdyNewLine Howdy!
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.
[Haskell]
printMessages = do putStrLn "Hello" putStr "Here is a number: " print (7*8 + 15) putStrLn "Bye!"
[Interactive Haskell]
> :t printMessages printMessages :: IO () > printMessages Hello Here is a number: 71 Bye!
We can nest do
blocks.
[Haskell]
printMessages2 = do putStrLn "My" do putStrLn "dog" putStrLn "has" putStrLn "fleas."
[Interactive Haskell]
> :t printMessages2 printMessages2 :: IO () > printMessages2 My dog has fleas.
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 “<-
”.
[Haskell]
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:
do
block.reverse
).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.
[Haskell]
-- 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 “return
”
does 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.
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.
[Haskell]
-- 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.
do
block combines multiple I/O actions into
a single I/O action.<-
” to bind a variable name
to a wrapped value,
in the context of a do
block.return
to create a do-nothing I/O action
wrapping a specified value.let
inside a do
block
to bind a variable name to a non-I/O value.
See
haskell_io.hs
for Haskell source code related to today’s lecture.
ggchappell@alaska.edu