views:

120

answers:

4

I'm not sure exactly how much this falls under 'programming' opposed to 'program language design'. But the issue is this:

Say, for sake of simplicity we have two 'special' lists/arrays/vectors/whatever we just call 'ports' for simplicity, one called stdIn and another stdOut. These conceptually represent respectively

  • All the user input given to the program in the duration of the program
  • All the output written to the terminal during the duration of the program

In Haskell-inspired pseudocode, it should then be possible to create this wholly declarative program:

 let stdOut =   ["please input a number", 
                "and please input another number", 
                "The product of both numbers is: " ++ stdIn[0] * stdIn[1]]

Which would do the expected, ask for two numbers, and print their product. The trick being that stdOut represents the list of strings written to the terminal at the completion of the program, and stdIn the list of input strings. Type errors and the fact that there needs to be some safeguard to only print the next line after a new line has been entered left aside here for simplicity's sake, it's probably easy enough to solve that.

So, before I go of to implement this idea, are there any pitfalls to it that I overlooked? I'm not aware of a similar construct already existing so it'd be naïve to not take into account that there is an obvious pitfall to it I overlooked.

Otherwise, I know that of course:

 let stdOut =   [stdIn[50],"Hello, World!"]

Would be an error if these results need to be interwoven in a similar fashion as above.

+5  A: 

A similar approach was used in early versions of Haskell, except that the elements of the stdin and stdout channels were not strings but generic IO 'actions'--in fact, input and output were generalized to 'response' and 'request'. As long as both channels are lazy (i.e. they are actually 'iterators' or 'enumerators'), the runtime can simply walk the request channel, act on each request and tack appropriate responses onto the response channel. Unfortunately, the system was very hard to use, so it was scrapped in favor of monadic IO. See these papers:

  • Hudak, P., and Sundaresh, R. On the expressiveness of purely-functional I/O systems. Tech. Rep. YALEU/DCS/RR-665, Department of Computer Science, Yale University, Mar. 1989.
  • Peyton Jones, S. Tackling the Awkward Squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell. In Engineering theories of software construction, 2002, pp. 47--96.
mathy
Hmm, what I understand from reading your top paper now, my approach is in effect the streams used in Miranda, am I understanding this correctly?Also, difficult as in 'to implement' or 'for programmers to get used to'
Lajla
Apparently (I wasn't there), the model was a pain to use for everyone and was henceforth abandoned. Also, the "Tackling the Awkward Squad" paper can be found at http://research.microsoft.com/en-us/um/people/simonpj/papers/marktoberdorf/mark.pdf .
Antal S-Z
+4  A: 

The approach you're describing sounds like "Dialogs." In their award-winning 1993 paper Imperative Functional Programming, Phil Wadler and Simon Peyton Jones give some examples where dialogs really don't work very well, and they explain why monadic I/O is better.

Norman Ramsey
+1  A: 

I don't see how you will weave them considering this example compared to your own:

let stdOut =   ["Welcome to the program which multiplies.",
                "please input a number", 
                "and please input another number", 
                "The product of both numbers is: " ++ stdIn[0] * stdIn[1]]

Should the program prompt for the number represented by stdIn[0] after outputting one line (as in your example) or two lines? If the index 0 represents the 0th input from stdin, then it seems something similar to:

let stdOut =   ["Welcome to the program which multiplies.",
                "please input a number",
                some_annotation(stdIn[0]),
                "and please input another number", 
                some_annotation(stdIn[1]),
                "The product of both numbers is: " ++ stdIn[0] * stdIn[1]]

will be required in order to coordinate the timing of output and input.

I like your idea. Replace some_annotation with your preference, perhaps something akin "synchronize?" I couldn't come up with the incisive word for it.

Heath Hunnicutt
I was thinking of weaving them with a 'need' function, for instance, let's say that `need stdIn[0]` evaluates to an empty string, no matter what stdIn[0] is, but still 'needs' stdIn conceptually to compute it. `"please input another number" ++ need stdIn[0]` will then ask for the first input before printing a new number. But there are countless ways to delay the output messages until a desired time.
Lajla
I like the use of the word "need".
Heath Hunnicutt
Well, my draft nowadays calls it 'needing', it's basically a lazy lisp-like language which no one will really like because it gives them headaches, but:`(extend-port standard-out-port "Give me your input" ("I got your input" (needing (standard-in-port 0)))` basically waits until it prints the second line. Strings applied to nil yield themselves.
Lajla
A: 

This approach seems to be the "most obvious" way to add I/O to a pure λ-calculus, and other people have mentioned that something along those lines has been tried in Haskell and Miranda.

However, I am aware of a language, not based on a λ-calculus, that still uses a very similar system:

How to handle input and output in a language without side effects? In a certain sense, input and output aren't side effects; they are, so to speak, front- and back-effects. (...) [A program is] a function from the space of possible inputs to the space of possible outputs.

Input and output streams are represented as lists of natural numbers from 0 to 255, each corresponding to one byte. End-of-file is represented by the value 256, not by end of list. (This is because it is often easier to deal with EOF as a character than as a special case. Nevertheless, I wonder if it wouldn't be better to use end-of-list.)

(...)

It's not difficult to write interactive programs (...) [but] doing so is, technically speaking, a sin. (...) In a referentially transparent language, anything not explicitly synchronized is fair game for evaluation in any order whatsoever, at the run-time system's discretion.

(...) The most obvious way of writing this particular program is to cons together the "Hello, [name]!" string in an expression which is conditioned on receipt of a newline. If you do this you are safe, because there's no way for any evaluator to prove in advance that the user will ever type a newline.

(...)

So there's no practical problem with interactive software. Nevertheless, there's something unpleasant about the way the second case is prevented. A referentially transparent program should not have to rely on lazy evaluation in order to work properly.

How to escape this moral dilemma? The hard way is to switch to a more sophisticated I/O system, perhaps based on Haskell's, in which input and output are explicitly synchronized. I'm rather disinclined to do this, as I much prefer the simplicity of the current system. The easy way out is to write batch programs which happen to work well interactively. This is mainly just a matter of not prompting the user.

Perhaps you would enjoying doing some programming in Lazy K?

camccann
Ahaha, it's an interesting language to be sure, but not exactly my approach here though, or at least, one of many I've chosen to work with this streams-as-I/O approach. Mine in the end has become some kind of hybrid between monads and streams.
Lajla