The lab begins by developing solutions for two problems. In each case, a solution is given in pseudocode -- a step-by-step outline of what we need to do to solve the problem. Then, the lab develops the Scheme implementations of those solutions.
Problem 1: Find the sum of the numbers in a given file.
The general form for this problem follows a common approach for much file processing. First, we open the file; then, we read and process data; finally, we close the file and print the results. The pseudocode solution that follows adds a few more details:
Here are the built-in Scheme procedures, together with some programming hints, that can help us do various parts of this job:
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.
let expression.
let initializes the running total
to 0 and initializes the parameter next as the first value read
from the file.
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.
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.
close-input-port procedure takes one argument, an
input port, and closes it, freeing the resources used to connect the
program to the file.
(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") 200The file
/u2/stone/courses/scheme/lab-1.input contains the
four numbers
50 50 75 25
(read source) appears twice in the
above code. What is the purpose of each appearance of this expression?
The pseudocode solution to this problem is:
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 50but the creation of this file is invisible to the interactive Scheme user.
peek-char does and why it is
used here.
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.
(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.
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/~walker/courses/151/lab-file-examples.html