On MathLAN, a utility program called cp is often used to
create an exact duplicate of a given text file. In a dtterm
window, the command
cp original duplicate
copies the file named original into a new file named
duplicate. You wind up with two files that have exactly the
same contents.
At this point, we can write a Scheme program to do exactly the same thing:
(define copy-file
(lambda (name-of-original name-of-duplicate)
(let ((source (open-input-file name-of-original))
(target (open-output-file name-of-duplicate)))
(let loop ((next-character (read-char source)))
(if (eof-object? next-character)
(begin
(close-input-port source)
(close-output-port target))
(begin
(write-char next-character target)
(loop (read-char source))))))))
(copy-file "original" "duplicate")
In English: Let source be a port through which we can pull
characters in from the file to be copied, and let target be a
port through which we can push characters out to the new file. Try to read
a character from source. If it's the end-of-file object,
close both ports and exit; otherwise, write the character to
target, try to read another character from
source, and repeat this step.
Since every new call to the loop procedure consumes one
character from the source file, the end of that file will ultimately be
reached and the recursive calls will cease. Since loop is
tail-recursive, it's all right to generate a new recursive call for each
character of the original file, even if that file is quite large.
In this arrangement of the program, the copy-file procedure
receives the strings that name the files involved and is
responsible for opening and closing the ports to those files. An
alternative approach, frequently used because of its greater flexibility,
is to write the copying procedure so that it takes the ports as
arguments, making the caller responsible for opening them before the
procedure call and closing them afterwards. Here's how the program looks
if this approach is used:
(define port-copy
(lambda (source target)
(let loop ((next-character (read-char source)))
(if (not (eof-object? next-character))
(begin
(write-char next-character target)
(loop (read-char source)))))))
(let ((in (open-input-file "original"))
(out (open-output-file "duplicate")))
(port-copy in out)
(close-input-port in)
(close-output-port out))
Adapt the second version of the copying program so that it copies only letters and whitespace characters into the output file, discarding all others.
Let's say that the complement of the character in position n in the ASCII character set is the character in position 127 - n. (For example, the complement of the capital Y, which is in position 89, is the ampersand, &, which is in position 38.) Adapt the second version of the copying program so that, instead of echoing each character from the source file into the target file without change, the program replaces each character with its complement, producing an encrypted file.
An input port operation is a Scheme procedure that takes an input
port as its only argument. For instance, it would be easy to rewrite the
sum-of-file procedure as an input port operation, by requiring
the caller to create the port before invoking the procedure and to close it
afterwards:
(define port-sum
(lambda (source)
(if (not (input-port? source))
(error 'port-sum "The argument must be an input port"))
(let kernel ((total 0)
(next-number (read source)))
(if (eof-object? next-number)
total
(kernel (+ total next-number) (read source))))))
One advantage of writing this procedure as an input port operation is that
one can then use the built-in Scheme procedure
call-with-input-file to invoke it. The
call-with-input-file procedure takes two arguments, the first
of which is a string that names an existing file and the second an input
port operation. Call-with-input-file automatically opens the
file, invokes the input port procedure (giving it the port to the input
file), collects the value that it returns, closes the port, and returns the
value collected from the input port procedure. In other words, it works
as if it were defined like this:
(define call-with-input-file
(lambda (name-of-input-file operation)
(let* ((source (open-input-file name-of-input-file))
(result (operation source)))
(close-input-port source)
result)))
If the file numbers.dat contains nothing but numbers, the
following expression computes the sum of those numbers:
(call-with-input-file "numbers.dat" port-sum)
Write an input port operation port-size that reads characters
one at a time through a given port until it encounters the end-of-file
object, then returns the number of characters read (not including the
end-of-file object).
Use port-size and call-with-input-file to
determine how many characters are in the file
/u2/stone/courses/scheme/sample.dat.
Naturally, there is a corresponding notion of an output port
operation -- a procedure that takes an output port as its only
argument, and Scheme provides a built-in procedure
call-with-output-file that takes as its arguments a string
that names a file to be created and an output port operation, opens a port
to the specified output file, runs the output port operation on that port,
closes the port, and returns the result of the output port operation. (At
this point, call-with-output-file is somewhat less useful,
because it's hard to think of plausible output port operations -- the
interesting output procedures take two or more arguments. Shortly we'll
see how to get around this restriction.)
One handy procedure that is not built into Scheme is
read-line, which takes an input port as its argument and
returns a string containing all the characters from the next line of text
that is available through that port (not including the newline character
that terminates the line). If no more characters are available,
read-line returns the end-of-file object. Here's the code:
(define read-line
(lambda (in)
(let loop ((so-far '())
(next-character (read-char in)))
(cond ((eof-object? next-character) next-character)
((char=? next-character #\newline)
(list->string (reverse so-far)))
(else
(loop (cons next-character so-far) (read-char in)))))))
In English: Initially, the list of characters encountered on the current
line is empty. Try to read in a character. If you get the end-of-file
object, return it -- no more characters are available. If you get the
newline character, reverse the list of characters encountered so far and
compress it into a string with the list->string procedure.
Otherwise, add the character just read to the list of characters
encountered so far, try to read in another character, and repeat.
Using the read-line procedure, write a Scheme procedure that
takes as arguments an input port and an output port, reads a line at a time
from the input port and writes to the output port the length of each line
that it reads (i.e., the number of characters on that line, not including
the newline character that terminates the line).
Figure out how to test the procedure you wrote in the preceding exercise and run the test.
This document is available on the World Wide Web as
http://www.math.grin.edu/~stone/courses/scheme/files-continued.html
created October 28, 1997
last revised October 28, 1997