Fundamentals of Computer Science I (CS151.01 2006F)
[Skip to Body]
Primary:
[Front Door]
[Syllabus]
[Glance]
[Search]

[Academic Honesty]
[Instructions]
Current:
[Outline]
[EBoard]
[Reading]
[Lab]
[Homework]
Groupings:
[EBoards]
[Examples]
[Exams]
[Handouts]
[Homework]
[Labs]
[Outlines]
[Projects]
[Readings]
Reference:
[Scheme Report (R5RS)]
[Scheme Reference]
[DrScheme Manual]
Related Courses:
[CSC151.02 2006F (Davis)]
[CSCS151 2005S (Stone)]
[CSC151 2003F (Rebelsky)]
[CSC153 2004S (Rebelsky)]
This reading is also available in PDF.
Contents:
An abstract data type is a set of values and operations on those values, considered independently of the ways in which those values might be represented and the operations implemented in actual programs. Separating the definition of an abstract data type from its implementation is a technique that has been found to be especially useful in the development of large software systems.
Objects produced by a constructor procedure, such as the switches in the reading on objects in Scheme, can often be developed efficiently from the definition of an abstract data type, with the advantage that other procedures that take such objects as arguments cannot operate on them or modify them in any way not considered by the definition of the abstract data type. As a result, the methods of such an object can often be implemented in such a way as to preserve certain simplifying invariants  conditions that are known to be true at the beginning and end of the execution of each method. Relying on such invariants often allows the programmer to dispense with some precondition tests in the methods, because the invariants imply that the preconditions will be met whenever the method is called.
As illustrations of the use of abstract data types in the development of programs, we consider a frequently encountered ADT that Scheme happens not to supply: the stack.
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 stacks for bills that need paying, magazines that we plan to read, and notes we have taken. We can perform several operations that involve a stack:
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, from the top of the pile and pay them until the money runs out.
When discussing these operations, it is conventional to call the addition of an item to the top of the stack a push operation and the deletion of an item from the top a pop operation. (These terms are derived from the workings of a springloaded rack containing a stack of cafeteria trays. Such a rack is loaded by pushing the trays down onto the springs; as each diner removes a tray, the lessened weight on the springs causes the stack to pop up slightly.)
Here is a more formal definition of the stack ADT: A stack is a data structure containing zero or more elements, on which the following operations can be performed:
#t
if it is and false
if it is not.
This abstract data type definition says nothing about how we will program the various stack operations; rather, it tells us how stacks can be used. We can 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 elements farther 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.
We can implement stacks in Scheme as objects that respond to the messages
':empty?
, ':push!
, ':pop!
, and
':top
. The create operation will correspond to the
constructor procedure makestack
, which takes no arguments and
returns an empty stack.
But how do we store the values in the stack? We use a list. The front
element of the list represents the top of the stack. To push something
on the stack, we cons it at the front of the list. To pop the stack, we
take the cdr of the list. Since we want to change the stack, we keep
a reference to the list in a vector, which we call stk
.
;;; Procedure: ;;; makestack ;;; Parameters: ;;; (none) ;;; Purpose: ;;; Creates a stack ;;; Produces: ;;; stack, an object ;;; Postconditions: ;;; stack responds to the following messages: ;;; :type ;;; Returns the type of the object. ;;; :>string ;;; Summarizes the stack in a string. (Does not show contents.) ;;; :empty? ;;; Check if the stack is empty. ;;; :push! value ;;; Push a value on the stack. ;;; :pop! ;;; Get the top value on the stack and remove it. ;;; :top ;;; Get the top value on the stack and do not remove it. (define makestack (lambda () (let ((stk (vector null))) (lambda (message . arguments) (let ((lst (vectorref stk 0))) (cond ((eq? message ':type) 'stack) ; The tostring message intentionally reveals little ; about the contents. ((eq? message ':>string) "#<stack>") ((eq? message ':empty?) (null? (vectorref stk 0))) ((eq? message ':push!) (if (null? arguments) (error "stack:push!: an argument is required") (vectorset! stk 0 (cons (car arguments) lst)))) ((eq? message ':pop!) (if (null? lst) (error "stack:pop!: the stack is empty") (begin (vectorset! stk 0 (cdr lst)) (car lst)))) ((eq? message ':top) (if (null? lst) (error "stack:top: the stack is empty") (car lst))) (else (error "stack: unrecognized message"))))))))
Since the vector stk
is allocated during the definition
process, outside of the lambda
expression for the procedure
being returned, it will persist as part of the object between operations on
that object. Further, note that a different static variable is created each
time makestack
is invoked. Thus, a program can arrange for
the construction of any number of stacks, which can be pushed and popped
independently.
Stacks are also useful when dealing with more complex forms of recursion, such as recursive procedures that call themselves multiple times. Rather than including both recursive calls in our code, we put information about the recursive call on the stack, and then repreatly process the remaining values on the stack.
For example, suppose we have a tree of values and want to simultaneously
count the number of symbols, strings, and numbers that appear in the
tree. We'll return a list of three values (number of symbols, number
of strings, number of numbers). We'll call that procedure
tallythings
.
Because writing procedures that recurse on trees can be difficult, let's start by writing such a procedure for lists.
The base case is easy: For an empty list, there are no symbols, strings, or numbers, so we return a list of three 0's.
(cond ((null? lst) (list 0 0 0))
Now, let's suppose we see a useful value, such as a symbol. What do we want to do? We want to recurse on the rest of the list, and then add 1 to the car. Here's one way to do so.
((symbol? (car lst)) (cons (+ 1 (car (tallythings (cdr lst)))) (cdr (tallythings (cdr lst)))))
I don't know if your mental alarms are going off yet, but they should be. The two identical recursive calls here can lead to a lot of extra work. Hence, we should do a single recursive call and name the result.
((symbol? (car lst)) (let ((recursiveresult (tallythings (cdr lst)))) (cons (+ 1 (car recursiveresult)) (cdr recursiveresult))))
Our code for strings is similar, except that we're filling in the middle value.
((string? (car lst)) (let ((recursiveresult (tallythings (cdr lst)))) (list (car recursiveresult) (+ 1 (cadr recursiveresult)) (caddr recursiveresult))))
Putting it all together, we get something like the following:
;;; Procedure: ;;; tallythings ;;; Parameters: ;;; lst, a list. ;;; Purpose: ;;; Count the numbers of symbols, strings, and numbers that ;;; appear in lst. ;;; Produces: ;;; (symboltally stringtally numbertally), a list of three integers ;;; Preconditions: ;;; (None) ;;; Postconditions: ;;; symboltally contains the number of symbols that appear at the ;;; top level of lst. ;;; stringtally contains the number of strings that appear at the ;;; top level of lst. ;;; numbertally contains the number of numbers that appear at the ;;; top level of lst. (define tallythings (lambda (lst) (cond ((null? lst) (list 0 0 0)) ((symbol? (car lst)) (let ((recursiveresult (tallythings (cdr lst)))) (cons (+ 1 (car recursiveresult)) (cdr recursiveresult)))) ((string? (car lst)) (let ((recursiveresult (tallythings (cdr lst)))) (list (car recursiveresult) (+ 1 (cadr recursiveresult)) (caddr recursiveresult)))) ((number? (car lst)) (let ((recursiveresult (tallythings (cdr lst)))) (list (car recursiveresult) (cadr recursiveresult) (+ 1 (caddr recursiveresult))))) (else recursiveresult))))
Of course, it's a bit of a pain that we construct lists of values in each recursive call only to immediately deconstruct each list in the surrounding call. The normal solution is to write a kernel that accumulates the three tallies as we go.
(define tallythings (lambda (lst) (let kernel ((remaining lst) (symboltally 0) (stringtally 0) (numbertally 0)) (cond ((null? remaining) (list symboltally stringtally numbertally)) ((symbol? (car remaining)) (kernel (cdr remaining) (+ 1 symboltally) stringtally numbertally)) ((string? (car remaining)) (kernel (cdr remaining) symboltally (+ 1 stringtally) numbertally)) ((number? (car remaining)) (kernel (cdr remaining) symboltally stringtally (+ 1 numbertally))) (else (kernel (cdr remaining) symboltally stringtally numbertally))))))
This version is both more efficient than the previous verion and easier to read (at least for many programmers).
Now, let's turn to the problem of tallying in a tree. (Remember, that's where we began this problem.) The normal strategy for trees is to have a case for pairs, for the empty list, and for other values (the leaves). As in the case of lists, let's start by writing a version in which we don't have a kernel that keeps track of the various tallys. In fact, this version is clearner than the original version for lists.
For the case null
, we're back to return a list of three
0's.
(cond ((null? tree) (list 0 0 0))
When we hit a leaf, we return the appropriate list of values.
((symbol? tree) (list 1 0 0)) ((string? tree) (list 0 1 0)) ((number? tree) (list 0 0 1))
Now, pairs are the hard part. In this case, we need to count in both subtrees and then add.
((pair? tree) (let ((left (tallythings (car tree))) (right (tallythings (cdr tree)))) (list (+ (car left) (car right)) (+ (cadr left) (cadr right)) (+ (caddr left) (caddr right)))))
Putting it all together, we get the following:
;;; Procedure: ;;; tallythings ;;; Parameters: ;;; tree, a tree ;;; Purpose: ;;; Count the numbers of symbols, strings, and numbers that ;;; appear in the tree. ;;; Produces: ;;; (symboltally stringtally numbertally), a list of three integers ;;; Preconditions: ;;; (None) ;;; Postconditions: ;;; symboltally contains the number of symbols that appear in tree. ;;; stringtally contains the number of strings that appear in tree. ;;; numbertally contains the number of numbers that appear in tree. (define tallythings (lambda (lst) (cond ((null? tree) (list 0 0 0)) ((symbol? tree) (list 1 0 0)) ((string? tree) (list 0 1 0)) ((number? tree) (list 0 0 1)) ((pair? tree) (let ((left (tallythings (car tree))) (right (tallythings (cdr tree)))) (list (+ (car left) (car right)) (+ (cadr left) (cadr right)) (+ (caddr left) (caddr right))))) (else (list 0 0 0)))))
All well and good, except for one little thing: Once again, we're
building lots and lots of lists, only to take them apart. What
do we do? Well, we want to add the tallies we added before.
Unfortunately, we have the little problem of two recursive calls
to tallythings
. So, what do we do? We get to the
reason that this example appears in this reading: We can use a
stack to keep track of the parts of the tree we have not yet
processed. At each step, we process the next remaining part of the
tree. If it's a symbol, string, or number, we increment the
appropriate tally. If it's a pair, we push both halves on the stack
for future processing.
(define tallythings (lambda (tree) (let ((remaining (makestack))) (remaining ':push! tree) (let kernel ((symboltally 0) (stringtally 0) (numbertally 0)) (if (remaining ':empty?) (list symboltally stringtally numbertally) (let ((current (remaining ':pop!))) (cond ((null? current) (kernel symboltally stringtally numbertally)) ((symbol? current) (kernel (+ 1 symboltally) stringtally numbertally)) ((string? current) (kernel symboltally (+ 1 stringtally) numbertally)) ((number? current) (kernel symboltally stringtally (+ 1 numbertally))) ((pair? current) (remaining ':push! (cdr current)) (remaining ':push! (car current)) (kernel symboltally stringtally numbertally)) (else (kernel symboltally stringtally numbertally)))))))))
A bit longer, but significantly more efficient.
The idea of storing parts of a tree (or otherwise storing parts of a multiplyrecursive procedure) on a stack is a useful programming strategy. Keep an eye out for other occasions to use it.
http://www.cs.grinnell.edu/~rebelsky/Courses/CS151/History/Readings/stacks.html
.
[Skip to Body]
Primary:
[Front Door]
[Syllabus]
[Glance]
[Search]

[Academic Honesty]
[Instructions]
Current:
[Outline]
[EBoard]
[Reading]
[Lab]
[Homework]
Groupings:
[EBoards]
[Examples]
[Exams]
[Handouts]
[Homework]
[Labs]
[Outlines]
[Projects]
[Readings]
Reference:
[Scheme Report (R5RS)]
[Scheme Reference]
[DrScheme Manual]
Related Courses:
[CSC151.02 2006F (Davis)]
[CSCS151 2005S (Stone)]
[CSC151 2003F (Rebelsky)]
[CSC153 2004S (Rebelsky)]
Disclaimer:
I usually create these pages on the fly
, which means that I rarely
proofread them and they may contain bad grammar and incorrect details.
It also means that I tend to update them regularly (see the history for
more details). Feel free to contact me with any suggestions for changes.
This document was generated by
Siteweaver on Thu Nov 30 21:43:55 2006.
The source to the document was last modified on Mon Nov 20 22:47:47 2006.
This document may be found at http://www.cs.grinnell.edu/~rebelsky/Courses/CS151/2006F/Readings/stacks.html
.
You may wish to validate this document's HTML ; ;
Samuel A. Rebelsky, rebelsky@grinnell.eduhttp://creativecommons.org/licenses/bync/2.5/
or send a letter to Creative Commons, 543 Howard Street, 5th Floor,
San Francisco, California, 94105, USA.