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] -> IntandmyHead :: Ord a => [a] -> afunctions. -
Make a
IOLab.hsfile in the same directory asMyModule.hsand at the top writeimport MyModule. Write some code that usesmyLengthandmyHeadfromMyModule. Inghci, loadIOLab.hs. You should see a message that bothMyModule.hsandIOLab.hsare 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) whereThe 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.hsinghci, you will get a message thatmyHeadis 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
readandshow. 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. -
showdoes the opposite ofread. Typingshow 8inghcidoes not tell us much because just typing8inghcialso prints the string version of8. This is because behind the scenesghciis usingshowto convert values to strings for us. But try this inghci:"Pair " ++ show (9,2). This would not work ifshowwas 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 afor someais 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. Therunhaskellcommand compiles, links, and then executes a program. TheputStrLnaction has typeString -> IO (); in other words, it is an IO action. It causes itsStringargument to be written to the terminal. -
We can read from the terminal too. The
getLine :: IO Stringaction 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 adoblock using a special<-operator. Modify yourmainaction to look like the following:main = do putStrLn "Your name?" name <- getLine putStrLn ("Hello " ++ name)A
doblock 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 <- getLineremoves theStringreturned bygetLinefrom its IO context, so the type of name isString. Hence we can use++on it to form theStringoutput in the last line of thedoblock. -
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 responseThe problem is that
<-expects something of typeIO aon its right, but"Hello" ++ namehas typeString. However, we can make regular assignments as in pure code using alet, with the added advantage that we don’t need theinkeyword. We can just putletand then follow it with one or more assignments; these will be in effect until the end of thedoblock. Fix the code above so it works by usingletto 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 Stringthat 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
getContentsdoes. Replace themainaction inIOLab.hswith the following:main = do input <- getContents putStr inputYou 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.txtwith a few lines in it, and runrunhaskell IOLab.hs <IOLab.txtat the command line. -
Now lets write
getTilBlank :: IO Stringthat readsstdinuntil it encounters a blank line. BecausegetTilBlankis an action, its body must be adoblock. It can read a line fromstdinusinggetLineand 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 calledreturnthat 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 adoblock, 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? CallgetTilBlankagain, of course (that is, use recursion). So we can do things in actions anddoblocks that we do in regular functions. -
Finish writing
getTilBlank, modifymainto use it, modifyIOLab.txtto 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.