Sketchy LISP |
Copyright (C) 2006,2007
Nils M Holm Buy a copy at Lulu.com |
An Introduction to Functional Programming in Scheme
| Previous Section | - Contents - Index - | Next Section |
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.
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.
| Previous Section | - Contents - Index - | Next Section |