Programming Languages (CSC-302 98S)
Outline of Class 31: Functionally Pure Input and Output
Held: Friday, April 17, 1998
- Don't forget that
assignment five is
due today. I'll do my best to get it back to you by Monday, but
given that most of you asked for extensions until today, I won't
- We won't have another assignment this week since we have an exam
scheduled for next Friday. I'll try to have a review sheet ready
on Monday. The exam will mostly cover functional programming and
I may invent a functional programming language for the exam.
- On Tuesday, April 21 at 11 a.m. in Science 1023, Robert Cadmus,
professor of Physics, will present a lecture about electronic imaging
- If you haven't done so already, please start to read chapter 11 of
- And our question for today: "Prove that there are infinitely many
- Many of you have been wondering "if Haskell is pure, how do we do I/O"?
- At first glance, I/O functions seem to be "nonfunctional", in that
they either return different values each time, or "do more than" compute
- However, there are a number of strategies to provide functionally pure
I/O (depending on your notion of purity).
- Most such strategies assume that it's okay to have functions behave
differently in different invocations of a program, as long as they behave
the same way within a single invocation.
- A simple model of I/O (not used in Haskell): read() returns a lazy list
of the input.
- A related model: read takes one parameter, representing the position of
the character to be read.
- How do we handle output? We model every program as a map from
sequences of characters to sequences of characters (which is appropriate
for the Unix system :-).
- Are there problems? Certainly.
- It can be hard to correctly sequence input and output.
- Functions must pass around the "remainder of the input stream"
- Other models of I/O also handle I/O by passing around some sort of
environmental variable, which they also return.
- This is a generalization of the "related model" above.
- In this model, an input function returns a pair consisting of the input
read and the new "state" of the system.
- If you make the state of the program explicit, you no longer need to
worry about functions implicitly modifying it.
- What does Haskell do? Haskell has at least two views of I/O.
Why? Because there were at least two views at the time, and
proponents of both sides contributed to the design of Haskell.
- One model of I/O is based on the notion of streams (lazy lists).
- The other model is based on continuations.
- Haskell's message stream model is, in effect, similar to the earlier
"character stream" model.
- If you step back and consider the relationship between program and
operating system, we can think of the program as generating a sequence
of I/O requests for the operating system, and the operating system
returning a sequence of responses.
+--->| Program |>---+
| +------------+ |
| 1. Ok | 3. What is my user's home directory?
| 2. Read "hello" | 2. Read the first line of "fred"
| 3. /home/rebelsky | 1. Open file "fred"
| +------------+ |
+---<| OS |<---+
- Using this model, we can represent both program on operating system as
- An operating system is a function from lists of I/O requests to
lists of I/O responses.
- A program is a function from lists of I/O responses to lists of
- This means the type of a program is
[Response] -> [Request].
- Yes, this is quite odd (or is odd at first glance).
- We need both lists to be lazy.
- Presumably, the first element of the request list is generated before
the first element of the response list is read.
- What are the legal requests? They include
data Request = ReadFile String
| WriteFIle String String
| AppendFile String String
| ReadBinFile String
| DeleteFile String
| StatusFile String
| ReadChan String
| AppendChan String String
| ReadBinChan String
| AppendBinChan String Bin
| StatusChan String
| Echo Bool
| GetEnv String
| SetEnv String String
- What are the valid responses? They are defined as
data Response = Success
| Str String
| StrList [String]
| Bn Bin
| Failure IOError
- You might want to think about why there are so many.
- So, how do we use all of this?
- We write a
main function of type
[Request] -> [Response], have it generate one or more
requests, and use the responses to those requests to get/produce more
- For example, to print out the environmental variable "Home", you might write:
main resps = [ GetEnv "Home", printhome (head resps) ]
printhome (Str home) = AppendChan stdout ("My home is" ++ home)
printhome (Failure x) = AppendChan stdout "Couldn't find the home"
- Unfortunately, it is easy to make minor mistakes in sequencing.
Consider the following program, which tries to greet the user:
main resps =
AppendChan stdout "Hi there, what is your name?",
greet (head (tail resps))
greet (Str name) =
AppendChan stdout ("Hi there " ++ name)
greet (Failure x) =
AppendChan stdout "Hmmm ... something is very wrong"
- The subtle error is that this prints "Hi there" before the user
enters anything. Can you guess why?
- It's also important to be careful not to look at the structure of the
responses list until you've given it some indication what the structure
should be. Consider
main (resp : resps) = [ ... ]
- Continuations provide an elegant mechanism for considering pure I/O.
- How? By writing I/O functions that take continuations as parameters.
Clearly, at different points in the program, "what is left to do" will
vary, and we can therefore maintain the rule that "when applied to
the same arguments, the same value is returned".
- Basically, you provide each I/O operator with (at least) two continuations
which describe what to do with the result of the I/O.
- One continuation is used for success
- The other is used for failure.
- Some basic definitions
writeFile :: Name -> String -> FailCont -> SuccCont -> Dialogue
getEnv :: Name -> FailCont -> StrCont -> Dialogue
appendChan :: Name -> FailCont -> SuccCont -> Dialogue
done :: Dialogue
type Dialogue = [Response] -> [Request]
type FailCont = IOError -> Dialogue
type SuccCont = Dialogue
type StrCont = String -> Dialogue
- Programs written with this strategy can start to resemble traditional
imperative programs, particularly if you're clever with layout. E.g.,
getEnv "Home" exit (\home ->
appendChan "stdout" ("my home is " ++ home) exit
- Observe that we can really define all these transactions in terms of the
request/response I/O (and vice versa).
- For example, appendChan might be defined as
appendChan chan str fail succ resps =
(AppendChan chan str) :
(checkandcontinute fail succ (head resps) (tail resps)
checkandcontinue :: FailCont -> SuccCont -> Response -> Dialogue
checkandcontinue fail succ Success resps = succ resps
checkandcontinue fail succ (Failure msg) = fail msg resps