In our last session, we looked at two problems involving the use of files, developing a solution for each one in pseudocode -- a step-by-step outline of what we need to do to solve the problem. In this lab, we'll work up the Scheme implementations of those solutions.
Problem 1: Find the sum of the numbers in a given file.
The pseudocode solution to this problem looked like this:
Here are the built-in Scheme procedures that we need to do various parts of this job:
The open-input-file procedure takes as argument a string
that names an existing, accessible file; it returns a ``port'' through
which data values, such as integers, can be read in from the file.
The read procedure, which we have previously seen in its
zero-argument form as a way of obtaining data from the user at the
keyboard, also has a one-argument form: If an input port is supplied as the
argument read reads in, through that port, the next value
stored in the file.
When the end of the file has been reached, read reports
this fact by returning a special ``end-of-file object'' instead of a normal
datum. The eof-object? predicate takes one argument,
typically something that read has just returned, and itself
returns #t if its argument is the end-of-file object and
#f otherwise.
The close-input-port procedure takes one argument, an
input port, and closes it, freeing the resources used to connect the
program to the file.
Here is the Scheme implementation of the pseudocode:
(define sum-of-file
(lambda (source-file-name)
(let ((source (open-input-file source-file-name)))
; Open the file.
(let loop ((total 0) ; Initialize the running total.
(next (read source))) ; Try to read a number.
(if (eof-object? next) ; If you get the end-of-file object,
(begin
(close-input-port source) ; close the file
total) ; and report the final total.
(loop (+ next total) ; Otherwise, add the number to
; the running total,
(read source))))))) ; try to read another number,
; and repeat the loop.
A typical interaction using this procedure would look like this:
> (sum-of-file "/u2/stone/courses/scheme/lab-1.input") 200
The file /u2/stone/courses/scheme/lab-1.input contains the
four numbers
50 50 75 25
(as read aloud by the input-port impersonator during the in-class demonstration!).
Problem 2: Given a file containing numbers, with one or more numbers on each line, compute the average of the numbers on each line and write it in a new file. (In other words, the new file should contain one number for each line of the given file -- the average of the numbers on that line.)
The pseudocode solution to this problem was:
To detect the end of a line in Scheme, we need a procedure that has not yet
been introduced: peek-char. The peek-char
procedure takes one argument, an input port, and returns the first unread
character that can be accessed through that port. It does not actually
read that character or extract it from the port -- it just peeks at it to
see what it will be when and if it is (subsequently) read in.
If the value returned by (peek-char source) is the newline
character, #\newline, then we know that we're at the end of
the line and can proceed to average the numbers that we've encountered.
(This is also a good time to read in and discard the newline character, so
that we can start into the next line of numbers without encountering it
again.)
Like (read source) and (read-char source),
(peek-char source) returns the end-of-file object if there are
no more characters in the file.
Here's a procedure that manages the inner loop of the pseudocode shown
above. It reads in one line of numbers from the input port
source and writes their average to the output port.
(define average-line
(lambda (source target)
(let loop2 ((total 0) ; Initialize the running total
(tally 0) ; Initialize the tally.
(ch (peek-char source))) ; Peek at the next character.
(if (char=? ch #\newline) ; If it's a newline character,
(begin
(read-char source) ; discard it,
(write (/ total tally) target) ; compute the average and write
; it to the target file,
(newline target)) ; and terminate the line in the
; target file.
(let ((next (read source))) ; Otherwise, read a number.
(loop2 (+ total next) ; Add it to the running total.
(+ tally 1) ; Add 1 to the tally.
(peek-char source))))))) ; Peek at the next character
; and repeat the loop.
The Scheme implementation of the solution to problem #2 is now easy to
write: Open up the files, call average-line once for each
line of the input file, and finally close the files:
(define average-each-line
(lambda (source-file-name target-file-name)
(let ((source (open-input-file source-file-name))
; Open the input file.
(target (open-output-file target-file-name)))
; Open the output file.
(let loop1 ((ch (peek-char source))) ; Peek at the next character.
(if (eof-object? ch) ; If you get the eof-object,
(begin
(close-input-port source) ; close the input file
(close-output-port target)) ; and the output file.
(begin ; Otherwise,
(average-line source target) ; read the numbers on one
; line and compute and
; write their average.
(loop1 (peek-char source)))))))) ; Peek at the next character
; and repeat the loop.
Here's what a typical invocation of this procedure looks like:
> (average-each-line "/u2/stone/courses/scheme/lab-2.input" "lab-2.output")
Nothing shows up on screen, because the last operation performed by
average-each-line is the call to
close-output-port, which returns an unspecified value (and
Chez Scheme doesn't bother to print unspecified values). All the action
takes place off stage, in the files: If the input file contains two lines
of numbers -- say, for instance,
25 100 50 50 200 50 25 75
-- then the program will create the output file lab-2.output,
looking like this:
85 50
but the creation of this file is invisible to the interactive Scheme user.
OK, now it's your turn:
Write and test a Scheme procedure that takes two arguments -- the name of an input file containing zero or more integers, and the name of an output file to be created by the procedure -- and copies each integer from the input file to the output file if it is in the range from 0 to 99. Values outside of this range should be read in but not copied out again. The idea is that this procedure will act as a filter, ensuring that only the values that are in the correct range will make it into the output file.
Line breaks in the input file should be ignored. In the output file, arrange for each integer to be printed on a line by itself.
Here is a Scheme procedure that copies an input file to an output file character by character:
(define copy-file
(lambda (source-file-name target-file-name)
(let ((source (open-input-file source-file-name))
(target (open-output-file target-file-name)))
(let loop ((ch (read-char source)))
(if (eof-object? ch)
(begin
(close-input-port source)
(close-output-port target))
(begin
(write-char ch target)
(loop (read-char source))))))))
Modify this procedure so that every lower-case letter that is read in is converted to upper case before being written to the output file.
Write a Scheme procedure tally-char that takes two
arguments, the name of an input file and a character, and returns a tally
of the number of occurrences of that character in the specified file.
(tally-char "/u2/stone/courses/scheme/lab-1.input" #\5) ===> 4 (tally-char "/u2/stone/courses/scheme/lab-2.input" #\0) ===> 7 (tally-char "/u2/stone/courses/scheme/lab-2.input" #\newline) ===> 2
This document is available on the World Wide Web as
http://www.math.grin.edu/courses/Scheme/file-examples.html