Laboratory Exercises For Computer Science 151

Input and Output

Input and Output

Goals: This laboratory discusses interactive Scheme programming, using read, display and write statements. The lab also mentions the use of sentinel values to halt the reading of data from the keyboard.

Problem: In a class, students each have taken two tests, as described in the following define statement:


(define class 
        '( ("Egbert Bacon"      88    85)
           ("William Hemingway" 79    63)
           ("Frances Homer"     94    86)
           ("Po Wer MacHinery"  84    91)
           ("R. McDonald"       82    96)
           ("L. Bo Peep"        76    78)
           ("A. N. Onymous"     72    81)
           ("Henry Shakespeare" 90    92)
           ("I. M. Silly"       87    70)
           ("P. Arty Time"      62    59)
           ("Mac K. Walker"     93    87))
)
Given this data, we would like to write a program to fill in the following table:

    Name               Test 1  Test 2  Average
    Egbert Bacon         88      85      ...
    William Hemingway    79      63      ...
       ...
       ...
       ...
    Averages:            ...     ...
Solution: A previous lab described how to use the display procedure to print output to the screen and how to use newline to move from one line of output to the next. In what follows, we use these procedures repeatedly to print the table desired for the current problem.

The structure of our solution will parallel the form of the table:

We now consider each of these output elements in turn.

Printing the Title: Overall, we want a class-print procedure which takes a class of the above form as a parameter. Since the display procedure prints text, we can produce the title with a simple display and newline at the start of class-print. This suggests the following start for our solution:


(define class-print ;;; version 1 with title
    (lambda (class-list)
        ;;; Print Table Header
        (display "Name               Test 1  Test 2  Average")
        (newline)
    )
)
Printing Names, Their Scores, and Their Averages: To print successive names and their scores, we move recursively down the class -- extracting the name and each score from each student entry. Since we follow the same work for each successive name, we use a local procedure print-student for each person on the class list.

To compute an average, we extract the scores for test 1 and test 2 from the information about a student and we average these values.

To print all-students, we proceed down the class list -- starting with the entire list and continuing recursively until we have only the null class list remaining. This yields the next version of class-print.


(define class-print ;;; version 2 with first cut at student entries
    (lambda (class-list)
        ;;; Print Table Header
        (display "Name               Test 1  Test 2  Average")
        (newline)

        ;;; Print successive class entries
        (let ((print-student ;;; local procedure for individual student
                  (lambda (student-entry)
                      (let ((student (car student-entry))
                            (test-1 (cadr student-entry))
                            (test-2 (caddr student-entry)))
                          (display student)
                          (display test-1)
                          (display test-2)
                          (display (/ (+ test-1 test-2) 2.0))
                          (newline)
                      )
                  )
              ))
             ;;; recursive procedure to run through class
             (let all-students ((class class-list))
                   (if (null? class)
                           "End of table"
                           (begin
                               (print-student (car class))
                               (all-students (cdr class))
                           )
                   )
              )
        )
    )
)
When this procedure is run with the data set we first described, the following output is obtained:

> (class-print class)
Name               Test 1  Test 2  Average
Egbert Bacon888586.5
William Hemingway796371.0
Frances Homer948690.0
Po Wer MacHinery849187.5
R. McDonald829689.0
L. Bo Peep767877.0
A. N. Onymous728176.5
Henry Shakespeare909291.0
I. M. Silly877078.5
P. Arty Time625960.5
Mac K. Walker938790.0
"End of table"
While this output shows the correct information, all values are compressed together. Thus, we will need to add some spaces between the test scores and between the name and the first score.

Adding space between the test scores is reasonably straightforward: we might print 6 spaces between one number and the next.

Adding space after the name is slightly more complicated, however, as the names have different lengths. One simple approach to handle this task is to pad each name with a reasonably large number of spaces. Then, we use the substring procedure to select the desired number of characters for the name part of the table. The next version of the table program follows:


(define class-print ;;; version 3 with formatted student entries
    (lambda (class-list)
        ;;; Print Table Header
        (display "Name               Test 1  Test 2  Average")
        (newline)

        ;;; Print successive class entries
        (let ((print-student ;;; local procedure for individual student
                  (lambda (student-entry)
                      (let ((student (car student-entry))
                            (test-1 (cadr student-entry))
                            (test-2 (caddr student-entry)))
                          ;;; take 22 characters of padded name
                          (display (substring 
                                     (string-append student "            ")
                                       0  21)) 
                          (display test-1)
                          (display "      ")  ;;;spaces printed here
                          (display test-2)
                          (display "      ")  ;;; more spaces here
                          (display (/ (+ test-1 test-2) 2.0))
                          (newline)
                      )
                  )
              ))
             ;;; recursive procedure to run through class
             (let all-students ((class class-list))
                   (if (null? class)
                           "End of table"
                           (begin
                               (print-student (car class))
                               (all-students (cdr class))
                           )
                   )
              )
        )
    )
)
  1. The above procedure contains three let expressions. Explain what each one does and why it is placed where it is.
Computing Class Averages: To complete the table, we must consider how to determine the averages for test 1 and test 2. From the definition of an average, we must compute the number of students in the class and the sum of the scores for each test.

To allow tail recursion within all-students, we keep these totals as additional parameters -- adding each new student's data during the recursion. When all students have been processed (e.g., when we reach the null list as the base case), the final average information can be printed. This motivates the following program:


(define class-print ;;; version 4 -- the almost completed program
    (lambda (class-list)
        ;;; Print Table Header
        (display "Name               Test 1  Test 2  Average")
        (newline)

        ;;; Print successive class entries
        (let ((print-student ;;; local procedure for individual student
                  (lambda (student-entry)
                      (let ((student (car student-entry))
                            (test-1 (cadr student-entry))
                            (test-2 (caddr student-entry)))
                          ;;; take 22 characters of padded name
                          (display (substring 
                                     (string-append student "            ")
                                       0  21)) 
                          (display test-1)
                          (display "      ")  ;;;spaces printed here
                          (display test-2)
                          (display "      ")  ;;; more spaces here
                          (display (/ (+ test-1 test-2) 2.0))
                          (newline)
                      )
                  )
              ))
             ;;; recursive procedure to run through class
             (let all-students ((class class-list)
                                (test-1-total 0)
                                (test-2-total 0)
                                (number-students 0))
                   (if (null? class)
                           (begin ;;; print final averages
                                (display "Averages             ")
                                (display (/ test-1-total number-students))
                                (display "  ")
                                (display (/ test-2-total number-students))
                                (newline)
                           )
                           (begin
                               (print-student (car class))
                               (all-students (cdr class)
                                   (+ test-1-total (cadar class))
                                   (+ test-2-total (caddar class))
                                   (+ number-students 1))
                           )
                   )
              )
        )
    )
)
When we run this program, we obtain the following:

> (class-print class)
Name               Test 1  Test 2  Average
Egbert Bacon         88      85      86.5
William Hemingway    79      63      71.0
Frances Homer        94      86      90.0
Po Wer MacHinery     84      91      87.5
R. McDonald          82      96      89.0
L. Bo Peep           76      78      77.0
A. N. Onymous        72      81      76.5
Henry Shakespeare    90      92      91.0
I. M. Silly          87      70      78.5
P. Arty Time         62      59      60.5
Mac K. Walker        93      87      90.0
Averages             907/11  888/11
This gives correct results, except that we might have preferred to have the class averages in decimal form rather than printed as fractions. Further, we might like the results printed to two decimal places.

One way to accomplish this is to initialize test-1-total and to the real number 0.0, so the final division will give a real number result.

  1. Change this initialization as suggested and rerun the program on the class data given. Is the result satisfactory? Why not?

  2. Following the discussion of procedure round-n-places in section 6.4 of the textbook, we can use the round procedure to give a result to 2 decimal places as follows: If R is a real number, then
    
      (/ (round (* R 100)) 100.0)
    
    will give a result to two decimal places.

    Describe why does this approach returns the value of R to the desired number of decimal places.

    Use this approach to rounding to adjust the printing of averages in the previous program. Adjust spacing, so that columns remain aligned.

  3. Modify the previous program to compute and print the average of the "Average" column as well.

Because of the development of this procedure over several steps, procedure print-student contains a let statement so that three variables student, test-1 and test-2 may be used conveniently wihtin the procedure. In this final form, however, similar values are needed within all-students as well.

The following program moves the let statement from the print-student procedure to the else clause of all-students -- replacing the begin. Then the three values for a student are passed as parameters in the call to print-student, and the same values are used locally within the else clause.


(define class-print ;;; version 5 -- another program 
    (lambda (class-list)
        ;;; Print Table Header
        (display "Name               Test 1  Test 2  Average")
        (newline)

        ;;; Print successive class entries
        (let ((print-student ;;; local procedure for individual student
                  (lambda (student test-1 test-2)
                     ;;; take 22 characters of padded name
                     (display 
                        (substring (string-append student "            ")
                               0  21)) 
                     (display test-1)
                     (display "      ")  ;;;spaces printed here
                     (display test-2)
                     (display "      ")  ;;; more spaces here
                     (display (/ (+ test-1 test-2) 2.0))
                     (newline)
                  )
              ))
             ;;; recursive procedure to run through class
             (let all-students ((class class-list)
                                (test-1-total 0)
                                (test-2-total 0)
                                (number-students 0))
                   (if (null? class)
                       (begin ;;; print final averages
                           (display "Averages             ")
                           (display (/ test-1-total number-students))
                           (display "  ")
                           (display (/ test-2-total number-students))
                           (newline)
                       )
                       (let* ;;; process data for a student and continue
                             ((student-entry (car class))
                              (student (car student-entry))
                              (test-1 (cadr student-entry))
                              (test-2 (caddr student-entry)))
                          (print-student student test-1 test-2)
                          (all-students (cdr class)
                               (+ test-1-total test-1)
                               (+ test-2-total test-2)
                               (+ number-students 1))
                       )
                   )
              )
        )
    )
)
This revised program differs from version 4 in several ways:

  1. Compare versions 4 and 5 of this solution.
    1. In what ways do they have similar structure?
    2. Why is let* used in version 5, while let is used in version 4.
    3. Why is the final begin expression of version 4 not needed in version 5?
    4. Is one version more efficient than the other? Why or why not?
    5. Is one version clearer or easier to understand than the other the other? Why or why not?
    6. Which version do you think represents better code? Explain.
Reading and Writing: Sometimes, in addition to printing, it is convenient to read values from the keyboard. This is accomplished with the read procedure with returns with the value of the next item typed at the keyboard. The use of this procedure is illustrated with two approaches to the following simple problem

Problem: Write a procedure that reads 3 scores from the keyboard and averages them:

Approach 1: The first approach explicitly calls read 3 times in its computation:


(define average-3
    (lambda ()
        (display "Enter three numbers:  ")
        (display (/ (+ (read) (read) (read)) 3.0))
        (display " is the average of the three numbers")
        (newline)
    )
)
When we run this program to average the numbers 3, 5, and 10, we might have the following interaction with the computer:

> (average-3)
Enter three numbers:  3   5  10
6.0 is the average of the three numbers
Here, we began processing by calling the procedure with (average-3). Note that this procedure does not require any parameters.

When the program runs, the first display procedure prompted the user to enter some numbers. Then, before the next display can finish, three values must be read and averaged. Each call to procedure read fetches the next value, and the average then is computed.

  1. What happens if you enter three fractions or some negative numbers when you run this procedure?

  2. What happens if you enter a string instead of a number when you run this procedure?

Approach 2: We might proceed recursively, binding each value read to a local variable. A separate parameter within a kernel procedure could record how many values have been read:

(define average-3
    (lambda ()
        (display "Enter three numbers:  ")
        (let kernel ((sum 0)
                     (number-to-process 3))
                    (if (= number-to-process 0)
                        (begin ;;; base case -- all numbers read
                            (display (/ sum 3.))
                            (display " is the average of the three numbers")
                            (newline)
                        )
                        ;;; else -- read another number and add it
                        (let ((value-read (read)))  ;; read and bind value
                             (kernel (+ sum value-read) 
                                     (- number-to-process 1)))
                    )
        )
    )
)
  1. Test the above procedure to be sure it works correctly.

  2. In this version of average-3, a value is read and bound to a local variable value-read. Write a sentence or two to explain how value is used later in the processing.

  3. Replace the final let in the else clause by a call to read within the call to kernel.

    Comment upon whether the resulting code seems simpler or more complex than the version given above.

  4. Suppose you wanted to average any number of values; you might ask the computer to continue reading numbers until you entered a 0. In this context, you could pass the current sum and the number of values read to the kernel procedure. The base case would be identified by checking if the current value read was zero.

    Write an average procedure which reads successive numbers until 0 is entered and computes and prints the average of these numbers. Be sure you do not include 0 in your computation of the average.

    Note: In this problem, the number 0 is called a sentinel. Generally, a sentinel is a value read that tells the computer something special about how processing should proceed.

  5. Textbook Exercises: As you have time, work on exercises 6.5-6.7 from the textbook.

Work to be turned in:


This document is available on the World Wide Web as

http://www.math.grin.edu/~walker/courses/151/lab-i-o.html

created March 5, 1997
last revised October 29, 1998