Module 12 Optional Lab
-
We have seen how to do information hiding with local entities using let and where. We can also control which entities defined in a file can be exported to other files. We can make the code in a file into a nameable unit by making it into a module. Make a file called
MyModule.hs
. Open the file and make the first non-comment, non-blank linemodule MyModule where
. Below this in the file writemyLength :: [a] -> Int
andmyHead :: Ord a => [a] -> a
functions. -
Make a
IOLab.hs
file in the same directory asMyModule.hs
and at the top writeimport MyModule
. Write some code that usesmyLength
andmyHead
fromMyModule
. Inghci
, loadIOLab.hs
. You should see a message that bothMyModule.hs
andIOLab.hs
are being compiled, and you should be able to run your code fromIOLab.hs
. Note that the module name must match the file name. -
To control what gets exported from a module, we can make a list of exported names. In
MyModule.hs
, change the module declaration to the following.module MyModule (myLength) where
The names between the parentheses are exported (they are separated by commas if there are more than one of them). If you save this change and then try to reload
IOLab.hs
inghci
, you will get a message thatmyHead
is not in scope (because it was not included in the list of exported names). -
You can also control which names exported by a module are imported by listing the desired names in parentheses after the module name in the import statement. And there is a lot more control available too: you can specify that imported names can only be referenced as qualified names (such as
MyModule.myLength
), that the qualifier be renamed, and that some imported names be hidden. Also, modules can be put into directories that can be part of the qualified name (as in Java). But we won’t need all this fancy stuff. -
Haskell has built-in functions for converting from string to other values and back, called
read
andshow
. In ghci, typeread "8" + 5
. Now tryread "[1,2,3]" :: [Int]
. But tryread "8"
by itself. This does not work because read cannot tell the type of thing you are trying to read. If you put a string representation of a Haskell value between double quotes and the context disambiguates the type, then read will convert it to a value. -
show
does the opposite ofread
. Typingshow 8
inghci
does not tell us much because just typing8
inghci
also prints the string version of8
. This is because behind the scenesghci
is usingshow
to convert values to strings for us. But try this inghci
:"Pair " ++ show (9,2)
. This would not work ifshow
was not converting the tuple to a string that could be concatenated toPair
. -
The main topic we want to address in this lab is input and output. So far we have been doing everything in the
ghci
, and our programs have not read any data from anywhere. The interpreter has handled whatever IO was necessary. But clearly Haskell programs need to be able to do more than this. IO is a special sort of thing in Haskell because it cannot be referentially transparent. An expression is referentially transparent if we can execute it anywhere in a program and get the same results; or, in other words, if you give an expression the same arguments, you will always get the same results. This has been true of all the Haskell we have done so far. And this has worked because, at bottom, none of the code we have used so far has had side-effects, that is, no non-local resource have been changed. But to do IO we must give up referential transparency and accept side-effects: if we write some output, then we have changed the world outside our program (a side-effect), and if we read some input, then we won’t get the same value every time (so our expressions won’t always have the same values). In Haskell, referentially transparent parts of programs are called pure while non-referentially transparent parts are impure. Haskell programs segregate their pure and impure parts, with the goal being to make the impure parts as small as possible so that most of the program is pure and so can be reasoned about more thoroughly. -
The type of impure parts of programs is IO a where a is some type. A sub-program whose result type is
IO a
for somea
is by definition impure. Sub-programs with these types are called actions rather than functions. Every program that does IO must have a special action calledmain :: IO ()
that will be executed when the program is run. Although arbitrary actions can be defined in a Haskell program, none will actually be executed unless they are called (directly or indirectly) frommain
. -
To get started with actions, add the following line to
IOLab.hs
:main = putStrLn "Hello"
At the command line, typerunhaskell IOLab.hs
. Therunhaskell
command compiles, links, and then executes a program. TheputStrLn
action has typeString -> IO ()
; in other words, it is an IO action. It causes itsString
argument to be written to the terminal. -
We can read from the terminal too. The
getLine :: IO String
action reads a single line of text from the terminal. But it returns it inside an IO context, which means it is not immediately accessible. However, we can extract values from IO contexts inside a construct called ado
block using a special<-
operator. Modify yourmain
action to look like the following:main = do putStrLn "Your name?" name <- getLine putStrLn ("Hello " ++ name)
A
do
block allows us to stitch together a sequence of IO actions. (Note that the very idea of sequencing is new because order does not matter when you have referential transparency.) The linename <- getLine
removes theString
returned bygetLine
from its IO context, so the type of name isString
. Hence we can use++
on it to form theString
output in the last line of thedo
block. -
Although the code above makes the
<-
look sort of like an assignment statement, the<-
operator is really doing a special job (removing a value from an IO context). Try the following and see what happens.main = do putStrLn "Your name?" name <- getLine response <- "Hello" ++ name putStrLn response
The problem is that
<-
expects something of typeIO a
on its right, but"Hello" ++ name
has typeString
. However, we can make regular assignments as in pure code using alet
, with the added advantage that we don’t need thein
keyword. We can just putlet
and then follow it with one or more assignments; these will be in effect until the end of thedo
block. Fix the code above so it works by usinglet
to assign a value to response. -
We can make IO actions as separate pieces of code and then use them in
main
. We can also use recursion and other Haskell constructs in actions as in pure Haskell functions. For example, Haskell has a built-in actiongetContents :: IO String
that reads everything in the standard input stream (stdin
) until the end of the stream. We can build an approximation of this action usinggetLine
. -
First lets see what
getContents
does. Replace themain
action inIOLab.hs
with the following:main = do input <- getContents putStr input
You can probably guess that this program just echoes everything in
stdin
. The program stops at the end of the input. To test it, make a file calledIOLab.txt
with a few lines in it, and runrunhaskell IOLab.hs <IOLab.txt
at the command line. -
Now lets write
getTilBlank :: IO String
that readsstdin
until it encounters a blank line. BecausegetTilBlank
is an action, its body must be ado
block. It can read a line fromstdin
usinggetLine
and then check to see whether the result is the empty string""
. If so, it can simply return the empty string in an IO context. How do we do that? There is a built-in action calledreturn
that does this (this is NOT like a return in Java or Ruby because it does not affect control flow–it just takes a value and puts it in an IO context). If the input line is not the empty string, then, in ado
block, we can apply return to the line concatenated to a newline concatenated to the result of getting the rest of the input. How do we get the rest of the input? CallgetTilBlank
again, of course (that is, use recursion). So we can do things in actions anddo
blocks that we do in regular functions. -
Finish writing
getTilBlank
, modifymain
to use it, modifyIOLab.txt
to have a blank line somewhere in the middle, and test your code. -
IO contexts in Haskell are one case of the monad functional design pattern. The monad pattern enables a wide variety of traditional procedural programming concepts (e.g., iteration, input/output, global state, and error handling) in a functional paradigm, and does so in a way that is often far safer than in traditional procedural languages. If you are interested in learning more about monads and how “real” programs are written using Haskell, here are some good resources:
- Learn You a Haskell (specifically chapters 11, 12, and 13).
- “Functors, Applicatives, and Monads in Pictures” (adit.io blog post)
- “Railway Oriented Programming” (F# for fun and profit blog post)
This lab was originally written by Dr. Chris Fox and expanded by Dr. Mike Lam.