Laboratory Exercises For Computer Science 151

Object-Oriented Programming

Object-Oriented Programming

Goals: This laboratory exercise reviews some common approaches for tackling complex problems, providing a perspective on various types of abstraction. Object-oriented programming in the context of Scheme is introduced as an additional approach utilizing abstraction. The lab also provides experience with the case statement as a convenient construct for working within an object-oriented framework. Finally, the lab introduces stacks as an example.

Handling Complexity: Since many computer applications address complex problems, problem-solving with computers must include a consideration of the handling of complexity. Already in this course, we have seen several such strategies and use a variety of language capabilities.

Each of these topics allow us to organize a complex problem or task into relatively manageable parts and then to focus on those parts. Note that several of the items lists involve abstraction: we think of processing at a relatively high level, with details pushed aside to a separate procedure, structure, or file.

For example, association lists allow a natural matching of keyword and data, so we can focus upon the storage and retrieval of these information pairs. The assoc procedure retrieves relevant data, and we need not be bothered about the details of this retrieval. Similarly, the records lab shows how to think of data elements as part of a logical structure, with natural storage and retrieval operations.

  1. Identify how you have used each of the above approaches or capabilities in the solving of at least two specific problems during this course.

Abstract Data Types: The notion of an abstract data type further extends the strategy of abstraction within problem solving. The term abstract data type or ADT describes both a collection of data values and operations on those values. Thus, an ADT utilizes both data abstraction and procedural abstraction.

In sections 12.2.2 through 12.2.5, the textbook describes four simple examples of ADTs as boxes, counters, accumulators, and gauges. Here, we consider the Stack ADT as an illustration -- expanding the discussion in section 12.3 of the textbook.

Stacks as ADTs: Conceptually, the stack abstract data type mimics the information kept in a pile on a desk. Informally, we first consider materials on a desk, where we may keep separate piles for bills that need paying, magazines that we plan to read, and notes we have taken. These piles of materials have several properties. Each pile contains some papers (information). In addition, for each pile, we can do several tasks:

These operations allow us to do all the normal processing of data at a desk. For example, when we receive bills in the mail, we add them to the pile of bills until payday comes. We then take the bills, one at a time, off the top of the pile and pay them until the money runs out.

When discussing these operations it is customary to call the addition of an item to the top of the pile a Push operation and the deletion of an item from the top a Pop operation.

More formally, a stack is defined as an abstract data type that can store data and that has the following operations:

This specification says nothing about how we will program the various stack operations; rather, it tells us how stacks can be used. We can also infer some limitations on how we can use the data. For example, stack operations allow us to work with only the top item on the stack. We cannot look at other pieces of data lower down in the stack without first using Pop operations to clear away items above the desired one.

A Push operation always puts the new item on top of the stack, and this is the first item returned by a Pop operation. Thus, the last piece of data added to the stack will be the first item removed.

Classes, Objects, and Messages: One approach to problem solving involves an initial focus on data elements, operations on those elements, and relationships among data. Within computer science, such an approach motivates the viewpoint of object-oriented programming or OOP. When working within a OOP framework, it is useful to distinguish between the general notion of an abstract data type and specific uses of the ADT with definite data values. (For example, in Scheme we have found it useful to distinguish between an integer type and specific integer values.) Within OOP, the definition of an ADT (involving data variables and operations) is called a class, and specific instances of an ADT (with specific data values) are called objects.

Utilizing the notion of abstraction, we think of objects as self-contained entities, just as we might consider each pile on a desk as an separate, independent collection of material. To support this image, processing involving objects consists of sending messages to the objects, letting the object react to the object in an appropriate way, and receiving responses back. Within an OOP context, a mechanism for interpreting messages is called a method. For example, if our desk had two piles -- one for magazines and one for bills, we might have the following interaction:


> (define mag-pile (make-stack))     ; make stack for magazines
> (define bill-pile (make-stack))    ; make stack for bills
> (bill-pile 'push! "mortgage")      ; send push message to bill-pile
> (bill-pile 'push! "doctor's bill")
> (bill-pile 'push! "credit card")
> (bill-pile 'empty?)                ; send empty? message to bill-pile
#f                                   ; response from empty? message
> (mag-pile 'empty?)
#t
> (mag-pile 'push! "Communications of the ACM - April 1997")
> (mag-pile 'push! "CS Education Bulletin - Spring 1997")
> (bill-pile 'top)                   ; send top message to bill-pile
"credit card"                        ; data returned as message response
> (mag-pile 'top)
"CS Education Bulletin - Spring 1997"
> (bill-pile 'pop!)
"credit card"
> (bill-pile 'top)
"doctor's bill"
Stacks in Scheme: To implement an abstract data type within Scheme, we must define appropriate local variables for the data and provide a framework for an object to respond to messages. Both of these tasks are accomplished by the following code. (The code is based on Springer and Friedman's program 12.12, with adaptations by John Stone and further simplifications by Henry Walker):

(define make-stack
   (lambda ()
     (let ((stk '()))
        (lambda (message . args)
           (case message   ;; identify and respond to various messages
                ((empty?) (null? stk))

                ((push!) (set! stk (cons (car args) stk)))

                ((top) (if (null? stk)
                           (error 'stack-adt "top: The stack is empty")
                           (car stk)))

                ((pop!) (if (null? stk)
                          (error 'stack-adt "pop!: The stack is empty")
                          (let ((result (car stk)))
                            (set! stk (cdr stk))
                            result)))

                ;; It is an error to send the stack any other message.
                (else (error 'stack-adt "stack: unrecognized message")))))))
Data Within A Stack: In this code, note that the variable stk is allocated outside of the lambda-expression for the procedure being returned. This ensures that it will persist between calls to that procedure. Further, note that a new local variable is created for stk each time make-stack is invoked.

Operations On A Stack: Beyond the declaration of a local variable, a stack responds to various messages. Following Scheme's syntax for procedures of variable arity, a stack requires a message as a first parameter. The processing of some messages may require further information, so additional parameters, if any, are collected in the args list.

  1. Define two stacks and perform some operations on them, following the script given earlier in this lab. In each case, be sure you have a conceptual understanding of the results.

The Case Statement: Before responding to a message, the stack must determine which message has been sent. One way to accomplish this would be to use a cond expression:

(cond ((eq? message 'empty?) ...)
       (eq? message 'push!)  ...)
       (eq? message 'top)    ...)
       (eq? message 'pop)    ...)
       (else                 ...))
)
While this construction provides a mechanism to determine which message is present, the case special form shown in the above program does this work a bit more cleanly. As you can see, the general form of a case special form is:

(case message
     ((empty)     ...)
     ((push!)     ...)
     ((pop!)      ...)
     ((top)       ...)
     (else        ...)
)
Here, message is compared with the choices empty, push!, ..., with these options being in parentheses. When a match is found, the corresponding action is taken.

Additional examples using the case special form are given in section 12.2.1 of the textbook.

  1. Define vowel-or-consonant as shown in the textbook, and check it with various test cases.

  2. Expand the stack ADT by adding

  • Textbook Exercise: As you have time, work on exercise 12.9 in the textbook. (This exercise illustrates a common type of application for stacks.)
    
    


    This document is available on the World Wide Web as

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

    created April 28, 1997
    last revised April 30, 1997