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 lab on object-oriented programming, 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 two frequently encountered data structures that Scheme happens not to supply as built-ins: stacks and queues.
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 spring-loaded 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:
create
Create a new, empty stack object.
empty
Determine whether the stack is empty; return true if it is and
false if it is not.
push
Add a new element at the top of a stack.
pop
Remove an element from the top of the stack and return it. (This operation
has a precondition: It cannot be performed if the stack is empty.)
top
Return the element at the top of the stack (without removing it from the
stack). (This operation, too, can be performed only if the stack is not
empty.)
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 make-stack, which takes no arguments and
returns an empty stack. The object will protect access to a static variable,
stk, which will contain all of the elements that are currently
in the stack, assembled into a list. Here is the code:
(define make-stack
(lambda ()
(let ((stk null))
(lambda (message . arguments)
(cond ((eq? message ':empty?)
(null? stk))
((eq? message ':push!)
(if (null? arguments)
(error "stack:push!: an argument is required")
(set! stk (cons (car arguments) stk))))
((eq? message ':pop!)
(if (null? stk)
(error "stack:pop!: the stack is empty")
(let ((removed (car stk)))
(set! stk (cdr stk))
removed)))
((eq? message ':top)
(if (null? stk)
(error "stack:top: the stack is empty")
(car stk)))
(else
(error "stack: unrecognized message")))))))
Since the field 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 make-stack is invoked. Thus, a program can arrange for
the construction of any number of stacks, which can be pushed and popped
independently.
Documents on the World Wide Web usually contain special strings, called tags, that serve as instructions to the browser about what the document contains, how it is structured, and how the text should be displayed. In many cases, tags occur in pairs: The opening tag marks the beginning of a region of text that constitutes some natural unit within the document structure or should be displayed in some special way, and closing tag marks the end of that region.
An opening tag is a sequence of letters and digits enclosed between a
less-than character at the beginning and a greater-than character at the
end. For instance, "<html>" is the opening tag for a
document that contains hypertext markup, and "<title>"
is the opening tag for the title of such a document. The corresponding
closing tags look almost the same, but a closing tag has a slash character
after the less-than character: "</html>",
"</title>".
Create a new stack and name it tags. Push onto this stack the
opening tag "<html>". Next, push
"<head>", the tag that begins the header of a hypertext
document, and then "<title>". Now pop the stack. The
tag that appears is the one that must be matched first by a closing tag in
order for the tags to be correctly nested. Pop the stack two more times and
confirm that the stack is a ``last-in, first-out'' data structure.
Stacks are useful when it is necessary to interrupt or postpone part of a computation until some simpler or more urgent computation has been completed -- some description of the unfinished computation can be pushed onto a stack to make room for the simpler or more urgent one. When the latter is finished, we pop the stack to recover and resume the unfinished computation.
For example, suppose that we have a tree of numbers (as described in the lab on deep recursion), and we want to find the sum of all the numbers in the tree. The approach we described there was to issue a recursive call every time we identified a non-empty subtree:
(define sum-all
(lambda (tr)
(cond ((null? tr) 0)
((list? (car tr)) (+ (sum-all (car tr)) (sum-all (cdr tr))))
(else (+ (car tr) (sum-all (cdr tr)))))))
This is an elegant solution, but the fact that computing a result may require two recursive calls, or one, or none can be confusing. We could avoid this confusion by building up the sum as a running total, adding in the numbers one by one and considering only one subtree at any given time.
We can manage this by maintaining a stack that will at all times contain the subtrees whose contents we have not yet added to the running total. Initially, we'll set up the stack so that it contains the whole tree as its only element. Subsequently, at each step, we pop one subtree off this stack. If it is null, we discard it without changing the running total. Otherwise, it must have a car and a cdr. The cdr is a subtree that we aren't ready to consider yet, so we push it onto the stack. The car might be a number and might be another tree of numbers. If it's a number, we add it to the running total and continue to the next step; if it's a tree of numbers, we push it onto the stack and proceed to the next step (without changing the running total). When the stack becomes empty, everything has been added to the running total, so we stop and return the accumulated value. In Scheme:
(define sum-all
(lambda (tr)
(let ((to-do (make-stack)))
(to-do ':push! tr)
(let kernel ((so-far 0))
(if (to-do ':empty?)
so-far
(let ((current (to-do ':pop!)))
(if (null? current)
(kernel so-far)
(begin
(to-do ':push! (cdr current))
(if (number? (car current))
(kernel (+ so-far (car current)))
(begin
(to-do ':push! (car current))
(kernel so-far)))))))))))
The idea of storing subproblems that we can't address immediately and recovering them later, when we're in a better position to solve them, is a useful programming strategy -- keep an eye out for appropriate occasions to use it.
Netscape and other browsers use a stack of tags like this one -- a stack
containing tags that must eventually be matched but have not been matched
yet -- to determine whether the HTML document to be displayed is
correctly constructed. Write a Scheme procedure
correctly-nested? that takes a list of HTML
opening and closing tags and determines whether they are correctly nested.
> (correctly-nested? '("<html>" "<head>" "<title>" "</title>"
"</head>" "<body>" "<b>" "</b>" "</body>" "</html>"))
#t
> (correctly-nested? '("<html>" "<head>" "</html>" "</head>"))
#f
Some authors add a sixth operation to the definition of the stack ADT:
size, which returns the number of elements in the stack. Extend the
Scheme implementation of make-stack above so that the stacks
it constructs will accept the message ':size and perform this
operation when it is received.
This document is available on the World Wide Web as
http://www.cs.grinnell.edu/~stone/courses/scheme/stacks.xhtml
created April 28, 1997
last revised May 1, 2000
Henry Walker (walker@cs.grinnell.edu) and John David Stone (stone@cs.grinnell.edu)