Programming Languages (CSC-302 98S)

[Instructions] [Search] [Current] [Changes] [Syllabus] [Handouts] [Outlines] [Assignments]

# 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 guarantee it.
• 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 technology.
• If you haven't done so already, please start to read chapter 11 of Louden.
• And our question for today: "Prove that there are infinitely many primes."

## Lazy Functional I/O

• 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 values.
• 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.

### Message Streams

• 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 functions.
• 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 I/O requests.
• 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
| DeleteFile String
| StatusFile String
| AppendChan String String
| AppendBinChan String Bin
| StatusChan String
| Echo Bool
| GetArgs
| GetProgName
| 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 information.
• 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) = [ ... ]
```

### Continuation-based I/O

• 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
done
```

• 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
```

On to Introduction to Logic Programming and Prolog
Back to Lazy Evaluation, Revisited
Outlines: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
Current position in syllabus

[Instructions] [Search] [Current] [Changes] [Syllabus] [Handouts] [Outlines] [Assignments]

Disclaimer Often, these pages were created "on the fly" with little, if any, proofreading. Any or all of the information on the pages may be incorrect. Please contact me if you notice errors.

Source text last modified Thu May 7 20:29:43 1998.

This page generated on Thu May 7 20:34:47 1998 by SiteWeaver.

Contact our webmaster at rebelsky@math.grin.edu