t3x.org / sketchy / vol1 / sl16.html

Sketchy LISP

  Copyright (C) 2006,2007 Nils M Holm
Buy a copy at Lulu.com

An Introduction to Functional Programming in Scheme

2.8 Input, Output, and Side Effects

All Scheme procedures have an effect which is to map a number of arguments to a value. This is even true with most pseudo functions. For example cond maps a number of clauses to a value. Given the same arguments, a procedure always returns the same value.

There are constructs, though, which have an effect that cannot be explained by mapping arguments to values. In section 1.7, one of them was introduced in order to explain letrec: set! changes the value of a variable, thereby mutating the state of the current environment. This can easily be observed by checking the value of the variable before and after the application of set!:

(define x 'foo)
x => foo
(set! x 'bar)
x => bar

Because this effect of set! on x cannot be explained by mapping arguments to values, it is called a side effect. There are two common kinds of side effects in Scheme. One is the mutation of objects, and the other is caused by input and output operations.

Scheme, being a multi-paradigm language, provides several constructs for mutating state. The names of these constructs end with an exclamation mark (like set!) to indicate that they have side effects on objects.

In this book a purely applicative subset of Scheme is discussed, so there are only few procedures with side effects. The only constructs discussed here that alter values are set! and letrec. Letrec does this behind the scenes, which is why its name has no trailing exclamation mark.

An applicative language is a language in which programs are formed by combining the applicatitions of procedures and pseudo functions. Strictly speaking, it is purely applicative, if its procedures do not have any side effects at all. So, strictly speaking again, the language discussed here is not purely applicative, because some of its constructs do have side effects.

In fact, terms like purely applicative or purely functional are a bit blurry. There are a few real-world languages that claim to be pure in this regard, but a real-world language without I/O would not make much sense, so I/O seems to be an exception to the rule.

2.8.1 Input and Output

To cause side effects is in the nature of input and output procedures. In fact many I/O procedures do not even have any observable effect other than returning an unspecific value. The side effects of I/O procedures are often more important than their effects.

The most abstract I/O procedures of Scheme are called read and write. These procedures translate between internal and external representation. Read is used by Scheme environments to parse programs, and write is used to write the external representations of normal forms to the user's terminal.

Most output that is written by write can be read back using read. There are some objects that do not have any external representation, though, like procedures. Such objects are represented by some informative text enclosed by #< and >:

(write cons)              writes  #<primitive cons>
(write (lambda () 'foo))  writes  #<procedure ()>

Any attempt to read such a form results in bottom:

(read) #<primitive cons> => bottom

In case you wonder what applications of write reduce to: it is something called an unspecific value, frequently represented by #<unspecific> or #<void>. Unspecific values are used to indicate that the value returned by a procedure or pseudo function is not interesting. You cannot do anything useful with such a value, and they cause a type error when passed to most procedures. Output procedures typically reduce to an unspecific value, because their effect is not important.

Write writes its output to the same output stream as the Scheme system itself, so the output of write and the system itself are mixed:

(write "Hello, World!")
"Hello, World!"=> #<unspecific>

You already know read, too, because all programs you have typed in so far were processed by this procedure. In interactive environments, read reads the same input stream as the Scheme prompt, so you can place your input right after an application of read

(read) (this is a list with members)
=> (this is a list with members)

Two interesting things happen above. Read is a zero-argument procedure. It evaluates to the object read by it. Because it can evaluate to different values given the same argument (none), it is obvious that it has a side effect. The other interesting part is that the list read by read is not quoted. This is not necessary, because the list is never evaluated. Quotation is only needed to tell Scheme that a list is data and not a program. Because something that is read in obviously is data, there is no need to quote it. In fact, quoting it would cause read to return an application of quote instead the quoted object:

(read) foo  => foo
(read) 'foo => 'foo

Because read and write translate objects from and to an unambiguous external form, they are useful for storing and retrieving information.

If you want to write some output to a terminal while your program runs, however, the display procedure is probably what you want. Display pretty-prints objects. It does not print quotation marks around strings and emits special characters and white space without escaping them. For instance, display can be used to begin a new line by emitting a #\newline character:

(display #\newline)

=> #<unspecific>

Write would emit the external representation #\newline instead. Here are some other output samples of these two procedures:

write            display
#\space
"A \\/ B"        A \/ B
#\x              x
"\"Hi!\""        "Hi!"

Because output procedures are called for their side effects, their returned values are normally ignored. The following procedure makes use of this fact. It is used to call the same procedure a given number of times:

(define (do-times n f)
  (if (zero? n)
      (void)
      (begin (f)
             (do-times (- n 1) f))))

Do-times applies f n times and then returns a meaningless value using void. Note that the void procedure does not conform to the Scheme standard, although a lot of implementations do include it. If your Scheme does not provide it, you can define it using:

(define (void) (if #f #f))

When no alternative is given to if, it returns an unspecific value when the (non-existing) alternative is to be evaluated.

What is more interesting about do-times is its use of begin. This pseudo function is similar to and and or: it evaluates the expressions passed to it in sequence. Unlike and and or it does not ever do anything with the normal forms of these expressions, though. It always evaluates all of the given expressions and returns the value of the last one:

(begin 'foo) => foo
(begin 'foo 'bar 'baz) => baz

Because the values of all but the last expression are discarded, this pseudo function is only interesting if the expressions passed to it have side effects. In do-times the f procedure is called and its value is discarded. When f returns, do-times recurses to apply f another time. It can be used, for example, to write a string 10 times:

(do-times 10 (lambda () (display "Hello")))
HelloHelloHelloHelloHelloHelloHelloHelloHelloHello
=> #<unspecific>

In case you want each of the 10 occurrences of Hello to be written on a fresh line, begin can help, too:

(do-times 10 (lambda ()
               (begin
                 (display "Hello")
                 (newline))))

Newline is a built-in procedure that does the same as (display #\newline) but is slightly easier to type.

Note that begin is not really necessary in the above expression, because the body of lambda contains an implied begin, which allows you to write:

(do-times 10 (lambda ()
               (display "Hello")
               (newline)))

The read procedure reads only complete forms. Once applied, it waits until an object has been read entirely:

(read) (define foo ; this is ignored
(lambda () 'bar)
)
=> (define foo (lambda () 'bar))

This is pretty handy when parsing Scheme programs, but makes it hard to read some specific characters alone, such as the semicolon or the opening parenthesis. This is why Scheme has a procedure for reading raw characters, too:

(read-char)x => #\x

Read-char reads a single character and returns it. Because it does not process its input in any way, it can be used to read any character:

(read-char)( => #\(

Note that read-char reads the same input stream as the Scheme system itself, so when you use it at the Scheme prompt, you have to place the character to read immediately after the last closing parenthesis. If you leave a blank in between, that blank will be read instead:

(read-char) ) => #\space

After reading the space in this example, the closing parenthesis will be fed to the Scheme evaluator which will probably complain about it.

There is no built-in procedure for reading lines of text, but such a procedure can easily be constructed using read-char.

(define (read-line)
  (letrec
    ((collect-chars
       (lambda (c s)
         (cond ((eof-object? c)
             (cond ((null? s) c)
               (else (apply string (reverse s)))))
           ((char=? c #\newline)
             (apply string (reverse s)))
           (else (collect-chars (read-char)
                                (cons c s)))))))
    (collect-chars (read-char) '())))

Scheme's input/output procedures are not limited to the screen and the keyboard. They can be used to read and write files, too. Scheme uses so-called ports to implement access to files, but most port-related procedures work in an imperative way and hence do not integrate well with the procedural paradigm. Only such file I/O procedures that fit well in applicative programs will be discussed here.

The with-input-from-file procedure opens an input file for reading:

(with-input-from-file "some-file" read-line)
=> "first line of file"

The first argument of with-input-from-file specifies the file to read. Its second argument must be a procedure of zero arguments. With-input-from-file opens the given file and connects the default input port to that file. In this context it evaluates the given procedure. When the procedure returns, the default inport port is re-connected to the file or device that was in effect before the call to with-input-from-file.

The effect of redirecting the default input port is that all input read via read or read-char is read from the specified file rather than the user's terminal.

When a file specified in with-input-from-file does not exist, the application reduces to bottom:

(with-input-from-file "non-existant" read) => bottom

You can read more than a single line, character, or form by passing a procedure to with-input-from-file that does whatever you want with the input from the given file. The following procedure copies the content of a file to the default output port, effectively typing that file on the screen:

(define (type from)
  (with-input-from-file from
    (lambda ()
      (letrec
        ((type-chars
           (lambda (c)
             (cond ((eof-object? c) c)
               (else (display c)
                     (type-chars (read-char)))))))
        (type-chars (read-char))))))

Although the cond of type-chars has only two clauses, it is not replaced by if for a reason. Clauses of cond, like the bodies of lambda, imply begin, so you can place any number of expressions after the predicate of a clause.

The eof-object? procedure used in type tests whether the object passed to it is the so-called EOF object. The EOF object is a unique, unreadable object that is returned by read and read-char when a read operation is attempted on a file that offers no more input.

With-input-from-file has a counterpart named with-output-to-file. As its name suggests, it redirects the default output port in the same way as with-input-from-file redirects the input port.

The result of specifying an existing file in with-output-to-file is unspecified by the standard, so one Scheme environment may overwrite the file silently and another one may report an error and abort program execution.

Using with-output-to-file and type, it is easy to write a procedure that copies the content of one file to another:

(define (copy from to)
  (with-output-to-file to
    (lambda ()
      (type from))))

Because (type from) is reduced in a context where the default output is directed to the file to, the content of from is typed to that file. With-input-from-file and with-output-to-file redirect the input and output of all I/O procedures inside of their contexts, [Footnote: unless the I/O procedure specifies an explicit port, but this feature is not discussed here.] so no special preparations have to be made inside of type.