Structured Programming Everlyne
Structured Programming Everlyne
Academic Press
London New York San Francisco
A Subsidiary of Harcourt Brace Jovanovich, Publishers
A.P.I.C. Studies in Data Processing
No. 8
STRUCTURED
PROGRAMMING
0.-J. DAHL
Universitet i Oslo,
Matematisk Institut,
Blindern, Oslo, Norway
E. W. DIJKSTRA
Department of Mathematics,
Technological University,
Eindhoven, The Netherlands
C. A. R. HOARE
Department of Computer Science,
The Queen's University of Belfast,
Belfast, Northern Ireland
1972
ACADEMIC PRESS
LONDON AND NEW YORK
ACADEMIC PRESS INC (LONDON) LTD.
24/28 Oval Road,
London NWl
Copyright © I 972 by
ACADEMIC PRESS INC. (LONDON) LTD.
v
CONTENTS
Page
Preface v
VII
VDI CONTENTS
Page
12. Axiomatisation 166
References 174
EDSGER w. DIJKSTRA
1. To MY READER
These notes have the status of "Letters written to myself": I wrote them down
because, without doing so, I found myself repeating the same arguments
over and over again. When reading what I had written, I was not always too
satisfied.
For one thing, I felt that they suffered from a marked verbosity. Yet I do
not try to condense them (now), firstly because that would introduce another
delay and I would like to "think on", secondly because earlier experiences
have made me afraid of being misunderstood: many a programmer tends to
see his (sometimes rather specific) difficulties as the core of the subject and
as a result there are widely divergent opinions as to what programming is
really about.
I hope that, despite its defects, you will enjoy at least parts of it. If these
notes prove to be a source of inspiration or to give you a new appreciation
of the programmer's trade, some of my goals will have been reached.
Prior to their publication in book form, the "Notes on Struccured Pro-
gramming" have been distributed privately. The interest then shown in
them, for which I would like to express my gratitude here, has been one of
the main incentives to supplement them with some additional material and
to make them available to a wider public. In particular I would like to thank
Bob Floyd, Ralph London and Mike Woodger for their encouraging
comments and Peter Naur for the criticism he expressed. Finally I would
like to express my gratitude to Mrs. E. L. Dijkstra-Tucker for her kind
assistance in my struggles with the English language.
of the problem, as in (3), (4) and (5), because anyone knows what is meant
by the first value in the sequence, satisfying a condition? Certainly he does
not expect us, who have work to do, to supply such lengthy proofs, with all
the mathematical dressing, whenever we use such a simple loop as that?"
Etc.
To tell the honest truth: the pomp and length of the above proof infuriate
me as well! But at present I cannot do much better if I really try to prove the
correctness of this program. But it sometimes fills me with the same kind of
anger as years ago the crazy proofs of the first simple theorems in plane
geometry did, proving things of the same degree of "obviousness" as Euclid's
axioms themselves.
Of course I would not dare to suggest (at least at present!) that it is the
programmer's duty to supply such a proof whenever he writes a simple loop
in his program. If so, he could never write a program of any size at all! It
would be as impractical as reducing each proof in plane geometry explicitly
and in extenso to Euclid's axioms. (Cf. Section "On our inability to do
much.")
My moral is threefold. Firstly, when a programmer considers a construc-
tion like (6) as obviously correct, he can do so because he is familiar with the
construction. I prefer to regard his behaviour as an unconscious appeal to a
theorem he knows, although perhaps he has never bothered to formulate it;
and once in his life he has convinced himself of its truth, although he has
probably forgotten in which way he did it and although the way was
(probably) unfit for print. But we could call our assertions about program
(6), say, "The Linear Search Theorem" and knowing such a name it is much
easier (and more natural} to appeal to it consciously.
Secondly, to the best of llly knowledge, there is no set of theorems of the
type illustrated above, whose usefulness has been generally accepted. But we
should not be amazed about that, for the absence of such a set of theorems is a
direct consequence of the fact that the type of object-i.e. programs-has not
settled down. The kind of object the programmer is dealing with, viz.
programs, is much less well-established than the kind of object that is dealt
with in plane geometry. In the meantime the intuitively competent programmer
is probably the one who confines himself, whenever acceptable, to program
structures with which he is very familiar, while becoming very alert and
careful whenever he constructs sornething unusual (for him). For an estab-
lished style of programming, however, it might be a useful activity to look
for a body of theorems pertinent to such programs.
Thirdly, the length of the proof we needed in our last example is a warning
that should not be ignored. There is of course the possibility that a better
mathematician will do a much shorter and more elegant job than I have done.
Personally I am inclined to conclude from this length that programming is
NOTES ON STRUCTURED PROGRAMMING 11
more difficult than is commonly assumed: let us be honestly humble and
interpret the length of the proof as an urgent advice to restrict ourselves to
simple structures whenever possible and to avoid in all intellectual modesty
"clever constructions" like the plague.
4.3. ON ABSTRACTION
At this stage I find it hard to be very explicit about the role of abstraction,
partly because it permeates the whole subject. Consider an algorithm and all
possible computations it can evoke: starting from the computations the
algorithm is what remains when one abstracts from the specific values
manipulated this time. The concept of "a variable" represents an abstraction
from its current value. It has been remarked to me (to my great regret I
cannot remember by whom and so I am unable to give credit where it seems
due) that once a person has understood the way in which variables are used in
programming, he has understood the quintessence of programming. We can
find a confirmation for this remark when we return to our use of mathematical
induction with regard to the repetition: on the one hand it is by abstraction
that the concepts are introduced in terms of which the induction step can be
formulated; on the other hand it is the repetition that really calls for the
concept of "a variable". (Without repetition one can restrict oneself to
"quantities" the value of which has to be defined as most once but never has
to be redefined as in the case of a variable.)
There is also an abstraction involved in naming an operation and using it
on account of "what it does" while completely disregarding "how it works".
(In the same way one should state that a programming manual describes an
abstract machine: the specific piece of hardware delivered by the manu-
facturer is nothing but a-usually imperfect !-mechanical model of this
abstract machine.) There is a strong analogy between using a named operation
in a program regardless of "how it works" and using a theorem regardless
of how it has been proved. Even if its proof is highly intricate, it may be a
very convenient theorem to use!
Here, again, I refer to our inability to do much. Enumerative reasoning is
all right as far as it goes, but as we are rather slow-witted it does not go very
far. Enumerative reasoning is only an adequate mental tool under the severe
boundary condition that we use it only very moderately. We should appreciate
abstraction as our main mental technique to reduce the demands made upon
enumerative reasoning.
(Here Mike Woodger, National Physical Laboratory, Teddington, England,
made the following remark, which I insert in gratitude: "There is a parallel
analogy between the unanalysed terms in which an axiom or theorem is
expressed and the unanalysed operands upon which a named operation is
expected to act.")
12 E.W. DUKSTRA
7. ON UNDERSTANDING PROGRAMS
In my life I have seen many programming courses that were essentially like
the usual kind of driving lessons, in which one is taught how to handle a car
instead of how to use a car to reach one's destination.
My point is that a program is never a goal in itself; the purpose of a
program is to evoke computations and the purpose of the computations is to
establish a desired effect. Although the program is the final product made by
the programmer, the possible computations evoked by it-the "making" of
which is left to the machine!-are the true subject matter of his trade. For
instance, whenever a programmer states that his program is correct, he really
makes an assertion about the computations it may evoke.
The fact that the last stage of the total activity, viz. the transition from
the (static) program text to the (dynamic) computation, is essentially left to
the machine is an added complication. In a sense the making of a program is
therefore more difficult than the making of a mathematical theory: both
program and theory are structured, timeless objects. But while the mathe-
matical theory makes sense as it stands, the program only makes sense via its
execution.
In the remaining part of this section I shall restrict myself to programs
written for a sequential machine, and I shall explore some of the consequences
of our duty to use our understanding of a program to make assertions about
the ensuing computations. It is my (unproven) claim that the ease and
reliability with which we can do this depends critically upon the simplicity of
the relation between the two, in particular upon the nature of sequencing
control. In vague terms we may state the desirability that the structure of
the program text reflects the structure of the computation. Or, in other terms,
"What can we do to shorten the conceptual gap between the static program
text (spread out in "text space") and the corresponding computations
(evolving in time)?"
It is the purpose of the computation to establish a certain desired effect.
When it starts at a discrete moment t 0 it will be completed at a later discrete
moment t 1 and we assume that its effect can be described by comparing "the
state at t 0 " with "the state at t 1 ". If no intermediate states are taken into
consideration the effect is regarded as being established by a primitive action.
When we do take a number of intermediate states into consideration this
means that we have parsed the happening in time. We regard it as a sequential
computation, i.e. the time-succession of a number of subactions and we have
NOTES ON STRUCTURED PROGRAMMING 17
to convince ourselves that the cumulative effect of this time-succession of
subactions indeed equals the desired net effect of the total computation.
The simplest case is a parsing, a decomposition, into a fixed number of
subactions that can be enumerated. In flowchart form this can be represented
as follows.
r--- ---, I
1
I
I
I
I
I
I
I I
I
I
lJ __iJJ
51; 52; ..... ;Sn
we have represented that at the given level of detail the action "reduce r
modulo dd" can take one of two mutually exclusive forms and we have also
given the criterion on account of which the choice between them is made. If
we regard "if dd ~ r do" as a conditional clause attached to "decrease r by
dd" it is natural that the conditional clause is placed in front of the conditioned
statement. (In this sense the alternative clause
"if condition then statement I else statement 2"
is "over-ordered" with respect to "statement I" and "statement 2": they are
just two alternatives that cannot be expressed simultaneously on a linear
medium.)
The alternative clause has been generalised by C. A. R. Hoare whose
"case-of" construction provides a choice between more than two possibilities.
In flowchart form they can be represented as follows.
r-----
1
----, I
I I I
I I I
I I I
I 51 I 51 52
I I
I I
I I I
I
I I I
_ _ _ _ _ _ ...J
L.------ L------
i!? ~ 51 if?~ 51~52
- - - - - - - - - - - - - -., I
I
51
I
I
I I
I I
I " ,,,,
I I
'- - - - - - - - - - - - - - - - - - - - - - - - - - - _.J
r------------, r---- - - - - -,
I 1 I
I I
?
s
I
I
I
I 5
I
I
I I
L-------- - - ____ .J
These flowcharts also share the property of a single entry at the top and a
single exit at the bottom. They enable us to express that the action r:epresented
by the dotted block is on closer inspection a time-succession of "a sufficient
number" of subactions of a certain type.
We have now seen three types of decomposition; we could call them
"concatenation", "selection" and "repetition" respectively. The first two are
understood by enumerative reasoning, the last one by mathematical induction.
The programs that can be written using the selection clauses and the
repetition clauses as only the means for sequencing control, permit straight-
forwardJ.ranslation into a programming language that is identical but for the
20 E.W. DUKSTRA
the textual index identifies, say, semicolons.) The textual index is a kind of
generalised order counter, its value points to a place in the text.
If we restrict ourselves to decomposition by concatenation and selection, a
single textual index is sufficient to identify the progress of the computation.
With the inclusion of repetition clauses textual indices are no longer sufficient
to describe the progress of the computation. With each entry into a repetition
clause, however, the system could introduce a so-called "dynamic index",
inexorably counting the ordinal number of the corresponding current repeti-
tion; at termination of the repetition the system should again remove the
corresponding dynamic index. As repetition clauses may occur nested inside
each other, the appropriate mechanism is a stack (i.e. a last-in-first-out-
memory). Initially the stack is empty; at entry of a repetition clause a new
dynamic index( set to zero or one) is added on the top of the stack; whenever
it is decided that the repetition is not terminated the top element of this stack
is increased by 1 ; whenever it is decided that a repetition is terminated, the
top element of the stack is removed. (This arrangement reflects very clearly
that after termination of a repetition the number of times, even the fact that
it was a repetition, is no longer relevant.)
As soon as the programming language admits procedures, then a single
textual index is no longer sufficient. In the case that a textual index points
to the interior of a procedure body, the dynamic progress of the computation
is only characterised when we also describe to which call of the procedure we
refer, but this can be done by giving the textual index pointing to the place
of the call. With the inclusion of the procedure the textual index must be
generalised to a stack of textual indices, increased by one element at procedure
call and decreased by one element at procedure return.
The main point is that the values of these indices are outside the pro-
grammer's control; they are defined (either by the write-up of his program or
by the dynamic evolution of the current computation) whether he likes it or
not. They provide independent co-ordinates in which to describe the progress
of the computation, a "variable-independent" frame of reference in which
meanings to variables can be assigned.
There is, of course, even with the free use of jumps, a programmer inde-
pendent co-ordinate system in terms of which the progress of a sequential
computation can be described uniquely, viz. a kind of normalised clock that
counts the number of "discrete points of computation progress" passed since
program start. It is unique, but utterly unhelpful, because the textual index
is no longer a constituent component of such a co-ordinate system.
The moral of the story is that when we acknowledge our duty to control the
computations (intellectually!) via the program text evoking them, that then
we should restrict ourselves in all humility to the most systematic sequencing
NOTES ON STRUCTURED PROGRAMMING 23
mechanisms, ensuring that "progress through the computation" is mapped
on "progress through the text" in the most straightforward manner.
8. ON COMPARING PROGRAMS
"if B2 then
begin while Bl do Sl end
else
begin while Bl do S2 end" (1)
and
"while Bl do
begin if B2 then SI else S2 end" (2)
The first construction is primarily one in which sequencing is controlled
by a selective clause, the second construction is primarily one in which
sequencing is controlled by a repetitive clause. I can establish the equivalence
of the output of the computations, but I cannot regard them as equivalent in
any other useful sense. I had to force myself to the conclusiqn that (1) and
(2) are "hard to compare". Originally this conclusion annoyed me very much.
In the meantime I have grown to regard this incomparability as one of the
facts of life and, therefore, as one of the major reasons why I regard the
choice between (1) and (2) as a relevant design decision, that should not be
taken without careful consideration. It is precisely its apparent triviality
that has made me sensitive to the considerations that should influence such a
choice. They fall outside the scope of the present section but I hope to return
to them later.
Let me give a second example of incomparability that is slightly more
subtle.
Given two arrays X[l :N] and Y[l :N] and a boolean variable "equal",
make a program that assigns to the boolean variable "equal" the value:
"the two arrays are equal element-wise". Empty arrays (i.e. N = 0) are
regarded as being equal.
Introducing a variable j and giving to "equal" the meaning "among the
first j pairs no difference has been detected", we can write the following
two programs.
''j: = O; equal:= true;
while j =I= N do
beginj: = j + 1; equal: = equal and (XU] = YU]) end" (3)
and
''j: = O; equal:= true;
while j =I= N and equal do
beginj: = j + 1; equal:= (XU] = YU]) end". (4)
Program (4) differs from program (3) in that repetition is terminated as
soon as a pair-wise difference has been detected. For the same input the
NOTES ON STRUCTURED PROGRAMMING 25
number of repetitions may differ in the two programs and therefore the
programs are only comparable in our sense as long as the last two lines of the
programs are regarded as describing a single action, not subdivided into
subactions. But what is their relation when we do wish to take into account
that they both end with a repetition? To find this out, we shall prove the
correctness of the programs.
On the arrays X and Y we can define of 0 ~ j ~ N the N + I functions
EQUALi as follows:
for j = 0 EQUALi = true,
for j > 0 EQUALi = EQUALi- t and (XU]= YU]). (5)
In terms of these functions it is required to establish the net effect
equal= EQUALN.
Both programs maintain the relation
equal= EQUALi (6)
for increasing values of j, starting withj = 0.
It is tempting to regard programs (3) and (4) as alternative refinements
of the same (abstract) program (7):
''j: = O; equal:= EQUAL 0 ;
while "perhaps still:equal -::F EQUALN" do
beginj: = j +I; "equal:= EQUAL/' end" (7)
in which "perhaps still: equal"# EQUALN" stands for some sort of still open
primitive. When this is evaluated
equal= EQUAL1
will hold and the programs (3) and (4) differ in that they guarantee on different
criteria that "equal" will have its final value EQUALN.
In program (3) the criterion is very naive, viz.
j = N.
At the beginning of the repeated statement
equal = EQUALi
still holds. After the execution of ''j: = j + I" therefore
equal = EQUAL1 -1
holds and the assignment statement
"equal: = equal and (XU] = YU])"
is now a straightforward transcription of the recurrence relation (5).
To come to program (4) some analysis has to be applied to the recurrence
relation (5), from which can be derived (by mathematical induction again) that
26 E. W. DIJKSTRA
sake of argument, we assume this not to be the case. This means that in our
next refinement we have to express how the effect of these two actions can be
established by two further (sub)computations. Apart from that we have to
decide, how the information to be contained in the.intermediate value of the
still rather undefined object "table p" is to be represented.
Before going on, I would like to stress how little we have decided upon when
writing down description l, and how little of our original problem statement
has been taken into account. We have assumed that the availability of a
resource "table p" (in some form or other) would permit us to compute the
first thousand prime numbers before printing starts, and on this assumption
we have exploited the fact that the computation of the primes can be con-
ceived independently of the printing. Of our original problem statement we
have not taken into account very much more than that at least a thousand
different prime numbers do exist (we had to assume this for the problem
statement to make sense). At this stage it is still fairly immaterial what the
concept "prime number" really means. Also, we have not committed our-
selves in the least as regards the specific layout requirements of the print-out
to be produced. Apparently it is the strength of our approach that the
consequences of these two rather independent aspects of our original problem
statement seem to have been allocated in the respective refinements of our
two constituent actions. It suggests that we have been more or less successful
in our effort to apply the golden principle "divide and rule".
Resuming our discussion, however, we have to ask ourselves, to what extent
the two subcomputations can now be conceived independently of each other.
To be more precise "Have we now reached the stage that the design of the
two subalgorithms (that have to evoke the two subcomputations) can be
conceived by two programmers, working independently of each other?".
When the two actions can no longer be regarded as invoked by instructions
from the well-understood repertoire, neither can the variable "table p" any
longer be regarded as an implicitly available resource. And in a way similar
to the one in which we have to decompose the actions into subactions, we
have to choose how the variable "table p" will be composed, viz. what data
structure we select to represent the information to be handed over via "table
p" from the first action to the second. At some point this has to be decided
and the questions are "when?" and "how?".
In principle, there seem to be two ways out of this. The first one is to try
to postpone the decision on how to structure "table p" into (more neutral,
less problem-bound) components. If we postpone the decision on how to
structure "table p", the next thing to do is to refine one of the actions or both.
We can do so, assuming a proper set of operations on the still mysterious
object "table p"; finally we collect these operations and in view of their
demands we design the most attractive structure of "table p".
NOTES ON STRUCTURED PROGRAMMING 29
Alternatively, we can try to decide, here and now, upon the structure of
"table p". Once it has been decided how the table of the first thousand primes
will be represented, the refinements of both actions can be done fairly
independently of each other.
Both ways are equally tricky, for what will be an attractive algorithm for,
say, the first subcomputation will greatly depend on the ease and elegance with
which the assumed operations on "table p" can be realised, and if one or more
turn out to be prohibitively clumsy, the whole edifice falls to pieces. Alter-
natively, if we decide prematurely upon a structure for "table p" we may well
discover that the subcomputations then turn out to be awkward. There is
no way around it: in an elegant program the structure of "table p" and the
computations referring to it must be well-matched. I think that the behaviour
of the efficient programmer can be described as trying to take the easiest
decision first, that is the decision that requires the minimum amount of
investigation (trial and error, iterative mutual adjustment etc.) for the
maximum justification of the hope that he will not regret it.
In order not to make this treatment unduly lengthy we assume that the pro-
grammer finds the courage to decide that now the structure of "table p" is the
first thing to be decided upon. Once this position has been taken, two alter-
natives immediately present themselves. On the one hand we can try to exploit
that "a table of the first 1000 primes" is not just a table of a thousand
numbers-as would be a table of the monthly wages of 1000 employees in a
factory-but that all these numbers are different from each other. Using
this we can arrange the information with a linear boolean array (with con-
secutive elements associated with consecutive natural numbers) indicating
whether the natural number in question is a prime number or not. Number
theory gives us an estimation of the order of magnitude of the thousandth
prime number and thereby a boundary of the length of the array that will
suffice. If we arrange our material in that way we have prepared an easy
mechanism to answer the question "is n (less than the maximum) prime or
not?". Alternatively, we can choose an integer array in which the successive
prime numbers will be listed. (Here the same estimate, obtained by means of
number theory, will be used, viz. when a maximum value of the integer array
elements needs to be given a priori.) In the latter form we create a mechanism
suited to answer the question "what is the value of the kth prime number,
fork ~ 1000?".
We grant the programmer the courage to choose the latter representation.
It seems attractive in the printing operation in which it is requested to print
the prime numbers and not to print natural numbers with an indication
whether they are prime or not. It also seems attractive for the computing
stage, if we grant the programmer the clairvoyance that the analysis of
30 E.W. DIJKSTRA
whether a given natural number is a prime number or not, will have some-
thing to do with the question of whether prime factors of the number to be
investigated can be found.
The next stage of our program refinement then becomes the careful state-
ment of a convention regarding the representation of the still mysterious
object "table p" and a redefinition of the two operations in terms of this
convention.
The convention is that the information to be contained in "table p" will
be represented by the values of the elements of the "integer array p[l : 1000]",
such that for l ~ k ~ 1000 p[k] will be equal to the kth prime number, when
the prime numbers are arranged in order of increasing magnitude. (If a
maximum value of the integers is implicitly understood, we assume that
number theory allows us to state that this is large enough.)
When we now want to describe this new refinement we are faced with a new
difficulty. Our description 1 had the form of a single program, thanks to the
fact that it was a refinement of the single action named "print the first
thousand prime numbers", referred to in description 0. (In more conventional
terms: description l could have the form of a procedure body.) This no longer
holds for our next level, in which we have to refine (simultant'ously, in a sense)
three named entities, viz. "table p" and the two actions, and we should
invent some sort of identifying terminology indicating what refines what.
For the continuation of our discussion we make a very tentative proposal.
We say: description 0 is a valid text expressed in terms of a single named
action "print first thousand prime numbers"; let this be identified by the
code Oa.
Description 1 is called "l" because it is the next refinement of description
O; it contains a refinement of Oa-the only term in which description 0 is
expressed-and is itself expressed in terms of three named entities to which
we attach the codes:
"table p" la
"fill table p with first thousand prime numbers" lb
"print table p" le
code numbers, starting with I, because description 1 is expressed in terms of
them, and "a'', "b" and "c" being attached for the purpose of distinction.
Now we have to describe our convention chosen for the representation of
the information to be contained in "table p'', but this convention pertains to
all three elements la, lb and le. Therefore we call this description 2; it should
contain the descriptions of the three separate elements (I use the equality sign
as separator)
NOTES ON STRUCTURED PROGRAMMING 31
description 2:
la = "integer array p[l : 1000]"
lb = "make fork from 1 through 1000 p[k] equal to the kth prime number"
le= "printp[k] fork from 1through1000".
Description 2 is expressed in terms of three named entities to which we
give (in the obvious order) the codes 2a, 2b and 2c. (In code numbers,
description 2 is very meagre: it just states that for la, lb and le, we have
chosen the refinements 2a, 2b and 2c respectively.)
Remark. In the representation of the information to be contained in "table
p", we have chosen not to exploit the fact that each of the values to be printed
occurs only once, nor that they occur in the order of increasing magnitude.
Conversely, this implies that the action that has to take place under the name
of 2c is regarded as a specific instance of printing any set of thousand integer
values (it could be a table of monthly wages of thousand numbered
employees!). The net effect of the printing action in this example is an uniquely
defined as the first thousand prime numbers are: we conceive it, however, as a
specific instance of a larger class of occurrences. In the further refinement of
2c we deal with this whole class, the specific instance in this class being
defined by the values of the elements of the array p. When people talk about
"defining an interface" I often get the feeling that they overlook the pre-
supposed generalisation, the conception of the class of "possible" actions.
When 2b and 2c occur among the well-understood repertoire of instructions
(and therefore 2a among the resources implicitly available) our whole problem
is solved. For the sake of argument we again assume this not to be the case,
and so we find ourselves faced with the task of conceiving subcomputations
for the actions 2b and 2c. But now, thanks to the introduction of level 2,
the respective refinements of 2b and 2c can be designed independently.
The refinement of 2b: "make fork from 1 through 1000 p[k] equal to the
kth prime number".
We are looking for description 2bl, i.e. the first refinement of 2b. We
introduce a fresh numbering after 2b (rather than calling our next description
"3 something") in order to indicate the mutual independence of the refine-
ments of 2b and 2c respectively.
In description 2bl we have to give an algorithm describing how the elements
of the array p will get their values. This implies that we have to describe, for
instance, in what order this will happen. In our first refinement we shall
describe just that and preferably nothing more. An obvious, but ridiculous
version starts as follows (with "version number" enclosed within parentheses):
2bl(l):
beginp[l]: = 2; p[2]: = 3; p[3]: = S; p[4]: = 7; p[S]: = 11; ......... end
32 E. W. DIJKSTRA
implying that the programmer's knowledge includes that of a table of the first
thousand primes. We shall not pursue this version as it would imply that the
programmer hardly needed the machine at all.
The first prime number being given ( = 2), the thousandst being assumed
unknown to the programmer, the most natural order in which to fill the ele-
ments of the array p seems to be in the order of increasing subscript value,
and if we express just that we arrive (for instance) at
'Zbl(2):
begin integer k, j; k: = O; j: = 1;
while k < 1000 do begin "increase j until next prime number";
k:= k +l;p[k]:=jend
end
By identifying k as the number of primes found and by verifying that our
first prime number ( = 2) is indeed the smallest prime number larger than 1
(=the initial value of j), the correctness of 2bl(2) is easily proved by
mathematical induction (assuming the existence of a sufficient number of
primes).
Description 2bl(2) is a perfect program when the operation described by
"increase j until next prime number"-call it 2bl(2)a-occurs among the
repertoire, but let us suppose that it does not. In that case we have to express
in a next refinement how j is increased (and, again, preferably nothing more).
We arrive at a description of level 2b2(2)
2bl(2)a =
begin boolean jprime;
repeatj: = j + 1;
"give to jprime the meaning: j is a prime number"
until jprime
end
Remark. Here we use the repeat-until clause in order to indicate that j
has always to be increased at least once.
Again its correctness can hardly be subject to doubt. If, however, we
assume that the programmer knows that, apart from 2, all further prime
numbers are odd, then we may expect him to be dissatisfied with the above
version because of its inefficiency. The price to be paid for this "lack of
clairvoyance" is a revision of version 2bl(2). The prime number 2 will be
dealt with separately, after which the cycle can deal with odd primes only.
Instead of 2b1(2) we come to
NOTES ON STRUCTURED PROGRAMMING 33
2bl(3):
begin integer k,j; p[l]: = 2; k: = 1;j:=1;
while k < 1000 do
begin "increase odd j until next odd prime number";
k: = k + 1; p[k]: = j
end
end
where the analogous refinement of the operation between quotes-"2bl(3)a"
say-leads to the description on level 2b2(3):
2bl(3)a =
begin booleanjprime;
repeatj: = j + 2;
"give for odd j to jprime the meaning: j is a prime number";
until jprime
end
The above oscillation between two levels of description is in fact nothing
else but adjusting to our convenience the interface between the overall
structure and the primitive operation that has to fit into this structure. This
oscillation, this form of trial and error, is definitely not attractive, but with a
sufficient lack of clairvoyance and being forced to take our decisions in
sequence, I see no other way: we can regard our efforts as experiments to
explore (at a rather low cost!) where the interface can probably be most
conveniently chosen.
Remark. Both 2bl(2) and 2bl(3) can be loosely described as
begin "set table p and j at initial value";
while "table p not full" do
begin "increase j until next prime number to be added";
"add j to table p"
end
end
but we shall not do this as the sequencing in the two versions differs (see
"On comparing programs") and we regard them as "incomparable". By
choosing 2bl(3) we decide that our trial 2bl(2)-as 2bl(l)-is no longer
applicable and therefore rejected.
The change from 2bl(2) to 2bl(3) is justified by the efficiency gain at the
levels of higher refinement. This efficiency gain is earned at level 2b2, because
34 E. W. DIJKSTRA
now j can be increased by 2 at a time. It will also manifest itself in the still
open primitive at level 2b2(3) where the algorithm for "give for odd j to
jprime the meaning: j is a prime number" has only to cater for the analysis
of odd values of j.
Again: in 2b2(3) we have refined 2bl(3) with an algorithm which solves our
problem when "give for oddj to jprime the meaning: j is a prime number"-
call it "2b2(3)a"-occurs among the well-understood repertoire. We now
assume that it does not, in other words we have to evoke a computation
deciding whether a given odd value of j has a factor. It is only at this stage
that the algebra really enters the picture. Here we make use of our knowledge
that we only need to try prime factors: furthermore we shall use the fact that
the prime numbers to be tried can already be found in the filled portion of
the array p.
We use the facts that
(1) j being an odd value, the smallest potential factor to be tried is p(2],
i.e. the smallest prime number larger than 2
(2) the largest prime number to be tried is p[ord - 1] when p[ord] is the
smallest prime number whose square exceeds j.
(Here I have also used the fact that the smallest prime number whose square
exceeds j can already be found in the table p. In all humility I quote Don
Knuth's comment on an earlier version of this program, where I took this
fact for granted:
"Here you are guilty of a serious omission! Your program makes use of a
deep result of number theory, namely that if Pn denotes the nth prime
number we always have
Pn+i < p/."
Peccavi.)
If this set is not empty, we have a chance of finding a factor, and as soon
as a factor has been found, the investigation of this particular j value can be
stopped. We have to decide in which order the prime numbers from the set will
be tried, and we shall do so in order of increasing magnitude, because the
smaller a prime number the larger the probability of its being a factor of j.
When the value of ord is known we can give for "give for odd j to jprime
the meaning: j is a prime number" the following description on level 2b3(3):
2b2(3)a =
begin integer n; n: = 2; jprime: = true;
while n < ord and jprime do
begin "give tojprime the meaning: p[n] is not a factor ofj"; n: = n + 1
end
end
NOTES ON STRUCTURED PROGRAMMING 35
But the above version is written on the assumption that the value of ord,
a function of j, is known. We could have started this refinement with
begin integer n, ord;
ord: = 1; while p[ord] f 2 ~ j do ord: = ord + 1;
i.e. recomputing the value of "ord" afresh, whenever it is needed. Here some
trading of storage space for computation time seems indicated: instead of
recomputing this function whenever we need it, we introduce an additional
variable ord for its current value: it has to be set when j is set, it has to be
adjusted whenj is changed.
This, alas, forces upon us some reprogramming. One approach would be to
introduce, together withj, an integer variable ord and to scan the programs in
order to insert the proper operations on ord, whenever j is operated upon. I do
not like this because at the level at which j is introduced and has a meaning,
the function "ord" is immaterial. We shall therefore try to introduce ord only
at its appropriate level and we shall be very careful.
For 2b: "make for k from 1 through 1000 p[k] equal to the kth prime
number" we write (analogous to level 2bl(3))
level 2b1(4):·
begin integer k, j; p[l]: = 2; k: = l;
"set j to one";
while k < 1000 do
begin "increase odd j until next odd prime number";
k: = k + 1; p[k]: = j
end
end
expressed in terms of
2b1(4)a "increase oddj until next odd prime number"
2b1(4)b "setj to one".
In our next level we only introduce the subcomputation for 2bl(4)a; the
other is handed down.
level 2b2(4):
2bl(4)a =
begin boolean jprime;
repeat "increase j with two";
"give for oddj to jprime the meaning: j is a prime numher"
until jprime
end:
36 E.W. DIJKSTRA
2bl(4)b = 2b2(4)b
expressed in terms of
2b2( 4)b still meaning "set j to one"
2b2(4)c "increase j with two"
2b2(4)d "give for odd j to jprime the meaning: j is a prime number".
It is only at the next level that we need to talk about ord. Therefore we
now write
level 2b3(4): integer ord;
2b2(4)b =
beginj: =I; "set ord initial" end;
2b2(4)c =
beginj: = j + 2; "adjust ord" end;
2b2(4)d =
begin integer n; n: = 2; jprime: = true;
while n < ord and jprime do
begin "give to jprime the meaning: p[n] is not a factor of j";
n: = n +I
end
end
expressed in terms of
2b3(4)a "set ord initial"
2b3(4)b "adjust ord"
2b3(4)c "give tojprime the meaning: p[n] is not a factor ofj".
In our next level we give two independent refinements. (Note. We could
have given them in successive levels, but then we should have to introduce an
arbitrary ordering to these two levels. We could also try to treat the refine-
ments separately-Le. as separately as 2b and 2c-but we feel that it is a little
premature for this drastic decision.) We are going to express
(I) that, ord being a non-decre:\sing function of j and j only increasing in
value, adjustment of ord implies a conditional increase;
(2) that, whether p[n] is a factor of j is given by the question whether the
remainder equals zero.
This leads to
level 2b4(4):
2b3(4)a = 2b4(4)a
NOTES ON STRUCTURED PROGRAMMING 37
2b3(4)b =
begin while "ord too small" do "increase ord by one" end;
2b3(4)c =
begin integer r;
"maker equal to remainder of j over p[n]";
jprime: = (r =/: 0)
end
expressed in terms of
2b4(4)a still meaning "set ord initial"
2b4(4)b "ord too small"
2b4(4)c "increase ord by one"
2b4(4)d "maker equal to remainder of j over p[n]"
If we have a built-in division, the implementation of "make r equal to the
remainder ofj over p[n]" can be assumed to be an easy matter. The case that
the refinement of 2b4(4)d can be treated independently is now left to the
interested reader. To give the algorithm an unexpected turn we shall assume
the absence of a convenient remainder computation. In that case the algorithm
"r: = j; while r > 0 do r: = r - p[n]"
would lead to the (non-positive) remainder but it would be most unattractive
from the point of view of computation time. Again this asks for the intro-
duction of some additional tabulated material (similar to the way in which
"ord" has been introduced).
We want to know whether a gtven value ofj is a multiple of p[n] for n < ord.
In order to assist us in this analysis we introduce a second array in the
elements of which we can store multiples of the successive prime numbers, as
close to j as is convenient. In order to be able to give the size of the array we
should like to know an upper bound for the value of ord; of course, 1000
would be safe, but number theory gives us 30 as a safe upper bound. We
therefore introduce
integer array mult [1 : 30]
and introduce the convention that for n < ord, mult [n] will be a multiple of
p[n] and will satisfy the relation
mult [n] < j + p[n]
a relation that remains invariantly true under increase ofj. Whenever we wish
to investigate, whether p[n] is a factor of j, we increase mult [n] by p[n] as
long as
mult [n] < j.
38 E.W. DIJKSTRA
After this increase mult [n] = i is the necessary and sufficient condition for
j to be a multiple of p[n].
The low maximum value of ord has another consequence: the inspection
"ord too small" can be expressed by
"p[ord] j 2 ~ j"
but this inspection has to be performed many times for the same value of ord.
We may assume that we can speed up matters by introducing a variable
(called "square") whose value equals p[ord] j 2.
So we come to our final
level 2b5(4):
integer square; integer array mult [I : 30];
2b4(4)a =
begin ord: =I; square:= 4 end;
2b4(4)b =
(square ~ j);
2b4(4)c =
begin mult [ord]: = square; ord: = ord + l; square:= p[ord] j 2 end;
2b4(4)d =
begin while mult [n] < j do mult [n]: = mult [n] + p[n]; r: = j - mult [n] end
which has made our computation close to an implementation of the Sieve of
Eratosthenes!
Note. In the refinement of 2b4(4)d, when mult[n] is compared with the
current value of j, mult[n] is increased as much as possible; this could have
been done in steps of 2 * p[n], because we only submit odd values of j and
therefore are only interested in odd multiples of p[n]. (The value of mult[l]
remains, once set, equal to 4.)
The refinement of 2c "print p[k] fork from I through 1000" is left to the
reader. I suggest that the table should be printed on five pages, each page
containing four columns with fifty consecutive prime numbers.
* *
*
Here I have completed what I announced at the beginning of this section,
viz. "to describe in very great detail the composition process of such a
[well-structured] program". I would like to end this section with some
comments.
The most striking observation is that our treatment of a very simple
program has become very long, too long indeed for my taste and wishes,
even if I take into account that essentially we did two things: we made a
NOTES ON STRUCTURED PROGRAMMING 39
program and we discussed extensively the kind of considerations leading
to it. It is not so much the length of the latter part that bothers me (writers
fill whole novels with the description of human behaviour); what bothers
me is the length of the texts at the various levels. Therefore we may expect
that notational technique will be one of our main concerns.
But we have also had encouraging experiences. Giving full recognition to
the fact that the poor programmer cannot decide all at once, we succeeded
to a large extent in building up this program one decision at a time, and in
our example quite a lot of programming was already done in its definite
form while major decisions were still left open: irrespective of whether the
final decisions are taken this way or that way, the coding of the earlier levels
remains valid. In view of the requirement of program manageability, this
is very encouraging.
Before we have a program we must have composed it; after we have a program
-if there was any sense in making it-we shall have it executed. In this section
I shall not stress the activities of program composition and of program
execution too much, and I shall try to view the program as a static object.
We want to view it as a highly structured object and our main question is:
what kind of structures do we envisage and why? Our hope is that eventually
we shall arrive at a program structure that is both nice to compose and nice
to execute. Mentally, of course, I am unable to ignore these processes, but
at present I do not want to discuss them; in particular: I do not want to
discuss a design methodology (whether to work "from outside inwards" or
the other way round), nor do I want to discuss implementation consequences
now. Again, in order not to complicate matters too much, I shall restrict
myself to sequential programs.
If I judge a program by itself, my central theme, I think, is that I want
the program written down as I can understand it, I want it written down
as I would like to explain it to someone. However, without further qualifica-
tion these are just motherhood statements, so let me try and see whether I
can be more specific.
NOTES ON STRUCTURED PROGRAMMING 45
Let us consider a very simple computation, in which three distinct actions
can be distinguished to take place in succession, say: input of data, manipula-
tion (i.e. the computation proper) and the output of the results. One way of
representing the program is as a long string of statements:
begin
end
The next form adds some labels for explanatory purposes:
begin
begin of input:
begin of manipulation:
begin of output:
end
suggesting to us, when we read the text, what is going to happen next.
Still better, we write:
begin
input: begin
end;
manipulation: begin
end;
output: begin
end
end
where the labels are considered less as markers of points in the program
text than as names of regions-as indicated by the bracket pairs "begin -
end"-that follow the label, or as names of the three actions in which the
computation has been decomposed. However, if we take this point of view,
46 E. W. DIJKSTRA
the three "labels" are still comments, i.e. explanatory noise for the benefit
of the interested (human) reader, whereas I would like to consider them as
an integral part of the program. I want my program text to reflect somewhere
the fact that the computation has been decomposed into a time-succession
of the three actions, whatever form these might take upon closer inspection.
A way of doing this is to write somewhere the (textual) succession of the
three (abstract) statements
"input; manipulation; output"
on the understanding that the time-succession of these three actions will
indeed be controlled from the above textual succession, whereas the further
refinements of these three actions will be given "somewhere else", perhaps
separately, but certainly without relative ordering.
Well, if closed subroutines had not been invented more than twenty years
ago, this would have been the time to do it! In other words: we are returning
to familiar grounds, to such an extent that many of my readers will even
feel cheated! I don't, because one should never be ashamed of sticking to a
proven method as long as it is satisfactory. But we should get a clear picture
of the benefits we should like to derive from it, if necessary we should adjust
it, and finally we should create a discipline for using it. Let me therefore
review the subroutine concept, because my appreciation for it has changed
in the course of the last year.
I was introduced to the concept of the closed subroutine in connection
with the EDSACt, where the subroutine concept served as the basis for a
library of standard routines. Those were the days when the construction of
hardware was a great adventure and many of the standard routines were
means by which (scarce!) memory and computation time could be traded
for circuitry: as the order code did not comprise a divide instruction, they
had subroutines for division. Yet I do not remember having appreciated
subroutines as a means for "rebuilding" a given machine into a more
suitable one, curiously enough. Nor do I remember from those days sub-
routines as objects to be conceived and constructed by the user to reflect
his analysis: they were more the standard routines to be used by the user.
Eventually I saw them mainly as a device for the reduction of program
length. But the whole program as such remained conceived as acting in a
single homogeneous store, in an unstructured state space; the whole computa-
tion remained conceived as a single sequential process performed by a single
processor. In the following years, in the many programming courses I gave,
I preached the gospel faithfully and I have often explained how the
the opinion that the structure of such a ma ;hine should now be called
somewhat old-fashioned. The recursive procedure, however, forced upon
us the recognition of the difference between its (static) text and its (dynamic)
activation-its "incarnation" as it has been called. The procedure text is
one thing; the set oflocal variables it operates upon this time is quite another
matter.
So far, so good, but now some of its shortcomings (and I don't care,
whether you call them linguistic or conceptual). Local variables are "created"
upon procedure entry, and are "annihilated" upon procedure exit. It is
precisely this automatic control over the life-time of variables pertaining to
a procedure incarnation that allows us to implement the (recursive) procedures
by means of a stack (i.e. a last-in-first-out storage arrangement). The fact
that local variables pertaining to an incarnation only exist during the incar-
nation make it impossible for the procedure to transmit information behind
the scenes from one incarnation to the next. To overcome this the concept
"own" has been introduced, but this is no solution to the problem: what
own variables are really good for becomes very unclear in the case of
recursion and, secondly, it is impossible to write a set of procedures sharing
a number of own variables. (We can simulate this by declaring them in an
o.uter block, embracing the procedure declarations, but then the scope
rules make them too generally accessible: they can then no longer be regarded
as "behind the scenes".) Our conclusion-by no means new and by no
means only mine!-is that the concept "own" as introduced in ALGOL 60
must be regarded as ill-considered, and that we must look for new ways to
control and describe life-time, accessibility and identity of local variables.
But I have still another complaint about the procedure concept, and that
is that it is still primarily regarded as a means for shortening the program
text (although it may be a text of unknown length as in the case of recursion).
The semantics of the procedure call are described in terms of the famous
"copy rule": the procedure call is to be understood as a short-hand, because,
semantically speaking, we should replace it with a copy of the text of the
procedure body (with suitable adjustments of identifiers and substitutions
for parameters) whereupon the thus modified text will be executed by the
same machine as the one executing the main program. It remains (~ repre-
sentation for) a single program text to be executed by a single sequential
machine. And it is precisely this picture of a single machine that does not
satisfy me any longer.
I want to view tht> main program as executed by its own, dedicated
machine, equipped with the adequate instruction repertoire operating on
the adequate variables and sequenced under control of its own instruction
counter, in order that my main program would solve my problem if I had
such a machine. I want to view it that way, because it stresses the fact that
NOTES ON STRUCTURED PROGRAMMING 49
the correctness of the main program can be discussed and established
regardless of the availability of this (probably still virtual) machine: I don't
need to have it, I only need to have its specifications as far as relevant for
the proper execution of the main program under consideration.
For me, the conception of this virtual machine is an embodiment of my
powers of abstraction, not unlike the way in which I can understand a
program written in a so-called higher level language, without knowing how
all kinds of operations (such as multiplication and subscription) are imple-
mented and without knowing such irrelevant details as the number system
used in the hardware that is eventually responsible for the program execution.
In actual practice, of course, this ideal machine will turn out not to exist,
so our next task-structurally similar to the original one-is to program
the simulation of the "upper" machine. In programming this simulation
we have to decide upon data structures to provide for the state space of the
upper machine; furthermore we have to make a bunch of algorithms, each
of them providing an implementation of an instruction assumed for the
order code of the upper machine. Finally, the "lower" machine may have a
set of private variables, introduced for its own benefit and completely outside
the realm and scope of the upper machine. But this bunch of programs is
written for a machine that in all probability will not exist, so our next job
will be to simulate it in terms of programs for a next-lower machine, etc.
until finally we have a program that can be executed by our hardware.
If we succeed in building up our program along the lines just given, we
have arranged our program in layers. Each program layer is to be understood
all by itself, under the assumption of a suitable machine to execute it, while
the function of each layer is to simulate the machine that is assumed to be
available on the level immediately above it.
Why this model? What are the benefits we hope to derive from it? Let me
try to list them.
(I) Our experience as recorded in "A first example of step-wise program
composition" strongly suggests that the arrangement of various layers,
corresponding to different levels of abstraction, is an attractive vehicle for
program composition.
(2) It is not vain to hope that many a program modification can now be
presented as replacement of one (virtual) machine by a compatible one.
(3) We may hope that the model will give us a better grip on the problems
that arise when a program has to be modified while it is in action. If a
machine at a given level is stopped between two of its instructions, all lower
machines are completely passive and can be replaced, while all higher
machines must be regarded as engaged in the middle of an instruction: their
state must be considered as being in transition. In a sequential machine the
so E. W. DIJKSTRA
LONG REP
begin integer k;
line: {integer array sym[O : 99]};
lineprint: {k:= O; while k < lOOdo {PRSYM(sym[k]); k plus l }; NLCR};
lineclear: {k:= O;whilek < lOOdo{sym[k]:= space;kplusl}};
linemark: {sym[x]:= mark}
end
This however leads to an implementation filling out the line with spaces
at the righthand side of the rightmost mark: it is like banging the space
bar until the bell rings when we want to effect the transition to a new para-
graph while writing a letter!
The next version suppresses superfluous PRSYM-commands and even
leaves those elements of the variable of type "line" undefined that do not
need to be defined. With each line a counter ''!" is associated, giving the
number of PRSYM-commands to be given for that line. Clearing a line
now shrinks into setting ''f''to zero!
SHORTREP
begin integer k;
line: {integer f; integer array sym[O : 99]};
lineprint: {k:= O; whilek </do {PRSYM(sym[k]); kplus l}; NLCR};
lineclear: {/: = 0} ;
linemark: {sym[x] : = mark;
if/~ xdo {k:= f; while k < xdo {sym[k]:= space; k plus l};
f:=x+t}}
end
Note added later.
The above program is essentially the program as I have shown it to at
least five different audiences. Now, two months later, while thinking at
leisure about correctness proofs, I suddenly realise that the given algorithm
for "linemark" betrays my past, for it is a piece of lousy coding, compared
with the following alternative:
linemark: {while/~ xdo {sym[f]:= space;fplus l};
sym[x]:= mark}
a version which guarantees that whenever "sym[x]: = mark" is executed,
the relation "x < f" will always be satisfied: it is precisely the function of
the first line to see to this. The reader is invited to try to understand both
versions of linemark and to compare both reasonings. He will then agree
with my judgement that the original version is lousy.
NOTES ON STRUCTURED PROGRAMMING 59
The secon"d version jumped into my mind on account of the following
observation. The conditional clause
"if B do S"
is used in programs in two different ways. On the one hand we have the
applications, in which the execution of the statement S does not invalidate
the truth of B, on the other hand we have the situations in which the execution
of the statement S is guaranteed to invalidate the truth of B. In the latter
case, it is the function of the conditional statement to ensure that after its
execution B will not hold. It is then, essentially, a shortcut for
"while B do S",
which has the property of invalidating the truth of B (provided that it stops),
but the justification of the shortcut requires a separate proof that the repeated
statement will be executed at most once. (In "A first example of step-wise
program composition" we did not bother to introduce this shortcut on
level 2b4(4) where he wrote
"while "ord too small" do "increase ord by one"";
here a conditional clause would have done the job!)
One of the metaphors in which I find myself thinking about the program
structure envisaged regards the program as a necklace, strung from individual
pearls.
We have described the program in terms of levels and each level contained
"refinements" of entities that were assumed available in higher levels. These
refinements were either dynamic refinements (algorithms) or static refine-
ments (data structures) to be understood by an appropriate machine. I use
the term "pearl" for such a machine, refinements included.
Our previous program consists of a necklace of six pearls, in order either
COMPFIRST
CLEARFIRST
I SCANNER
COMPPOS
LINER
LONG REP
or
60 E. W. DIJKSTRA
COMP FIRST
CLEARFIRST
I SCANNER
COMPPOS
LINER
SHORTREP.
LONGREP and SHORTREP are two different pearls, they explain the
same concepts (from the "upper face") into the same concept (of the "lower
face"); only the particular refinements differ: they are as alternative programs
for the same job and the same machine.
Changing a program will be treated as replacing one or more pearls of
the original necklace by one or more other pearls. The pearl is the individual
unit from which programs are composed. Making a program (as a member
of a class of related programs) is now regarded as a two-stage process:
making pearls (more than strictly necessary) and then stringing a fitting
necklace out of (a selection of) them.
The reasons for this two-stage approach are many. In designing a program
we have to consider many, many alternative programs and once our program
is finished, we will have to change it (into one of the alternative ones). As
long as programs are regarded as linear strings of basic symbols of a pro-
gramming language and, accordingly, program modification is treated as
text manipulation on that level, then each program modification must be
understood in the universe of all programs (right or wrong!) that can be
written in that programming language. No wonder that program modification
is then a most risky operation! The basic symbol is too small and meaningless
a unit in terms of which to describe this. The pearl, embodying the independent
design decision or, as the case may be, an isolated aspect of the original
problem statement, is meant to be the natural unit for such modifications.
To rephrase the same argument: with the birth of ALGOL 60, syntax was
discovered as a powerful means for expressing structure in a program text.
(Syntax became so glorified that many workers in the field identified
Computing Science with Syntactic Analysis!) It was slightly overlooked,
however, that by expressing structure via syntax, this structure is only given
very indirectly, i.e. to be derived by means of a parsing algorithm to be
applied to a linear sequence of basic symbols. This hurts if we realise that
many a program modification leaves large portions of the structure un-
affected, so that after painful re-parsing of the modified text the same
structure re-emerges! I have a strong feeling that the adequacy of context-
free methods for the representation of structure has been grossly over-
NOTES ON STRUCTURED PROGRAMMING 61
estimated. (In my immediate environment the following program bug in an
ALGOL 60 program was brought to my attention. A program produced
erroneous output with a completely checking implementation which in
addition to the program text requires a final "progend" after the last "end";
this additional character is refused everywhere else so that a correct "begin -
end" bracketing can be established. It turned out that
(l) somewhere in the program a closing string quote was omitted;
(2) somewhere further down in the program text an opening string quote
was omitted;
(3) the "begin - end" structure of the resulting program was syntactically
correct;
(4) the identifiers declared between the two omissions were only used
between the two omissions, so that even context-dependent checks were
unable to give alarm.
Having already my doubts as to the adequacy of context-free methods for
expressing macroscopic structure, I was delighted when this bug was shown
to me!)
The more I think about pearls, the more I feel that something like them
is the only way out of it, if we recognise our responsibility to take (for a
large program) say a thousand (possible) versions into consideration. You
cannot expect the programmer to make all these thousand versi.Jns from
scratch, independent of each other. The only way I see to produce such a
potential variety is by a combinatorial technique, i.e. by making more pearls
(say 250) than needed for a single necklace (say 200) and stringing a necklace
from a particular selection. I see no other feasible way. The other mechanism
to achieve great variety by combinatorial means is permutation, but this is
denied to us because the final necklace must be a fitting necklace and, given
the pearls, the order in which they have to be strung on the thread to produce
a fitting necklace is pretty well defined. And also: if it is not, the permissible
change of order is pretty irrelevant!
Also, the pearl gives a clear status to an "incomplete" program, consisting
of the top half of a necklace: it can be regarded as a complete program to be
executed by a suitable machine (of which the bottom half of the necklace
gives a feasible implementation). As such, the correctness of the upper half
of the necklace can be established regardless the choice of the bottom half.
Between two successive pearls we can make a "cut" which is a manual for a
machine, provided by the part of the necklace below the cut and used by
the program represented by the part of the necklace above the cut. This
manual serves as an interface between the two parts of the necklace. We.feel
62 E.W. DIJKSTRA
0
01
010
0102
01020
010201
0102010
0102012
Each solution (apart from the first one) is an extension (by one digit)
of an earlier solution and the algorithm is therefore a straightforward
backtracking one.
We are looking for the "good" sequences, we assume a primitive available
for the investigation of whether a trial sequence is good. If it is good, the
trial sequence is printed and extended with a zero to give the next trial
sequence; if the trial sequence is no good, we perform on it the operation
"increase" to get the next trial sequence, i.e. final digits = 2 are removed
and then the last remaining digit is increased by 1. (The operations "extend
with zero" and "increase" guarantee that trial sequences are generated in
alphabetical order, the solutions, being a selection from them, will then be
printed in alphabetical order as well.) The algorithm will start investigating
the following trial sequences, those marked by an asterisk will be rejected as
"no good":
0
• 00
01
010
• 0100
• 0101
0102
01020
• 010200
010201
0102010
• 01020100
• 01020101
• 01020102
• 0102011
0102012
NOTES ON STRUCTURED PROGRAMMING 65
I found the majority of my students inclined to make a program with the
following structure:
"set trial sequence to single zero;
repeat if good then
begin print trial sequence; extend trial sequence with zero end
else
increase trial sequence
until length = IO I"
Although a program along these lines produces the correct output,
objections can-and to my taste: should-be made against it. The first
objections regards the stopping criterion: when a solution of length 100
has been printed, we (knowing the algorithm) can deduce that after that for
the first time the trial sequence will have length = 101 and this is now the
criterion to stop, but this is a rather indirect and tortuous way to establish
the stopping criterion. (How tortuous it is was clearly demonstrated by those
students who did not see that an unnecessary trial sequence was generated
and declared for the trial sequence an array of 100 elements instead of 101.)
The second objection is that the operation "increase trial sequence" never
increases its length: after rejection of a trial sequence a superfluous test
on the length is performed. (When I used this example for student examina-
tion examinations I had not stressed very explicitly in my lectures any
problem solving principles, so my disappointment was not too severe. In a
sense I am glad to have observed these examinations, for it was for me an
incentive to stress problem solving principles as far as I could find, formulate
and teach them.)
The program to which the above objections do not apply treats the empty
sequence as a virtual solution, not to be printed. It has-to the same level of
detail-the following structure:
"set trial sequence empty;
repeat extend trial sequence with zero;
while no good do increase trial sequence;
print trial sequence
until length = 100"
Here length is the length of the solution printed (if any), thus avoiding
the tortuous reasoning for the stopping criterion. Also no superfluous last
trial sequence (never to be investigated) will be generated, thanks to the
fact that we have two loops inside each other, superfluous length testing
no longer occurs. Those for whom efficiency is the main criterion will
probably be most convinced by the last observation. I myself, who attach
66 E. W. DIJKSTRA
This section has not been included because the problem tackled in it is
very exciting. On the contrary, I feel tempted to remark that the problem
is perhaps too trivial to act as a good testing ground for an orderly approach
to the problem of program composition. This section has been included
because it contains a true eye-witness account of what happened in the
classroom. It should be interpreted as a partial answer to the question that
is often posed to me, viz. to what extent I can teach programming style.
(I never used the "Notes on Structured Programming"-mainly addressed
to myself and perhaps to my colleagues-in teaching. The classroom
experiment described in this section took place at the end of a course
entitled "Introduction into the Art of Programming", for which separate
lecture notes-with exercises and all that-were written. As at the moment
of writing the students that followed this course have still to pass their
examination, it is for me still an open question how successful I have been.
They liked the course, I have heard that they described my programs as
"logical poems", so I have the best of hopes.)
Thanks to property (a) which tells us that each row contains precisely one
queen, we can order the queens according to the number of the row they
occupy. Then each configuration of8 queens can be given by the value of the
integer array x [0:7], where
x[i] = the number of the column occupied by the queen in the ith row.
Each solution is then a "8-digit word" (x[O] ... x[7]) and the only sensible
order in which to generate these words that I can think of is the alphabetical
order.
Note. As a consequence we open the way to algorithms in which rows and
columns are treated differently, while the original problem was symmetrical
in rows and columns! To consider asymmetric algorithms is precisely what
the above considerations have taught us!
Returning to the alphabetical order: now we are approaching familiar
ground. If the elements of set A are to be generated in alphabetical order
and they have to be generated by selection from a larger set B, then the
standard technique is to generate the elements of set B in alphabetical order
as well and to produce the elements of the subset in the order in which they
occur "in set B.
First we have to generate all solutions with x[O] = 0 (if any), then those
with x[O] = I (if any) etc.; of the solutions with x[O] fixed, those with
x[l] = 0 (if any) have to be generated first, followed by those with x[l] = I
(if any) etc. In other words: the queen of row 0 is placed in column 0--say
the square in the bottom left corner-and remains there until all elements
of A (and B) with queen 0 in that position have been generated and only
then is she moved one square to the right to the next column. For each
position of queen 0, queen I will walk from left to right in row I-skipping
the squares that are covered by queen 0--for each combined position of the
first two queens, queen 2 walks along row 2 from left to right, skipping all
squares covered by the preceding queens, etc.
But now we have found set B! It is indeed a subset of BI, set B consists of
all configurations with one queen in each of the first N row$, such that .no
two queens can take each other.
The criterion deciding whether an element of B belongs to A as well is
that N = 8.
Having established our choice for set B, we find ourselves faced with the
task of generating its elements in alphabetical order. We could try to do this
via an operator "GENERATE NEXT ELEMENT OF B" with a program
of the form
NOTES ON STRUCTURED PROGRAMMING 77
INITIALISE EMPTY BOARD;
repeat GENERATE NEXT ELEMENT OF B;
if N = 8 then PRINT CONFIGURATION
until B EXHAUSTED .
(Here we have used the fact that the empty board belongs to B, but not to A,
and is not B's only element. We have made no assumptions about the
existence of solutions.)
But for two reasons a program of the above structure is less attractive.
Firstly, we don't have a ready-made criterion to recognise the last element
of B when we meet it and in all probability we have to generalise the operator
"GENERATE NEXT ELEMENT OF B" in such a way that it will produce
the indication "B EXHAUSTED" when it is applied to the last "true"
element of B. Secondly, it is not too obvious how to make the operator
"GENERATE NEXT ELEMENT OF B": the number of queens on the
board may remain constant, it may decrease and it may increase.
So that is not too attractive. What can we do about it? As long as we
regard the sequence of configurations of set B as a single, monotonous
sequence, not subdivided into a succession of subsequences, the corresponding
program structure will be a single loop as in the program just sketched.
If we are looking for an alternative program structure, we must therefore
ask ourselves "How can we group the sequence of configurations from set B
into a succession of subsequences?".
Realising that the sequence ot configurations from set B have to be
generated in alphabetical order and thinking about the main subdivision in
a dictionary-viz. by first letter-the first grouping is obviol!s: by position
of queen 0.
Generating all elements of set B-for the moment we forget about the
printing of thost: configurations that belong to set A as well-then presents
itself as
INITIALISE EMPTY BOARD;
h:= O;
repeat SET QUEEN ON SQUARE[O,h];
GENERATE ALL CONFIGURATIONS WITH QUEEN 0
FIXED;
REMOVE QUEEN FROM SQUARE[O,h];
h:= h +l
until h = lS •
78 E. W. DIJKSTRA
But now the question repeats itself: how do we group all configurations
with queen 0 fixed? We have already given the answer: in order of increasing
column number of queen I, i.e.
hl:= O;
repeat if SQUARE[l, hl] FREE do
begin SET QUEEN ON SQUARE[l,hl];
GENERATE ALL CONFIGURATIONS WITH FIRST
2 QUEENS FIXED;
REMOVE QUEEN FROM SQUARE[l,hl]
end;
hl:=hl+l
until hi = 8
For "GENERATE ALL CONFIGURATIONS WITH FIRST2 QUEENS
FIXED" we could write a similar piece of program and so on; inserting
them inside each other would result in a correct program with eight nested
loops, but they would all be very, very similar. To do so has two disadvan-
tages
(I) it takes a cumbersome amount of writing
(2) it gives a program solving the problem for a chessboard of8*8 squares,
but to solve the same puzzle for a board of, say, 10*10 squares would require
a new, still longer program.
We are looking for a way in which all the loops can be executed under
control of the same program text. Can we make the text of the loops
identical? Can we exploit their identity?
Well, to start with, we observe that the outermost and the innermost loops
are exceptional.
The outermost loop is exceptional in the sense that it does not test whether
square[O,h] is free because we know it is free. But because we know it is
free, there is no harm in inserting the conditional clause
if SQUARE[O,h] FREE do
and this gives the outermost loop the same pattern as the next six loops.
The innermost loop is exceptional in the sense that as soon as 8 queens
have been placed on the board, there is no point in generating all configura-
tions with those queens fixed, because we have a full board. Instead the
configuration should be printed, because we have found an element of set B
that is also an element of set A. We can map the innermost cycle and the
embracing seven upon each other by replacing the line "GENERATE" by
NOTES ON STRUCTURED PROGRAMMING 79
if BOARD FULL then PRINT CONFIGURATION
else GENERATE ALL CONFIGURATIONS EXTENDING THE
CURRENT ONE
For this purpose we introduce a global variable, "n" say, counting the
number of queens currently on the board. The test "BOARD FULL"
becomes "n = 8" and the operations on squares can then have "n" as first
subscript.
By now the only difference between the eight cycles is that each has "its
private h". By the time that we have reached this stage, we can give an
affirmative answer to the question whether we can exploit the identity of
the loops. The sequencing through the eight nested loops can be evoked
with the aid of a recursive procedure, "generate" say, which describes the
cycle once. Using it, the program itself collapses into
INITIALISE EMPTY BOARD; n:= O;
generate
while "generate" is recursively defined as follows:
procedure generate;
begin integer h;
h:= O;
repeat if SQUARE[n,h] FREE do
begin SET QUEEN ON SQUARE[n,h]; n:= n +I;
if n = 8 then PRINT CONFIGURATION
else generate;
n:= n - I; REMOVE QUEEN FROM SQUARE[n,h]
end;
h:= h +I
until h = 8
end
Each activation of "generate" will introduce its private local variable h,
thus catering for h, hi, ... , h8 that we would need when writing eight
nested loops.
Our program-although correct to this level of detail-is not yet complete,
i.e. it has not been refined up to the standard degree of detail that is required
by our programming language. In our next refinement we should decide
upon the conventions according to which we represent the configurations
on the board. We have already decided more or less that we shall use the
integer array x[O :7]
80 E.W. DIJKSTRA
giving in order the column numbers occupied by the queens, and also that
integer n
should be used to represent the number of queens on the board. More
precisely
n = the number of queens on the board
x[i] for 0 ~ i < n = the number of the column occupied by the queen in
the ith row.
The array x and the scalar n are together sufficient to fix any configuration
of the set B and those will be the only ones on the chessboard. As a result
we have no logical need for more variables; yet we shall introduce a few
more, because from a practical point of view we can make good use of them.
The problem is that with only the above material the (frequent) analysis
whether a given square in the next free row is uncovered is rather painful
and time-consuming. It is here that we look for the standard technique as
described in the section "On trading storage space for computation speed"
(see page 42). The role of the stored argument is here played by the
configuration of queens on the board, but this value does not change wildly-
oh no; the only thing we do is to add or remove a queen. And we are
looking for additional tables (whose contents are a function of the current
configuration) such that they will assist us in deciding whether a square is
free, and also such that they can be updated easily when a queen is added
to or removed from a configuration.
How? Well, we might think of a boolean array of 8*8, indicating for each
square whether it is free or not. If we do this for the full board, adding a
queen might imply dealing with 28 squares. Removing a queen, however, is
then a painful process, because it does not follow that all squares no longer
covered by her are indeed free: they might be covered by one or more of
the other queens that remain in the configuration. There is a remedy (again
standard) for this, viz. associating with each square not a boolean variable,
but an integer counter, counting the number of queens covering the square.
Adding a queen then means increasing up to 28 counters by 1, removing a
queen means decreasing them by 1 and a square is free when its associated
counter equals zero. We could cio it that way, but the question is whether
this is not overdoing it: 28 adjustments is indeed quite a heavy overhead on
setting or removing a queen.
Each square in the freedom of which we are interested covers a row (which
is free by definition, so we need not bother about that), covers one of the
8 columns (which must still be empty), covers one of the 15 upward diagonals
(which must still be empty) and one of the 15 downward diagonals (which
must still be empty). This suggests that we should keep track of
NOTES ON STRUCTURED PROGRAMMING 81
(1) the columns that are free
(2) the upward diagonals that are free
(3) the downward diagonals that are free.
As each column or diagonal is covered only once we do not need a counter
for each, a boolean variable is sufficient. The columns are readily identified
by their column number and for the columns we introduce
boolean array col[O :7]
where "col[i]" means that the ith column is still free.
How do we identify the diagonals? Well, along an upward diagonal the
difference between row number and column number is constant; along a
downward diagonal their sum is constant. As a result, difference and sum
respectively are the easiest index by which to distinguish the diagonals and
we introduce therefore
boolean array up[-7:+7], down[O:l4]
to keep track of which diagonals are free.
The question whether square[n,h] is free becomes
col[h] and up[n-h] and down[n+h]
setting and removing a queen both imply the adjustment of three booleans,
one in each array.
In the final program the variable "k" is introduced for general counting
purposes, statements and expressions are labeled (in capital letters). Note
that we have merged two levels of description: what were statements and
functions on the upper level, now appear as explanatory labels.
With the final program we come to the end of the last section. We have
attempted to show the pattern of reasoning by which one could discover
backtracking as a technique, and also the pattern of reasoning by which
one could discover a recursive procedure describing it. The most important
moral of this section is perhaps that all that analysis and synthesis could be
carried out before we had decided how (and how redundantly) a configuration
would be represented inside the machine. It is true that such considerations
only bear fruit when eventually a convenient representation for configura-
tions can be found. Yet the mental isolation of a level of abstraction in which
we allow ourselves not to bother about it seems crucial.
Finally, I would like to thank the reader that has followed me up till here
for his patience.
00
begin integer n, k; integer array x[O :7]; boolean array col[O :7], up[ - 7: + 7], down[O: 14]; N
procedure generate;
begin integer h;
h:= O;
repeat if SQUARE[n,h] FREE: (col[h] and up[n-h] and down[n+h]) do
begin SET QUEEN ON SQUARE[n,h]:
x[n]:= h; col[h]:= false; up[n-h]:= false; down[n+h]:= false; n:= n + 1;
if BOARD FULL: (n = 8) then
begin PRINT CONFIGURATION:
k:= O; repeat print(x[k]); k:= k + 1 until k = 8; newline
tTI
end
~
else generate; t:I
~
n:= n - 1; REMOVE QUEEN FROM SQUARE[n,h]:
down[n+h]:= true; up[n-h]:= true; col[h]:= true
end;
h:= h + 1
until h = 8
end;
INITIALISE EMPTY BOARD:
n:= O;
k:= O; repeat col[k]:= true; k:= k + 1 until k = 8;
k:= O; repeat up[k-7]:= true; down[k]:= true; k:= k + 1untilk=15;
generate
end
II. Notes on Data Structuring •
C. A. R. HOARE
1. INTRODUCTION
he can predict the speed at which a falling object will hit the ground, although
he knows that this will not either cause it to fall, or soften the final impact
when it does.
The last stage in the process of abstraction is very much more sophisticated;
it is the attempt to summarise the most general facts about situations and
objects covered under an abstraction by means of brief but powerful axioms,
and to prove rigorously (on condition that these axioms correctly describe
the real world) that the results obtained by manipulation of representations
can also successfully be applied to the real world. Thus the axioms of
Euclidean geometry correspond sufficiently closely to the real and measurable
world to justify the application of geometrical constructions and theorems
to the practical business of land measurement and surveying the surface of
the earth.
The process of abstraction may thus be summarised in four stages:
(l) Abstraction: the decision to concentrate on properties which are shared
by many objects or situations in the real world, and to ignore the differences
between them.
(2) Representation: the choice of a set of symbols to stand for the abstrac-
tion; this may be used as a means of communication.
(3) Manipulation: the rules for transformation of the symbolic represen-
tations as a means of predicting the effect of similar manipulation of the real
world.
(4) Axiomatisation: the rigorous statement of those properties which have
been abstracted from the real world, and which are shared by manipulations
of the real world and of the symbols which represent it.
same abstractions that underlie the numerical application areas for which
these languages were primarily designed. Of course, these abstract concepts
have been mapped by the implementor of the language onto particular bit-
pattern representations on a particular computer. But in the design of his
algorithm, the programmer is freed from concern about such details, which
for his purpose are largely irrelevant; and his task is thereby considerably
simplified.
Another major advantage of the use of high-level programming languages,
namely machine-independence, is also attributable to the success of their
abstractions. Abstraction can be applied to express the important characteris-
tics not only of differing real-life situations, but also of different computer
representations of them. As a result, each implementor can select a repre-
sentation which ensures maximum efficiency of manipulation on his particular
computer.
A third major advantage of the use of a high-level language is that it
significantly reduces the scope for programming error. In machine code
programming it is all too easy to make stupid mistakes, such as using fixed
point addition on floating point numbers, performing arithmetic operations
on Boolean markers, or allowing modified addresses to go out of range.
When using a high-level language, such errors may be prevented by three
means:
(I) Errors involving the use of the wrong arithmetic instructions are
logically impossible; no program expressed, for example in ALGOL, could
ever cause such erroneous code to be generated.
(2) Errors like performing arithmetic operations on Boolean markers will
be immediately detected by a compiler, and can never cause trouble in an
executable program.
(3) Errors like the use of a subscript out of range can be detected by
runtime checks on the ranges of array subscripts.
Runtime checks, although often necessary, are almost unavoidably more
expensive and less convenient than checks of the previous two kinds; and
high-level languages should be designed to extend the range of programming
errors which logically cannot be made, or if made can be detected by a
compiler. In fact, skilful language design can enable most subscripts to be
checked without loss of runtime efficiency.
The automatic prevention and detection of programming errors may
again be attributed to a successful appeal to abstraction. A high-level pro-
gramming language permits the programmer to declare his intentions about
the types of the values of the variables he uses, and thereby specify the
meanings of the operations valid for values of that type. It is now relatively
NOTES ON DATA STRUCTURING 89
easy for a compiler to check the consistency of the program, and prevent
errors from reaching the execution stage.
1.4. NOTATIONS
In presenting a theory of data structuring, it is necessary to introduce some
convenient notation for expressing the abstractions involved. These notations
are based to a large extent on those e:tiready familiar to mathematicians,
logicians and programmers. They have also been designed for direct expres-
sion of computer algorithms, and to minimise the scope for programming
error in running programs. Finally, the notations are designed to ensure the
existence of efficient data representations on digital computers.
Since the notations are intended to be used (among other things) for the
expression of algorithms, it would be natural to conclude that they constitute
a form of programming language, and that an automatic translator should be
written for converting programs expressed in the language into the machine
code of a computer, thereby eliminating the expensive and error-prone
coding stage in the development of programs.
But this conclusion would be a complete misunderstanding of the reason
for introducing the notations, and could have some very undesirable conse-
quences. The worst of them is that it could lead to the rejection of the main
benefits of the programming methodology expounded in this monograph, on
the grounds that no compiler is available for the language, nor likely to be
widely accepted if it were.
But there are sound reasons why these notations must not be regarded as a
programming language. Some of the operations (e.g., concatenation of
sequences), although very helpful in the design of abstract programs and the
description of their properties, are grotesquely inefficient when applied to
large data objects in a computer; and it is an essential part of the program
design process to eliminate such operations in the transition between an
abstract and a concrete program. This elimination will sometimes involve
quite radical changes to both algorithm and representation, and could not in
general be made by an automatic translator. If such expensive operators were
part of a language intended for automatic compilation, it is probable that
many programmers would fail to realise their obligation to eliminate them
before approaching the computer; and even if they wanted to, they would
have little feeling for what alternative representations and operations would
be more economic. In taking such vital decisions, it is actually helpful if a
programming language is rather close to the eventual machine, in the sense
that the efficiency of the machine code is directly predictable from the form
and length of the corresponding source language code.
There is a more subtle danger which would be involved in the automatic
implementation of the notations: that the good programmer would soon
90 C. A. R. HOARE
learn that some of them are significantly less efficient than others, and he will
avoid their use even in his abstract programs; and this will result in a form
of mental block which might have serious consequences on his inventive
capacity. Equally serious, the implementation of a fixed set of notations
might well inhibit the user from introducing his own notations and concepts
as required by his understanding of a particular problem.
Thus there is a most important distinction to be drawn between an
algorithmic language intended to assist in the definition, design, development
and documentation of a program, and the programming language in which
the program is eventually conveyed to a computer. In this monograph we
shall be concerned solely with the former kind of language. All example
algorithms will be expressed in this language, and the actual coding of
these programs is left as an exercise to the reader, who may choose for this
purpose any language familiar to him, ALGOL, FORTRAN, COBOL, PL/I,
assembly language, or any available combination of them. It is essential to a
realisation of the relative merits of various representations of data to realise
what their implications on the resulting code will be.
In spite of this vigorous disclaimer that I am not embarking on the design
of yet another programming language, I must admit the advantages that
can follow if the programming language used for coding an algorithm is
actually a subset of the language in which it has been designed. I must also
confess that there exists a large subset of the proposed algorithmic language
which can be implemented with extremely high efficiency, both at compile
time and at run time, on standard computers of the present day; and the
challenge of designing computers which can efficiently implement even larger
subsets may be taken up in the future. But the non-availability of such a
subset implementation in no way invalidates the benefits of using the full
set of notations as an abstract programming tool.
1.5. SUMMARY
It turns out that whether you answer yes or no, you can be immediately
proved wrong.
Russell's solution to the paradox is to associate with each logical or
mathematical variable a type, which defines whether it is an individual, a
set, a set of sets, etc. Then he states that any proposition of the form "xis a
member of y" is grammatically meaningful only if x is a variable of type
individual and y a variable of type set, or if x is of type set and y is of type set
of sets, and so on. Any proposition that violates this rule is regarded as
meaningless-the question of its truth or falsity just does not arise, it is just a
jumble of letters. Thus any proposition involving sets that are or are not
members of themselves can simply be ruled out.
Russell's theory of types leads to certain complexities in the foundation
of mathematics, which are not relevant to describe here. Its interesting
features for our purposes are that types are used to prevent certain erroneous
expressions from being used in logical and mathematical formulae; and that a
check against violation of type constraints can be made merely by scanning
the text, without any knowledge of the value which a particular symbol
might happen to stand for.
(3) In a high-level programming language the concept of a type is of
central· importance. Again, each variable, constant and expression has a
unique type associated with it. In ALGOL 60 the association of a type with a
variable is made by its declaration; in FORTRAN it is deduced from the
initial letter of the variable. In the implementation of the language, the type
information determines the representation of the values of the variable, and
the amount of computer storage which must be allocated to it. Type informa-
tion also determines the manner in which arithmetic operators are to be
interpreted; and enables a compiler to reject as meaningless those programs
which invoke inappropriate operations.
Thus there is a high degree of commonality in the use of the concept of
type by mathematicians, logicians and programmers. The salient characteris-
tics of the concept of type may be summarised:
(1) A type determines the class of values which may be assumed by a
variable or expression.
(2) Every value belongs to one and only one type.
(3) The type of a value denoted by any constant, variable, or expression
may be deduced from its form or context, without any knowledge of its
value as computed at run time.
(4) Each operator expects operands of some fixed type, and delivers a
result of some fixed type (usually the same). Where the same symbol is applied
to several different types (e.g. + for addition of integers as well as reals),
NOTES ON DATA STRUCTURING 93
this symbol may be regarded as ambiguous, denoting several different actual
operators. The resolution of such systematic ambiguity can always be made
at compile time.
(5) The properties of the values of a type and of the primitive operations
defined over them are specified by means of a set of axioms.
(6) Type information is used in a high-level language both to prevent or
detect meaningless constructions in a program, and to determine the method
of representing and manipulating data on a computer.
(7) The types in which we are interested are those already familiar to
mathematicians; namely, Cartesian Products, Discriminated Unions, Sets,
Functions, Sequences, and Recursive Structures.
primitive types of ALGOL 60 are integer, real, and Boolean, and these will
be ass:1med available.
The most important practical aspect of data is the manner in which that
data can be manipulated, and the range of basic operators available for this
purpose. We therefore associate with each type a set of basic operators which
are intended to be useful in the design of programs, and yet which have at
least one reasonably efficient implementation on a computer. Of course the
selection of basic operators is to some extent arbitrary, and could have been
either larger or smaller. The guiding principle has been to choose a set large
enough to ensure that any additional operation required by the programmer
can be defined in terms of the basic set, and be efficiently implemented in
this way also; so an operator is regarded as basic if its method of efficient
implementation depends heavily on the chosen method of data represen-
tation.
The most important and general operations defined for data of any type
are assignment and test of equality. Assignment involves conceptually a
complete copy of a data value from one place to another in the store of the
computer; and test of equality involves a complete scan of two values
(usually stored at different places) to test their identity. These rules are those
that apply to primitive data types and there is no reason to depart from
them in the case of structured types. If the value of a structured type is very
large, these operations may take a considerable amount of time; this can
sometimes be reduced by an appropriate choice of representation; alter-
natively, such operations can be avoided or removed in the process of
transforming an abstract program to a concrete one.
Another general class of operators consists in the transfer functions, which
map values of one type into another. Of particular importance are the
constructors, which permit the value of a structured type to be defined in
terms of the values of the constituent types from which it is built. The
converse transfer functions are known as selectors; they permit access to
the component values of a structured type. In many cases, we use the name
of a defined type as the name of the standard constructor or transfer function
which ranges over the type.
Certain data types are conveniently regarded as ordered; and comparison
operators are available to test the values of such types. But for many types,
such an ordering would have no meaningful interpretation; and such types
are best regarded from an abstract point of view as unordered. This will
sometimes be of advantage in giving greater freedom in the choice of repre-
sentation and sequencing strategies at a later state in the concrete design.
NOTES ON DATA STRUCTURING 95
In the case of a large data structure, the standard method of operating
efficiently on it is not by assigning a wholly new value to it, but rather by
selectively updating some relatively small part of it. The usual notation for
this is to write on the left of an assignment an expression (variable) which
uses selectors to denote the place where the structure is to be changed.
However, we also introduce special assignment operators, always beginning
with colon, to denote other more general updating operations such as adding
a member to a set, or appending an item to a sequence. For both kinds of
selective updating, it must be remembered that, from a conceptual or abstract
point of view, the entire value of the variable has been changed by updating
the least part of it.
2.3. REPRESENTATIONS
It is fundamental to the design of a program to decide how far to store
computed results as data for subsequent use, and how far to compute them
as required. It is equally fundamental to decide how stored data should be
represented in the computer. In many simple and relatively small cases there
is an obvious standard way of representing data, which ensures that not too
much storage is used, and not too much time expended on carrying out the
basic operations. But if the volume of data (or the amount of processing)
is large, it is often profitable (and sometimes necessary) to choose some
non-standard representation, selected in accordance with the characteristics
of the storage media used (drums, discs, or tapes), and also taking into
account the relative frequencies of the various operations which will be
performed upon it. Decisions on the details of representation must usually
precede and influence the design of the code to manipulate the data, often
at a time when the nature of the data and the processing required are relatively
unknown. Thus it is quite common to make serious errors of judgement in
the design of data representation, which do not come to light until shortly
before, or even after, the program has been put into operation. By this time
the error is extremely difficult to rectify. However, the use of abstraction
in data structuring may help to postpone some of the decisions on data
representation until more is known about the behaviour of the program and
the characteristics of the data, and thus make such errors less frequent and
easier to rectify.
An important decision to be taken is on the degree and manner in which
data should be compressed in storage to save space; and also to save time on
input/output, on copying operations, and on comparisons, usually at the
expense of increasing the time and amount of code required to perform all
other operations. Representations requiring less storage than the standard
are usually known as packed; there are several degrees of packing, from
96 C. A. R. HOARE
All structured data must in the last analysis be built up from unstructured
components, belonging to a primitive or unstructured type. Some of these
unstructured types (for example, reals and integers) may be taken as given
by a programming language or the hardware of the computer. Although
these primitive types are theoretically adequate for all purposes, there are
strong practical reasons for encouraging a programmer to define his own
unstructured types, both to clarify his intentions about the po~ntial range of
NOTES ON DATA STRUCTURING 97
values of a variable, and the interpretation of each such value; and to permit
subsequent design of an efficient representation.
In particular, in many computer programs an integer is used to stand not
for a numeric quantity, but for a particular choice from a relatively small
number of alternatives. In such cases, the annotation of the program usually
lists all the possible alternative values, and gives the intended interpretation
of each of them. It is possible to regard such a quantity as belonging to a
separate type, quite distinct from the integer type, and quite distinct from
any other similar set of markers which have a different interpretation. Such
a type is said to be an enumeration, and we suggest a standard notation for
declaring the name of the type and associating a name with each of its
alternative values:
type suit = (club, diamond, heart, spade);
ordered type rank = (two, three, four, five, six, seven, eight, nine, ten, Jack,
Queen, King, Ace);
type primary colour = (red, yellow, blue);
ordered type day of week = (Monday, Tuesday, Wednesday, Thursday,
Friday, Saturday, Sunday);
type day of month = l .. 31;
ordered type month = (Jan, Feb, March, April, May, June, July, Aug, Sept,
Oct, Nov, Dec);
type year= 1900 .. 1969;
type Boolean = (false, true);
ordered type floor = (basement, ground, mezzanine, first, second);
type coordinate = 0 .. I023 ;
Our first two examples are drawn from the realm of playing cards. The first
declaration states that club, diamond, heart, and spade are suits; in other
words, that any variable or expression of type suit can only denote one of
these four values; and that the identifiers "club" "heart" "diamond" and
"spade" act as constants of this type. Similarly, the definition of the type
rank displays the thirteen constants denoting the thirteen possible values of
the type. In this case it is natural to regard the type as ordered. The next
examples declare the names of the primary colours and of the days of the
week. In considering the days of the month, it is inconvenient to write out
the thirty-one possible values in full. We therefore introduce the convention
that a . . b stands for the finite range of values between a and b inclusive.
This is known as a subrange of the type to which a and b belong, in this case
98 C. A. R. HOARE
3. I. MANIPULATION
accessible outside the loop. In addition, the value of the counting variable
is not allowed to be changed inside the body of the loop, since this would
frustrate the whole intention of declaring the variable by means of the for
construction.
In the case of an ordered type, it is natural to assume that the counting
variable sequences through the values of the type in the defintd order,
T.min, succ(T.min), ... , T.max. But if the type is an unordered one, it is
assumed that the sequence of the scan does not matter at the current level of
abstraction, and will be defined at some later stage in the development of a
concrete program.
(7) For subrange types, particularly integer subranges, it is sometimes
required to perform operations which are defined for the original larger type.
In principle, it is simple to accomplish this by first converting the subrange
value to the corresponding value of the larger type, and then performing the
operation, and finally converting back again if necessary. This requires a
type transfer function; and for this purpose it is convenient to use the name
of the destination type, for example:
xdistance: = integer(x) - integer(y);
z; = coordinate{integer(z) + xdistance);
where xdistance is an integer variable. Of course, this is an excessively
cumbersome notation, and one would certainly wish to adopt the convention
of omitting the conversions, where the need for their re-insertion can be
established from the context:
xdistance: = x - y;
z: = z + xdistance.
Exercise
Given m: month and y: year, write a case discrimination expression giving
the number of days in month m.
3.2. REPRESENTATION
3.3. EXAMPLE
The character set of a computer peripheral is defined by enumeration:
type character = ( .... ) ;
The set includes the subranges
type digit = nought. . nine;
type alphabet= A . . Z;
as well as individual symbols, point, equals, subten, colon, newline, space,
as well as a number of other single-character operators and punctuation
marks.
There is a variable
buffer: character
which contains the most recently input character from the peripheral. A
102 C. A. R. HOARE
new value can be input to buffer from the input tape by the procedure "read
next character".
In a certain representation of ALGOL 60, basic words are not singled out
by underlining, and therefore look like identifiers. Consequently, if they are
followed or preceded by an identifier or a number, they must be separated
from it by one or more spaces or newline symbols.
In the first pass of an ALGOL translator it is desired to read in the
individual characters, and assemble them into meaningful symbols of the
language; thus, an identifier, a basic symbol, a number, and the ": ="
becomes sign, each count as a single symbol, as do all the other punctuation
marks. Space and newline, having performed their function of separating
symbols, must be ignored. We assume that each meaningful symbol will be
scanned by a routine designed for the purpose, and that each such routine
will leave in the buffer the first input character which is not part of the
symbol.
As an example of the analysis of the symbols of a program, input of the
text
/:betal: = beta x 12;
should be analysed into the following symbols:
I
beta I
.-
·-
beta
x
12
Defined enumerations and subranges, like primitive data types, are in principle
unstructured. Of course, any particular representation of these types will be
structured, for example, as a collection of consecutive binary digits; but
from the abstract point of view, this structuring is essentially irrelevant. No
operators are provided for accessing the individual bits, or for building up a
value from them. In fact, it is essential to the successful use of an abstraction
that such a possibility should be ignored; since it is only thus that detailed
decisions can be postponed, and data representations can be decided in the
light of the characteristics of the computer, as well as the manner in which
the data is to be manipulated.
We now turn to deal with data types for which the structure is meaningful
to the programmer, at least at some stage in the development of his program.
The basis of our approach is that, as in the case of enumerations, the pro-
grammer should be able by declaration to introduce new data types; but for
structured data, the definition of a new type will refer to other primitive or
previously defined types, namely the types of the components of the structure.
Thus the declaration of a new type will be somewhat similar to the declara-
tion of a new function in a language such as ALGOL and FORTRAN. A
function declaration defines the new function in terms of existing or pre-
viously declared functions and operations. Just as a declared function can be
invoked on many occasions from within statements of the program or other
function declarations, so the new type can be .. invoked" many times from
within other declarations of the program; these may be either declarations
of variables specified to range over the newly declared type, or they may be
declarations of yet another new type.
We will deal first with elementary data structures, Cartesian products and
unions. These elementary structures are almost as simple and familiar to
mathematicians and logicians as the natural numbers. Furthermore, from
104 C. A. R. HOARE
4.1 MANIPULATION
Apart from assignment and test of equality, which are common to all types,
the main operations defined for a product type are just those of constructing
a value in terms of component values, and of selecting the components.
When constructing a value of a Cartesian product type, it is in principle
necessacy to quote the name of the type as a transfer function. However,
it is often more convenient to follow the traditional mathematical practice,
and leave the transfer function implicit in cases where no confusion would
arise. This is in any case necessary when a type is not even given an explicit
name. For example, one may write (heart, Jack) instead of cardface (heart,
Jack).
For selection of a component, a dot-notation has been used, e.g.,
n. imagpart. This is more convenient than the normal functional notation
imagpart (n), since it avoids unnecessarily deep nesting of brackets.
Another most important operation is the selective updating of the com-
ponents of a variable. This may be denoted by placing the component name
on the left of an assignment
u. imagpart: = 0;
4.2. REPRESENTATION
day§ day m y
m 2 day m y
y 8 6 2
I6 I2 I
0
1 15 14 1°1
7 bits
6 6 12 bits
5.1. MANIPULATION
Any value of a discriminated union carries with it a tag field indicating
which of the particular constituent types it originated from; on assignment
this is copied, and on a test of equality, the tag fields must be the same if the
values are-to be equal.
112 C. A.R. HOARE
On constructing a value of a discriminated union type, it is necessary to
name the alternative type from which the value originated:
patience card (red (spade, Jack)).
This will automatically cause the value "red" to be assigned to the tag fielo
of the result.
A particular car may be denoted by
car (Ford, "RUR157D",
local (me, date (I, Sept, 1968))).
In order to access and operate on the information encoded as a dis-
criminated union, it is necessary to convert it back to its original type.
This may be accomplished by the convention of using the label of this type
as if it were a selector, e.g.:
cardl.wild is of type (joker I, joker 2)
car I . foreign is of type (origin: country)
figl. tri is of type T
If the constituent type is a Cartesian product, its selectors may be validly
applied to the resulting value, using the convention that the . operator
associates to the left.
cardl .normal.r
earl .local. owner
fig I . circ. diameter
If the programmer attempts to convert a discriminated union value
back to a type from which it did not originate, this is a serious programming
error, which could lead to meaningless results. This error can be detected
only by a runtime check, which tests the tag field whenever such a conversion
is explicitly or implicitly invoked. Such a check is timeconsuming and when
it fails, highly inconvenient. We therefore seek a notational technique which
will guarantee that this error can never occur in a running program; and
the guarantee is given by merely inspecting the text, without any knowledge
of the runtime values being processed. Such a guarantee could be given by an
automatic compiler, if available.
The proposed notational technique is a mixture between the with con-
struction for Cartesian products and the case construction for discrimination.
Suppose that a value sv of union type is to be processed in one of several
NOTES ON DATA STRUCTURING 113
ways in accordance with which of the alternative types it came from. Then
one may write
with sv do {a1:S1
a2:S2,
an :Sn};
where Si is the statement to be selected for execution whenever the value of
the tag field of sv is ai. Within S 1 it is guaranteed safe to assume that the
value came from the corresponding alternative type, provided that the value
of sv remains unchanged. Consequently it is safe to use the component
selectors which are defined for that alternative type by themselves to refer
to the components of sv, just as in the case of a simple with statement
described previously for a Cartesian product.
If it is desired to regard a union type as ordered, the most natural ordering
is that defined by taking all values corresponding to earlier alternatives in
the list before any of the values of the later alternatives.
Exercise
Write a function that will compute the area of a figure as defined above.
5.2. REPRESENTATION
since recovery of the original value requires only subtraction rather than
division.
In general the values of the different alternative types occupy different
amounts of storage, so the shorter values have to be "padded out" to
equalise the lengths, thus observing the convenient rule that elementary
data types occupy a fixed amount of storage. In later chapters it will be seen
that this padding can often be omitted when the value is a component of
some larger structure.
The array is for many programmers the most familiar data structure, and in
some programming languages it is the only structure explicitly available.
From the abstract point of view, an array may be regarded as a mapping
between a domain of one type (the subscript range) and a range of some
possibly different type (the type of the array, or more accurately, the type of
its elements).
The type of a mapping is normally specified by a mathematician using an
arrow:
M:D-+ R;
where D is the domain type and R is the range type. An alternative notation
which will be more familiar to programmers is:
M :array D of R.
This notation is more expressive of the manner in which the data is repre-
sented, whereas the mathematical notation emphasises the abstract character
of the structure, independent of its representation.
When a particular value M of a mapping type is applied to a value x of the
domain type, it specifies some unique element of the range type, which is
known as M of x, and is written using either round or square brackets
M(x) or M[x].
Another name for a mapping is a function: the term "mapping" is used to
differentiate the data structure from a piece of program which actually
computes a value in its range from an argument in its domain. The essence of
the difference is that a mapping M is specified not by giving a computation
method but by explicitly listing the value of M(x) for each possible value x
in its domain. Thus an array can be used only for functions defined at a
finite set of points, whereas the domain of a computed function may be
infinite.
An example of a finite mapping is a monthtable, which specifies for each
month of the year the number of days it has:
type monthtable =array month of 28 .. 31.
The domain is the month type and the range type consists of the integers
between 28 and 31 inclusive. A typical value of this type may be simply
specified by listing the values of M(x) as x ranges over its domain. Thus
if M: monthtable is specified as
monthtable (Jan:31, Feb:28, March:31, April:30,
May:31, June:30, July:31, Aug:31,
Sept:30, Oct:31, Nov:30, Dec:31)
then M[Jan] = 31, M[Feb] = 28, and so on.
116 C. A. R. HOARE
A A (0) A A(2)
A [I)
A (2)
A [3) A(5)
A [4)
A (5)
t
padding
A [6)
A [7)
(al (b) (cl
FIG. 3. Representation of A: array 0 .. 7 of T
When the domain of a finite mapping is itself a data structure, for example,
a Cartesian product, it is usual to represent this domain in the minimal
representation, so as to avoid allocation of unused storage space. For
example, the display page has a domain which is the Cartesian product of
the integer ranges l to 40 and l to 27. In the minimal representation, this
gives a range of integers between 0 and 40 x 27 -1 = 1079. Consequently
1080 consecutive words are allocated to hold values of elements of the array.
In order to access any element in a given row and character position, it is
necessary first to construct a minimal representation for the subscript, in
the manner described in Section 4.2.
120 C. A. R. HOARE
A
Standard
A [o,o]
A [0,1]
A
Tree
§A[O,O] A[0,1]
A[0,2]
§A[l,O]
A [0,2]
A [ 1,0]
A [ 1,1]
A [ 1,2] A[I, I]
A[ 1,2]
}row2
A [3,0]
A [3,1]
a•M§} A[3,1]
A[3,2]
row2
A [3,2]
(a) ( b)
FIG. 4. Representation of two-dimensional arrays
7. THE POWERSET
The powerset of a given set is defined as the set of all subsets of that set;
and a powerset type is a type whose values are sets of values selected from
some other type known as the base of the powerset. For example, the primary
colours have been defined by enumeration as red, yellow and blue. The
other main colours are made up as a mixture of two or three of these colours:
orange is a mixture of red and yellow; brown is a mixture of all three primary
colours. Thus each main colour (including the primary colours) can be
specified as that subset of the primary colours out of which it can be mixed.
For example, orange may be regarded as the set with just two members,
red and yellow. Using the traditional notation for sets defined by enumeration,
this may be written: {red, yellow}. The pure colour red may be regarded as
the set whose only member is the primary colour red, i.e. {red}. In this way it
is possible to represent the seven main colours, red, orange, yellow, green,
blue, purple and brown. When no primary colour is present (i.e. the null or
empty set) this may be regarded as denoting the absence of colour, i.e.
perhaps white. The type whose values range over the colours may be declared
as the power set of the type primary colour:
type colour = powerset primary colour.
A second example is provided by considering a data structure required to
represent the status of the request buttons in a lift. A simple variable of type
NOTES ON DATA STRUCTURING 123
floor (see Section 3) is capable of indicating one particular stop of a lift.
But if we wish to record the status of the whole panel of buttons inside a
lift, it would be necessary to represent this as a subset of all possible floors
in the building, namely, the subset consisting of those floors for which a
request button has been depressed. Thus the type liftcall may be defined
as the powerset of the floor type:
type liftcall = powerset floor.
A third example is provided by a hand of cards in some card game, fo,·
example, poker or bridge. A hand is a subset of playing cards, without
repetitions, and is therefore conveniently represented by a value from the
powerset type:
type hand = powerset cardface;
This type covers all hands of up to fifty-two cards, even though for a
particular game there may be standard size of a hand, or a limit less than
fifty-two.
A final example expresses the status of a computer peripheral device, for
example, a paper tape reader. There are a number of exception conditions
which can arise on attempted input of a character:
(1) Device switched to "manual" by operator.
(2) No tape loaded.
(3) Parity error on last character read.
(4) Skew detected on last character read.
These conditions can be defined as an enumeration
type exception = (manual, unloaded, parity, skew);
and since several of these conditions can be detected simultaneously, the
status of the reader can be specified as a value of a powerset type:
type statusword = powerset exception.
The cardinality of the powerset type is two raised to the power of the
cardinality of the base type, i.e.
cardinality (powerset D) = 2 cardinality (D)
This may be proved by considering the number of decisions which have
to be made to specify completely a value of the type. For each value of the
base type there are two alternatives, either it is in the set or it is not. This
decision may be made independently cardinality (D) times.
7.1. MANIPULATION
The basic construction operation on sets is the one that takes a number of
values from the domain type, and converts them into a set containing just
124 C. A. R. HOARE
those values as members. As in the case of the Cartesian Product, the type
name is used as the transfer function, but for sets, the number of arguments
is variable from zero upwards. For example:
primary colour (red, yellow) i.e. orange
liftcall (ground) i.e. only a single button has been
pressed
statusword ( ) i.e. no exception condition.
The last two examples illustrate the concept of a unit set (which must be
clearly distinguished from its only member) and the null or empty set, which
contains no member at all. If the type name is omitted in this construction,
curly brackets should be used instead of round ones in the normal way.
The converse of the null set is the universal set, which contains all values
from the base type. This may be denoted
T.all.
However, this universal set exists as a storable data value only when the base
type is finite.
The basic operations on sets are very familiar to mathematicians and
logicians.
(1) Test of membership: If xis in the sets, the Boolean expression "x ins"
yields the value true, otherwise the value false.
(2) Equality: two sets are equal if and only if they have the same members.
(3) Intersection: sl A s2 contains just those values which are in both sl
and s2.
(4) Unions: sl v s2 contain just those values which are either in sl or s2,
or both.
(5) Relative complement: s 1 - s2 contains just those members of sl which
are not in s2.
(6) Test of inclusion: sl c:: s2 yields the value true whenever all members
of sl are also members of s2, and false otherwise.
(7) The size of a set tells how many members it has.
If the domain type of a set has certain operators defined upon it, it is often
useful to construct corresponding operations on sets. In particular, if the
domain type of a set is ordered, the following operators apply:
(8) min (s) the smallest member of s; undefined ifs is empty.
(9) s down n is a set containing just those values whose nth successors are
ins.
(10) s up n is a set containing just those values whose nth predecessors
are ins.
NOTES ON DATA STRUCTURING 125
(11) Range (a, b) is the set containing a, succ(a), ... , b if a ::::;; b, and which
is empty otherwise.
The most useful selective updating operations on sets are:
x: v y; join the set y to x
x: v T(a) add the member a to x
x:" y; exclude from x all members which are not also members
of y
x:-y exclude from x all members which are also members
of y
x:down n subtract n from every member of x and exclude members
for which this is not possible
x:up n add n to every member of x, and exclude members for
which this is not possible
It is also sometimes useful to select some member from x and simultaneously
remove it from x. This operation can be expressed by the notation:
a from x.
If the domain type of x is ordered, it is natural that the selected member
should be the minimum member of x; otherwise the selection should be
regarded as arbitrary.
It is often useful to define the value of a set by giving some condition B
which is satisfied by just those values of the domain type which are intended
to be members of the set. This may be denoted:
{i:D I B}
where i is a variable of type D regarded as local to B,
and B is a Boolean expression usually containing and depending on i.
In order for this expression to denote a value of the powerset type it is
essential that the cardinality of D be finite, and that B is defined over all
values of the type.
Finally, it is frequently required to perform some operation on each
member of some set, that is to execute a loop with a counting variable which
takes on successively all values in the set. A suitable notation for expressing
this is:
for x ins do ...
If the base type of s is an ordered type, it seems reasonable to postulate that
the elements will be taken in the natural order, starting with the lowest.
For an unordered base type, the programmer does not care in which order
the members are taken, and he leaves open the option to choose an order
that contributes best to efficiency.
126 C. A. R. HOARE
7.2 REPRESENTATION
In choosing a computer representation for powersets, it is desirable to
ensure that all the basic operations can be executed simply by single machine
code instructions; and further, that the amount of store occupied is
minimised. For most data structure storage methods, there is a fundamental
conflict between these two objectives, and consequently a choice between
representation methods must be made by the programmer; but in the case
of powersets the two objectives can be fully reconciled, provided that the
base type is not too large.
The recommended method of representation is to allocate as many bits
in the store as there are potential members in the set. Thus to each value
of the base type there is a single bit which takes the value one if it is in fact a
member, or zero if it is not. For example, each value of type colour can be
represented in three bits; the most significant corresponding to the primary
colour red, and the least significant corresponding to blue. Thus the orange
colour is represented as 110 and red as 100. Each set of size n is represented
as a bitpattern with exactly n ones in the appropriate positions. The null set
is accordingly represented as an all-zero bitpattern.
Another example is afforded by the "hand" type, which requires fifty-two
bits for its representation, one corresponding to each value of type cardface.
In this case, it is advisable to use the minimal representation of the base
type, to avoid unused gaps in the bitpattern representation.
Since the number of values of a powerset type is always an exact power of
two, for powersets of small base there can be no more economical method
of utilising storage on a binary computer than that of the bitpattern repre-
sentation. It remains to show that the operations defined over the powerset
type can be executed with high efficiency.
(1) The unitset of x may be obtained by loading a single 1 into the signbit
position, and shifting it right x places. On computers on which shifting is
slow, the same effect may be obtained by table lookup. The construction of a
set out of components may be achieved by taking the logical union of all the
corresponding unit sets.
(2) A membership test x in s may be made by shifting s up x places and
looking at the most significant bit: 1 stands for true and 0 for false.
(3) Logical intersection, union, and complementation are often available
as single instructions on binary computers.
(4) The size of a set can sometimes be discovered by a builtin machine
code instruction for counting the bits in a word. Otherwise the size can be
determined by repeated standardisation, masking off the next-to-sign bit on
NOTES ON DATA STRUCTURING 127
each occasion. A third method is to split the bitpattern into small parts, and
use table lookup on each part, adding together the results.
(5) The up and down operations can obviously be accomplished by right
or left shifts.
(6) The min of a set can be efficiently discovered by a standardise instruc-
tion, which automatically counts the number of shifts required to move the
first one-bit into the position next to the sign.
(7) The for statement may also be efficiently constructed using standardi-
sation, masking off each one-bit as it is reached.
(8) The range operation can be accomplished by two shifts, the first of
which regenerates the sign bit.
Thus when the cardinality of the domain type is not greater than the
number of biti. in the largest computer word to which logical and shift
operations can be applied, all these operations can be carried out with great
efficiency. If significantly more than one such word is involved, it will usually
pay to use selective updating operations rather than the normal result-
producing operators. Furthermore, operations such as size and min can
become rather inefficient, and it will often pay to store these values re-
dundantly together with the set, and keep them up to date whenever the value
of the set is updated, rather than recomputing them whenever they are
required.
When it is known that the cardinality of the base type is very large (perhaps
even infinite) compared with the size of the typical set, the bitpattern repre-
sentation altogether loses its attraction, since it no longer pays to store and
operate upon large areas of zeroes. The treatment of such sparse sets is
postponed to Section IO.
7.3. EXAMPLE
n, next:(w, b:integer);
where w indicates the wordnumber and b indicates the bitnumber.
It is now as well to check the efficiency of this representation by recoding
the innermost loop first.
for n: = next step next until N do sieve: - {n};
is recoded as :
n: =next;
whilen.w ~ Wdo
beginsieve[n.w]:- {n.b};
n.b: = n.b + next.b;
NOTES ON DATA STRUCTURING 129
n.w: = n.w + next.w;
if n. b ;;i:: wordlength then begin n. w: = n. w +I;
n.b: = n.b - wordlength
end
end
Since this appears acceptably efficient we will code the other operations of
the outer loop, starting with the most difficult:
next: = min (sieve) ;
Here we do not wish to start our search for the minimum at the beginning
of the sieve set each time, since towards the end of the process this would
involve scanning many empty words. We therefore take advantage of the
fact that the new value of next must be larger than the old value.
The search consists of two parts, first finding a nonempty word, and then
its first bit. But if the search for a word reaches the end of the array, the
whole program is completed
while sieve [next. w] = { }do {next. w: = next. w +I;
if next. w > W then exit primefinder};
next.b: = min (sieve [next. w]);
The remaining operations are trivial. Since the outer loop is terminated
by an exit, there is no need to test a separate while condition; and the
statement
primes: v {next};
can be coded as
primes [next.w]:v {next.b}.
The whole program including initialisation is as follows:
primes, sieve: array 0 .. W of powerset 0 .. wordlength - I ;
begin primefinder;
n, next:(w, b:integer);
for t:O .. W do begin primes [t]: = { };
sieve [t]: = range (0 .. wordlength -1)
end;
sieve [O]: - {O, I};
next. w: = O;
while true do
begin while sieve [next. w] = { } do
130 C. A. R. HOARE
8. THE SEQUENCE
The previous chapters have dealt with the topic of elementary data structures,
which are of great importance in practical programming, and present very
NOTES ON DATA STRUCTURING 131
little problem for representation and manipulation on modern digital com-
puters. Furthermore, they provide the essential basis on which all other more
advanced structures are built.
The most important distinction between elementary structured types and
types of advanced structure is that in the former case the cardinality of the
type is strictly finite, provided that the cardinality of the constituent types is.
The distinction between a finite and an infinite set is one of profound mathe-
matical significance, and it has many consequences relating to methods of
representation and manipulation.
(I) Since the number of potential values of the type may be infinite, the
amount of storage allocated to hold a value of an advanced structure is not
determinable from the declaration itself. It is normally only determined
when the program is actually running, and in many cases, varies during the
execution of the program. In the case of an elementary structure, the number
of different potential values is finite, and the maximum amount of storage
required to hold any value is fixed and determinable from the form of the
declaration.
(2) When the size of a structured value is fairly Jarge, it is more efficient
to update individual components of the structure separately, rather than to
assign a fresh value to the entire structure. Even for elementary types, it
has been found sometimes more efficient to perform selective updating,
particularly for unpacked representations of Cartesian products and for
arrays. The increased efficiency of selective updating is usually even more
pronounced in the case of advanced data structures.
(3) Advanced data structures, whose size varies dynamically, require some
scheme of dynamic storage allocation and relinquishment. The units of
storage which are required are usually linked together by pointers, sometimes
known as references or addresses; and their release'is accomplished either by
explicitly programmed operations, or by some form of general garbage
collection. The use of dynamic storage allocation and pointers leads to a
significant complexity of processing, and the problems can be particularly
severe when the data has to be transferred between the main and backing
store of a computer. No problems of this kind need arise in the case of
elementary data structures.
(4) The choice of a suitable representation for an advanced data structure
is often far more difficult than for an elementary structure; the efficiency of
the various primitive operations depends critically on the choice of repre-
sentation, and therefore a sensible choice of representation requires a
knowledge of the relative frequency with which these operations will be
invoked. This knowledge is especially important when a part or all of the
structure is held on a backing store; and in this case, the choice of re pre-
132 C. A. R. HOARE
sentation should take into account the characteristics of the hardware device;
that is, arrangement of tracks and cylinders on a rotating medium, and times
of head movement and rotational delay. In the case of elementary structures,
the primitive operations are of roughlv comparable efficiency for most
representations.
Thus the differences between advanced and elementary structures are quite
pronounced, and the problems involved are significantly greater in the
advanced case. This suggests that the practical programmer would be well
advised to confine himself to the use of elementary structures wherever
possible, and to resort to the use of advanced structures only when the
nature of his application forces him to do so.
The first and most familiar example of an advanced data structure is the
sequence. This is regarded as nothing but a sequence of an arbitrary number
of items of some given type. The use of the term "sequence" is intended to
cover sequences on magnetic tapes, disc, or drum, or in the main store.
Sequences in the main store have sometimes been known as streams, lists,
strings, stacks, deques, queues, or even sets. The term file (or sequential
file) is often used for sequences held on backing store. The concept of a
sequence is an abstraction, and all these structures may be regarded as its
various representations.
Our first example of a sequence is the string, familiar to programmers in
ALGOL and SNOBOL. Since a string is constructed as a sequence of
characters of arbitrary length, it may be defined:
type string = sequence character.
The next example is drawn from a data processing application; the
maintenance of a file of data on cars. Each item of the file (sometimes known
as a record) represents a single car, and is therefore of type car; an example
of a possible definition of the car type has been given previously:
type car file = sequence car.
The third example gives an alternative method of dealing with a pack of
cards. This may be regarded as just a sequence of cards, of length which
perhaps varies as the cards are dealt:
type deck = sequence cardface;
Of course, not all card-sequences represent actual decks of cards in real life;
for example, sequences which contain the same card twice are invalid, and
should be avoided by the programmer. Thus the maximum length of a valid
deck is 52, although this fact is not expressed in the declaration.
The next example is drawn from the processing of a particular class of
symbolic expression, namely the polynomial. A polynomial
OnX" + On-1Xn-i •••. 01X + Oo
NOTf.S ON DAT A STRUCTURING 133
can be represented as the sequence of its coefficients a1• If the degree n of the
polynomial is unpredictable or variable during the course of a calculation,
a sequence is the most appropriate method of defining it:
type polynomial = sequence integer.
Our final example shows how it is possible to represent the programming
language concept of the identifier. Since in theory an identifier may be of
arbitrary length, a sequence is required. The items of the sequence are either
letters or digits. However, the first character is always alphabetic and may be
separated from the rest. Thus an exact definition of a data structure corres-
ponding to the identifier is:
type identifier= (first:letter; rest: sequence (/:letter, d:digit)).
8.1 MANIPULATION
The zero element of a sequence type Tis the sequence that contains no items-
this is known as the null or empty sequence, and is denoted by T( ). For
each value v of the domain type, there is a sequence whose only item is v;
this is known as the unit sequence of v and is denoted by T(v). Finally, if
vi. v 2 , ••• , vn are values from the base type (possibly with repetition},
T(v 1 , v 2 , ••• , vn) denotes the sequence consisting of these values in the
stated order. If for convenience the type name T is omitted, we will use
square brackets to surround the sequence:
[v], [v 1 ,v 2 , ••• , vJ
However, a sequence of characters is normally denoted by enclosing them in
quotes.
The basic operation on sequences is concatenation, that is, adjoining two
sequences one after the other. Thus if xis the sequence of characters "PARIS
IN THE" and y is the sequence "THE SPRING", their concatenation F'y
is the sequence
z = "PARIS IN THETHE SPRING"
Unless the operands are exceptionally small, concatenation is very inefficient
on a computer, since it usually involves making fresh copies of both operands.
The programmer should therefore make every effort to replace concatenation
by selective updating.
The basic operators for breaking down a sequence into its component parts
are those that yield the first and last items of a non-empty sequence
x. first, x. last
and those that remove the last or first items of a non-empty sequence,
yielding the initial or final segments.
initial (x}, final (x).
134 C. A. R. HOARE
and it removes the last item from x. This operation can be used to "pop up"
the top item of a stack which has been "pushed down" by an ordinary
writing operation:
,--..
x: T(v).
If desired, it is possible to define the fourth updating operation, that of
attaching a new value to the beginning of a sequence. (putback (x, v)).
NOTES ON DATA STRUCTURING 135
In some cases, it is more efficient to avoid the copying of an item which is
involved in the from• operation. These cases may be dealt with by merely
omitting the left hand variable, e.g.
fromx
back from x.
In this case, access to the items of the sequence will usually be made by the
selectors x. first and/or x. last.
It is very common to wish to scan all the items of a sequence in succession;
a suitable notation for this is modelled on the for statement:
for v in x do S;
If xis empty, the statement is omitted. Otherwise the variable v (regarded
as local to S) takes in succession the values of all items from the sequence
x, and Sis executed once for each value. In this construction neither x nor 11
should be updated within S.
A similar construction can be used for defining a sequence as an item-by-
item transformation E(v) of items v in sequences.
for v ins take E(v).
In deciding a representation for a sequence, it is most important to know
which of the selective updating operations are going to be carried out upon it.
(I) If the only operation is from, the sequence is known as an input
sequence; obviously in order to have any value at all, an input sequence
must be initialised to some value existing in the outer environment in which
it is declared. The association of a sequence local to a program with some
file existing more or less permanently on backing store is often known as
"opening" the file for input, and we assume that this operation is invoked
implicitly on declaration of a local input sequence. The reverse operation of
"closing" the file is invoked implicitly on exit from the block to which
the sequence is local.
(2) If the only operation is writing to the file, the sequence is known as an
output sequence. An output sequence may be initialised from the environment
in the same way as an input sequence; or more commonly, it may take an
empty initial value. In either case, in order to serve any useful purpose, the
final value of the sequence on exit from the block must be assigned to some
variable existing in the outer environment in which the sequence is declared.
The identity of this outer variable should be declared together with the
sequence; if this outer variable is held more or less permanently on backing
store, it is known as an output file; and the rules for implicit invocation of
opening and closing of the file on entry and exit to the block are similar to
those for input files.
136 C. A. R. HOARE
{3) If the only operations are writing and reading back (push down and
pop up), the sequence is known as a stack; the initial value of a stack is
always empty, and the final value is not usually preserved.
(4) If the only operations are writing to the end and reading from the
beginning, the sequence is known as a queue; again, the initial value is always
empty, and the final value is not usually preserved.
(5) If reading and writing at both ends of a sequence are permitted, the
sequence is sometimes known as a deque (double-ended queue). However,
to make all four operations equally efficient requires some complexity of
representation, so it is fortunate that most programs can get by without
using deques.
8.2. REPRESENTATION
8.2.1. Contiguous representation
The simplest method of representing a sequence is to allocate to it a fixed
contiguous area of storage, adequate to hold all items actually required.
This method is suitable if the value (or at least the length) of the sequence is
constant throughout the execution of the program-for example, a string of
characters intended to be used as an output message or title.
In some cases, the length of the sequence is unknown at the time the
program is written, but is known on entry to the block in which the sequence
is declared, and this length remains constant throughout the existence of the
sequence. In such cases, it is possible to allocate a contiguous area of storage
in the local workspace of the block, using the standard stack method of store
allocation and deallocation.
Even if the length of the sequence is subject to variation, it is sometimes
possible to place an acceptably small upper bound on its length, and allocate
permanently this maximum area. If the limit is exceeded during a run of the
program, the programmer must be willing to accept its immediate termina-
tion. In addition to the fixed area, a pointer or count is required to indicate
the current beginning and end of the sequence. In the case of a stack, the first
item is always at the beginning, and only one pointer to the top of the stack
is required. In the case of a queue, the sequence will at times overlap the
end of the store area, and be continued again at the beginning. Such a
representation is known as a cyclic buffer, and may be used in a parallel
programming situation to communicate information between processes
running in parallel. In this case, when a writing process finds the buffer full,
it has to wait until a reading process reduces the size of the sequence again.
Similarly, the reading process must wait when the buffer is empty.
Another case where the contiguous representation is the best is when the
program requires only a single sequence, which may therefore occupy the
NOTES ON DATA STRUCTURING 137
whole of the remaining store available after allocation to other purposes;
and if overflow occurs, the program could not have been run anyway. If
two stacks are required, they can both be accommodated by arranging that
one of them starts at one end of remaining available store and grows upwards,
and the other starts at the other end and grows downwards. If the stacks
meet, the program cannot continue.
If many sequences are to be represented, it is possible to set up a scheme
in which they are spread through the remaining available store; and if any
of them grows to meet its neighbour, it is possible to reshuffle some or all
of the sequences, so that they all have sufficient room to grow again for a bit.
For each sequence there must be a base location pointing to its beginning,
through which that sequence is always addressed. In addition, the actual
length of the sequence must be stored. The base location and length of the
neighbouring sequence must always be inspected when the sequence is
extended. When reshuffling takes place, the base locations of all moved
sequences are updated to point to the new position of the sequence. This is
quite a useful ad hoc scheme in cases where the reshuffling is known to be
relatively infrequent; otherwise non-contiguous representations are to be
preferred.
used
used
- free
. tfree
I ...
free
--
used
(a) ( b)
-- free
(c)
used
= 2 length
I length
=
3 length
,__ __
_____,
...__ ___,
write pointer
read painter
1..r--==:i-J m~gnce
pointers
(al (bl ( c)
trap of supposing that this will help when there is a basic mismatch in the
speeds of processing and transfer. In general, if double or triple buffering is
inadequate, it is not worth while filling the store with any further extra
buffers.
In a machine which is endowed with an automatic paging scheme, the
problems of representing sequences are very much reduced. As far as the
programmer is concerned, he need only allocate the amount of storage
required for the longest possible sequence, using the contiguous representa-
tion. This should not actually cause any waste of storage, since the paging
system should delay allocation of store until it is first used. As the sequence
expands, new blocks of store will be allocated, but the addressing of these
blocks will appear contiguous to the programmer, so there is no problem
of leaving unused space at the end of blocks which are not large enough to
hold the next item. Shortly after a block has been filled, it will automatically
migrate to backing store; and it will be brought back again automatically
as soon as it is required. On input sequences, a block which has been scanned
will also be removed shortly afterwards from main store; but this will not
involve an unnecessary backing store transfer if the material has not been
changed since the last input took place. The only operation which a paging
system will not perform automatically is to read a block of an input sequence
into store ahead of its actual requirement.
There are certain close analogies between the methods used for structuring
data and the methods for structuring a program which processes that data.
Thus, a Cartesian product corresponds to a compound statement, which
assigns values to its components. Similarly, a discriminated union corresponds
to a conditional or case construction, selecting an appropriate processing
method for each alternative. Arrays and powersets correspond to for state-
ments sequencing through their elements, with an essentially bounded
number of iterations.
The sequence structure is the first that permits construction of types of
infinite cardinality, with values of unbounded length; and it corresponds to
the unbounded form oflooping, with a while condition to control termination.
The reason why the sequence is unbounded is that one of its components
(i.e. the initial segment) from which it is built up belongs to the same type as
itself, in the same way as the statement which remains to be obeyed after
any iteration of a while loop is the same statement as before.
The question naturally arises whether the analogy can be extended to a
data structure corresponding to recursive procedures. A value of such a
type would be permitted to contain more than one component that belongs
NOTES ON DATA STRUCTURING 143
to the same data type as itself; in the same way that a recursive procedure
can call itself recursively from more than one place in its own body. As in
the case of recursive procedures such a structure can conveniently be defined
by writing the name of the type being defined actually inside its own definition;
or in the case of mutually recursive definition, in the definition of some
preceding type.
The most obvious examples of recursive data structures are to be found
in the description of arithmetic or logical expressions, programming lan-
guages, where the recursion reflects the possibility of nesting one expression
inside another. For example, an arithmetic expression might be defined as
follows:
"An expression is a series of terms, each of which consists of a sign
(+ or - ) followed by a sequence of factors. Each factor except the first
consists of a sign ( x or/) followed by a primary. A primary is either a
constant, a variable, or an expression surrounded by brackets. An initial
plus sign in an expression may be omitted."
A structured data type whose values comprise such expressions may be
defined usip.g only techniques already familiar, plus recursion:
type expression = sequence term;
type term= (addop:operator;f:sequence factor);
type factor= (mulop:operator;p:primary);
type primary = (const: (val: real),
var: (id: identifier),
bracketed : (e: expression));
type operator = (plus, minus, times, div);
This definition expresses the abstract structure of an arithmetic expression,
but not the details of its concrete representation as a string of characters.
For example, it does not specify the symbols used for brackets or operators,
nor does it state whether an infix, prefix or postfix notation is used for them.
It does not state how the three kinds of primary are to be distinguished.
It does not even represent the optional omission of plus on the first term of
an expression, and the necessary omission of x on the first factor of a term.
Apart from this degree of abstraction and representation-independence, this
type definition would correspond to a set of BNF syntax equations:
(expression)::= (term) I (addop)(term) I
(expression)(addop)(term)
(term): : = (primary) I (term) (mulop) (primary)
(primary)::= (unsigned real number) I (variable) I
( (expression))
144 C. A. R. HOARE
Tree
--+-----<-iO A B
NIL
tag
B
A
(al
end
compile expression: - [compile term (sign)]
end;
function compile term (s: operator): term;
begin p: primary; sign: operator; fs: sequence factor;
148 C. A. R. HOARE
p: = compile primary;
fs: = [factor (times, p)];
while source. first = times v source. first = div do
begin sign from source:
-
p: = compile primary;
fs: [factor (sign, p)]
end;
compile term:= term (s,fs)
end;
function compile primary: primary;
begin s: symbol;
s from source;
withs do {constant: compile primary:= const (value),
variable: compile primary: = var (identifier),
leftbracket:
begin from source;
compile primary:= bracketed (compile expression);
s from source;
ifs .,p rightbracket then go to error
end,
else go to error}
end;
Exercise
Write programs to convert an expression from tree representation to
bitstream and back again.
This array caters for multiple marriages better than the more tree-like
representations of a family, which can be defined as a recursive structure.
In the case of sparse arrays, it is sometimes useful to regard them as
partial rather than total mappings. A partial mapping is one which does not
necessarily give a value for each member of its domain type. In other words,
the actual domain over which it is defined is a subset of the domain type.
For such an array type it is necessary to introduce an additional constant
omega, denoting a mapping which is everywhere undefined. It is also useful
to introduce a function
domain (x)
which delivers as result the set of subscripts for elements of x which are
actually defined. Thus the programmer can sequence through all the defined
elements, or test whether a particular element is defined or not. Many of the
examples quoted above might well have been declared as partial instead of
sparse. In the case of a partial mapping, the default value does not have to be
recorded.
10.l REPRESENTATION
Sparse sets and arrays are usually represented by simply keeping a record
of the default value and those members or elements which are significant;
thu5 the representation borrows techniques which are used in the case of the
sequence type to deal with structures of changeable size. A sparse set may be
regarded as a special case of a sparse mapping, which maps all its members
onto the Boolean value true, and all its non-members onto the default value
false. Thus their representations are closely similar to those of sparse arrays,
and do not require separate treatment.
A sparse mapping consists of a number of elements. Each element
of the mapping is represented as the Cartesian product of its subscript and
its value; in this case the subscript is known as the key, and the value is
known as the information associated with the element, and the juxtaposition
of the two will be known as an entry. In the case of a set which is sparse,
there is no need to record any information, since the presence of the key
itself is sufficient to indicate that this value is a member of the set. Thus an
entry for a sparse set consists only of a key.
152 C. A. R. HOARE
to ensure that the sizes and location of the sequences and sections be chosen
to correspond closely with the access characteristics of the storage medium.
A - - 02 border chain
r-- --
d2
~---
d2
d1 ------- No value of
A [d1 ,d2) A [dt ,d2)
-1---
~------
d'I A (dj ,d2)
A [dt ,d2]
each student can attend the examination for each course that he has taken.
This can always be arranged by allocating a separate session for each examina-
tion; but the interests of examiner and student alike dictate that the total
examination period be as short as possible. This means that each session
should contain as many examinations as possible, subject to some limit k.
An additional constraint is imposed by the size of the examination hall,
which can only accommodate a certain maximum number of students.
Before designing the program, it is desirable to confirm our understanding
of the problem by making a more rigorous formalisation in terms of the
structure of the various items of data, both given and required. The types
"student" and "exam" are obviously unstructured and need no further
definition at this stage. The load of exams to be taken by each student is
given by a mapping:
load: array student of powerset exam.
A timetable is a set of sessions, where each session consists of a set of exams:
type session = powerset exam;
timetable: powerset session.
We next attempt to formalise the properties which the input and output
data are required to possess.
(I) We choose not to formalise the condition that the number of sesi.ions
be minimised, since in fact we do not want an absolute minimum if this
turns out to be too expensive to compute.
(2) Each exam is scheduled for one of the sessions
U s =exam.all
s in timetable
individual sessions, and do not mention the timetable at all. This suggests
that the program can be structured as an inner part which selects a suitable
session satisfying (4) (5) and (6), and an outer loop which constructs the
timetable out of such suitable sessions.
The objective of the outer loop is to achieve satisfaction of conditions (2)
and (3) on its completion. We therefore choose one of these conditions as a
terminating condition of the loop, and design the body of the loop in such a
way that is preserves the truth of the other condition (that is, the invariant
of the loop); furthermore we ensure that the invariant is true before starting
the loop.
The obvious choice of invariant is exclusiveness (condition (3)), leaving
exhaustiveness as the terminating condition towards which each execution
of the body of the loop will progress. The empty timetable obviously satisfies
the invariant. This leads to an algorithm of the following structure:
timetable: = { } ;
while timetable does not satisfy (2) do
begin select a session satisfying (4), (5), (6);
add the session to the timetable
end;
print timetable.
In order for the addition of a new session to preserve the truth of the
invariant, it is necessary that the exams of the session shall be selected from
exams which do not yet appear in the timetable. We therefore introduce a
new variable to hold these remaining exams:
remaining: powerset exam;
which is defined by the invariant relation:
remaining = exam. all- LJ s.
s in timetable
already to be possible, but excluding any exams which have already been
tried. We therefore introduce a variable:
untried: powerset exam,
and a procedure
procedure gensupersets,
which generates and records all possible supersets of trial by adding one or
more exams from untried to it. This procedure will be called from within
"suitable".
function suitable: session;
begin trial, bestsofar: session; e: exam; untried: powerset exam :
e from remaining;
trial : = bestsofar: = {e} ;
untried:= remaining - trial - incompat (e);
gensupersets;
suitable: = bestsofar
end;
Note that the first value of the trial is the unitset of some exam chosen from
the remainder according to some as yet undefined criterion. The justification
for this is that the chosen exam must eventually feature in some session of
the timetable, and it might as well be this one. If this prior choice were not
made, gensupersets would keep on generating the same supersets on every
cycle of the major loop of the timetabling program.
As another significant optimisation, we have removed from untried any
exams which are incompatible with the exams in the trial, since there is no
need to even consider the addition of any of these exams to the trial.
The generation of supersets of a given trial may proceed by selecting
each exam from untried, and adding it to trial. If the result is still valid, it
should be recorded, and the new value of trial is then a suitable session to
act as a basis for further superset generation. This suggests a recursive
program structure. Of course, the exam added to trials should also be sub-
tracted from untried, to avoid unnecessary repetitions; and it is very advan-
tageous to remove from untried any exams which are incompatible with the
exam just added to the trial, so that these do not have to be considered again
in future. Also, the values of trial and untried must be left unchanged
by each call, so any change made to them must be recorded and restored in
variables save I and save 2.
NOTES ON DATA STRUCTURING 161
procedure gens upersets;
begin e: exam ; save I , save 2: l>O"'erset exam ;
record; save I : = untried;
if size (trial) < k then
while untried '# { } do
begin e from untried;
save 2: = untried /\ incompat (e);
untried: - save 2;
trial: v {e};
if sessioncount (trial) < hallsize then
gensupersets;
untried: v save 2;
trial: - {e}
end;
untried: = save I
end gensupersets.
The validity of this program depends on the fact that trial invariantly
satisfies all conditions (4) (5) and (6) for sessions of the timetable, as well as
always being a subset of remaining.
The reasoning is as follows:
for (4): gensupersets never generates a superset except when the size of the
trial is strictly less than k.
for (5): gensupersets is never entered when the sessioncount of trial is
greater than the hall size (we assume that no examcount is greater than
hallsize).
for (6): removal of incompatible sets from untried ensures that at all
times all exams remaining in untried are compatible with all exams of trial.
Therefore, transfer of an arbitrary exam from untried to trial can never
cause (6) to be violated.
Finally, at the initial call of gensupersets, untried c remaining. Untried is
an essentially non-increasing quantity: every addition of members to it has
always been preceded by removal of those very same members. Untried is
therefore always a subset of remaining; and trial, which is constructed only
from members of untried, must also always be a subset of remaining.
This completes our first version of an abstract program to construct
examination timetables. Collecting all the material together, it looks like.this:
162 C. A. R. HOARE
(6) incompat
The most frequent use of elements of incompat is to subtract them from
untried. They should therefore also use the bitpattern representation. This
will require 500 x 500 bits, of the order of 10000 words. This is by far the
largest data structure required, but its total size is probably fully justified
by the extra speed which it imparts to the program, and since it is acceptable
on most computers on which this program will run, it does not seem worth
while to seek a more compact representation.
(7) load
The load of each student is the primary input data for the problem; it may
also be extremely voluminous. It is therefore doubly fortunate that the
program only needs to make a single scan of the data; for not only will this
enable the data to be presented as an external sequence; it also means that
the representation can be designed to be suitable for human reading, writing,
and punching.
We therefore allocate one card for each student, and use ten columns of
six characters each to hold the examination numbers. To save unnecessary
punching, the first blank column will signify the end of the examination set.
For identification purposes, each card should also contain the student
number; fortunately this can be wholly ignored by the program, though it
should probably be checked to avoid duplications or omissions.
Exercise
Code the abstract program described above using the recommended data
representations.
166 C. A. R. HOARE
I 2. AXIOMA TISATION
Abbreviations:
If e is a monadic operator and EB is a dyadic operator, both taking operands
from the base type T 0 , then the following abbreviations permit omission of
the transfer function, if a is of type T 0 and x, y are of type T:
(14) 8 x stands for 8 T 0 (x).
(15) x EB y ,, ,, T 0 (x) EB T 0 (y).
(16) x EB a To(x) Ea a.
" "
(17) a EB x a EB T 0 (x).
" "
(18)a:=x a:= T 0 (x).
" "
,,
(5) If x '.s a T then
with x do S or with x take S stands for
which means that each of the subscripts of S replaces all free occurrences
of the corresponding superscript in S.
a in (x v y) =(a in x v a in y)
a in (x y) =(a in x & a in y)
A
-
(2) If x is a T and d is a D
. aT
then x T(d) is
(3) The only elements of Tare as specified in (1) and (2)
(4) (;-'T(d)).last = d
NOTES ON DATA STRUCTURING 173
(5) initial (x.-T(d)) = x
(6) x-.(/-z) = (x-..y) z
(7) T(d).first = d
(8) x =!: T( ) ::> (x-..T(d)).first = x.first
(9) final (T(d)) = T( )
(10) x =!: T( ) ::> final (x-.T(d)) = final (x)-.T(d)
Note: last, initial, first, and final are not defined for T( )
(11) T( ) ends y
(12) x-.T(d) ends y =y =!: T( ) &y.last = d & x ends initial (y)
(13) x begins T( ) =
x = T( )
(14) x begins y-.T(d) =
x = y-.T(d) v x begins y
(15) length (T( )) = 0
(16) length (x-..T(d)) = succ(length (x))
(18) x ~ T( ) => x = T( )
{19) x, y =!: T( ) => (x ~ y =x.first < y.first v (x.first = y.first
& final (x) ~ final (y)))
Abbreviations:
(20) x:-..T(d) means x: = x-..T(d)
(21) d from x means d: = x.first; x: = final (x)
(22) d back from x means d: = x. last; x: = initial (x)
(23) from x means x: = final (x)
(24) back from x means x: = initial (x)
(25) T(d 1 , di, .. ., dn) stands for
(T( )-..T(d 1 )T(di)-. •. :-'T(dJ)
(26) [d 1 , di, ... , dnJ stands for T(d 1 , di, ... , dn)
(27) If x = [d 1 , di, ... , dJ then
for d in x do S stands for
""'. .. .' s•...
•. ..l.,,
Sdi'
for d in x take E stands for
[EL E: 2, ••• , E:n1
174 C. A. R. HOARE
Theorems
x = y !!!.: (x = y = T( ) v x .first = y. first & x .final = y.final)
2' (x = y = T( ) v x.Iast = y.last&x.initial = y.initial)
REFERENCES
The following works have acted as an inspiration and guide for this chapter, and
they are recommended for further reading.
I am also deeply indebted to Professor N. Wirth for many fruitful discussions
and suggestions, and for his willingness to test several of the ideas of the paper
in his design and implementation of PASCAL; and to Professor E.W. Dijkstra for
his perpetual inspiration.
Dijkstra, E. W. (1972). Notes on Structured Programming. "Structured
Programming". pp. 1-82. Academic Press, London.
Kr:uth, D. E. (1968). "The Art of Computer Programming" Vol. 1, Chapter 2.
Addison-Wesley, Reading, Mass.
McCarthy, J. (1963). "A Basis for a Mathemetical Theory of Computation in
Computer Programming and Formal Systems" (eds. Braffort, P. & Hirschberg D.).
North-Holland Publishing Company, Amsterdam.
Mealy, G. H. (1967). Another Look at Data. A.F.l.P.S. Fall Joint Computer
Conference Proceedings. 31, pp. 525-534.
Wirth, N. (1970). Programming and Programming Languages. Contribution to
Conference of European Chapter of A.C.M, Bonn.
Wirth, N. (1971). Program Development by Stepwise Refinement. Comm. A.C.M.
14, 4, pp. 221-227.
Wirth, N. (1971). The Programming Language PASCAL. Acta Informatica, 1, I,
pp. 35-63.
III. Hierarchical Program Structures
I. INTRODUCTION
In this monograph we shall explore certain ways of program structuring and
point out their relationship to concept modelling.
We shall make use of the programming language SIMULA 67 with
particular emphasis on structuring mechanisms. SIMULA 67 is based on
ALGOL 60 and contains a slightly restricted and modified version of
ALGOL 60 as a subset. Additional language features are motivated and
explained informally when introduced. The student should have a good
knowledge of ALGOL 60 and preferably be acquainted with list processing
techniques.
For a full exposition of the SIMULA language we refer to the "Simula 67
Common Base Language" [2]. Some of the linguistic mechanisms introduced
in the monograph are currently outside the "Common Base"*.
The monograph is an extension and reworking of a series of lectures
given by Dahl at the NA TO Summer School on Programming, Marktoberdorf
1970. Some of the added material is based on programming examples that
have occurred elsewhere [3, 4, 5].
2. PRELIMINARIES
•The Simula 67 language was originally designed at the Norwegian Computing Center,
Oslo. The Common Base defines those language features which are common to all
implementations. The Common Base is continually being maintained and revised by the
"Simula Standards Group", each of whose members represents an organisation responsible
for an implementation. 8 organisations are currently represented on the SSG. (Summer
1971).
175
176 OLE-JOHAN DAHL AND C. A. R. HOARE
language provides us with basic concepts and composition rules for con-
structing and analysing computing processes.
The following are some of the basic concepts provided by ALGOL 60.
(l) A type is a class of values. Associated with each type there are a
number of operations which apply to such values, e.g. arithmetic operations
and relations for values of type integer.
(2) A variable is a class of values ofa given type ordered in a time sequence.
The associated operations are accessing and assigning its current value.
Both can be understood as copying operations.
(3) An array is a class of variables ordered in a spatial pattern. Associated
is the operation of subscripting.
Notice that each of the concepts includes a data structure as well as one
or more associated operations.
As another example consider machine level programming. The funda-
mental data structure is a bit string, which is not itself a very meaningful
thing. However, combined with an appropriate sensing mechanism it has the
significance of a sequence of Boolean values. In connection with a binary
adder the bit string has the meaning of a number in some range, each bit
being a digit in the base two number system. An output channel coupled to a
line printer turns the bit string into a sequence of characters, and so forth.
Thus the meaning of the data structure critically depends on the kind of
operations associated with it.
On the other hand, no data process is conceivable which does not involve
some data. In short, data and operations on data seem to be so closely
connected in our minds, that it takes elements of both kinds to make up any
concept useful for understanding computing processes.
3. OBJECT CLASSES
3.2. GAUSS-INTEGRATION
A definite integral may be approximated by an "n-point Gauss formula",
which is a weighted sum of n function values computed at certain points in
the integration interval.
b n
f f(x)dx ~ i~l wJ(x
a 1)
The weights and abscissa values are chosen such as to give an exact result for
the integral of any polynomial of degree less than 2n. By a suitable trans-
formation we find
w1 = (b - a)W1 and X; =a+ (b - a)X1,
184 OLE-JOHAN DAHL AND C. A. R. HOARE
4. COROUTINES
resume (X)
if we first swap p[I] and p[k], then p[2] and p[k], ... , and then p[k - I] and
p[k]. Thus we are led to the following kernel:
integer i;
permute (k - 1);
for i: = 1 step 1 until k - I do
begin swap (p[i], p[k]); permute (k - l) end;
On the assumption that permute (k - 1) leaves p unchanged, this kernel has
the net effect of rotating the elements p[l], p[2], ... , p[k] one place cyclically
to the right. This can be seen from the example:
original state: 1 2 3 4 5
after swap (p[l], p[5]): 5 2 3 4 1
after swap (p[2], p[5]): 5 1 3 4 2
after swap (p[3], p[5]): 5 1 2 4 3
after swap (p[4], p[5]): 5 1 2 3 4
Since the overall effect of the operation must be to leave the array p as it was
before, the right rotation must be followed by a compensatory left rotation.
q: = p[l];
for i: = 1 step 1 until k - 1 do p[i]: = p[i +I];
p[k]: = q
Finally it is necessary to determine an appropriate action for the case where
k = I. Recall that the purpose of the procedure is to
"generate all permutations of k objects, issuing a detach command after
each of them".
Since the only permutation of one number is that number itself, all that is
necessary is to issue a single detach instruction.
The permute procedure must be written as an attribute of the permuter
class, so that the detach which it issues relates to the relevant object. The
whole class may now be declared:
class permuter (n); integer n;
begin integer array p[l :n]; integer q; Boolean more;
procedure permute (k); integer k;
if k = 1 then detach else
begin integer i; permute (k - l);
for i: = 1 step 1 until k - l do
begin q: = p[i]; p[i]: = p[k];
p[k]: = q; permute (k -1) end;
HIERARCHICAL PROGRAM STRUCTURES 193
q: = p[l];
for i: = l step l until k - l do p[i]: = p[i + l];
p[k]: = q
end of permute;
for q: = l step l until n do p[q]: = q;
more:= true; permute (n); more:= false
end of permuter;
Note. The detach issued by a permute procedure instance is not an exit
out of the procedure instance, and does not return control to the call of the
procedure. Rather, it is an intermediate exit out of the object as a whole
(including the entire recursion process) and passes control back to the main
program which generated or called the object. A subsequent call on the object
will thus resume the recursion process exactly where it left off.
The decision (assumption) that the procedure permute should leave the
sequence unchanged is really quite arbitrary. The reader is invited to convince
himself of this fact by writing a procedure based on the same swapping
strategy, which returns with the numbers in the reverse order.
5. LIST STRUCTURES
end of tree;
The bodies of the two procedure components are quite simple recursive
procedures, matching the recursive structure of the tree:
insert: if x < val then
begin if left = = none then left: - new tree (x)
else left. insert (x)
end
else if right = -= none then right: - new tree (x)
else right.insert (x);
integer min, j, i;
min:= O;
while min < integer max do
begin min:= trav [1] .current; j: = I;
for i: = 2 step I until N do
if min > trav [i]. current then
begin min: = trav [i). current;
j: = i
end search for smallest current;
if min < integer max then
begin output (min);
call (trav UD
end
end of merge process;
Note that the analyser will discover all successful analyses of any initial
segment of the text.
The syntax tree output on each call of the analyser will contain a node for
each phrase identified in the text. Each phrase has the following attributes:
integer i: indicating the syntactic class of the phrase
integer j: indicating which alternative of its class it belongs to
ref (phrase) sub:refers to the last subphrase of the given phrase
ref (phrase) left: refers to the phrase immediately preceding this phrase
on the same level of analysis. The left of the first
subphrase of any phrase is none.
Thus the expression
MxN+ 7
should give rise to a tree of the form shown in Fig. 1.
The syntax analyser will be recursively structured, as a class of phrase
objects, each of which reproduces on a single phrase the intended behaviour
of the analyser as a whole.
A phrase object accepts a meta-symbol i and a left neighbour as parameter,
and is responsible for producing all possible syntax trees of the given syntax
class which match a portion of text to the right of (and including) the current
symbol. The input text will on each occasion be stepped on to the first
symbol which does not match the stated analysis. When all possible analyses
are complete, the tape is stepped back to the position it was before entry to
the given phrase, a global variable good is set to false, and the phrase
terminates.
We are now in a position to outline the general structure of the phrase
class:
class phrase (i, left); integer i; ref (phrase) left;
begin integer j; ref (phrase) sub;
for j: = 1 step l until jm[i] do
... match remainder of text in all possible
ways to alternative j of class i,
issuing a detach after each successful match ... ;
good: = false
end of phrase;
Assume that an object has successfully matched the first k - I(k > 0)
symbols of a chosen alternative U) for the given meta-symbol (i). We now
formulate a piece of program for matching the kth symbol to the input in all
possible ways. We assume that the remainder, if any, of the right hand side is
HIERARCHICAL PROGRAM STRUCTURES 199
expression
sub
left !!!!!!!!
expression
'
~
i
~,~.
2 term
~...
e
'
I
/
7 variable 3 primary
/
3 primory
M
5 2
!1l!!!!t
!!2!11 ~ none
7 variable constant
N 7
6
FIGURE 1
matched to the input in all possible ways by the statement "match remainder",
and that this statement leaves unaltered the position of the reading head and
the part of the syntax tree so far constructed. We make the latter assumption
also for an object which has failed to identify (another) phrase.
1. begin integer g; g: = G[i, j, k];
2. if g = ".L" then begin good : = true; detach end
3. else if g = symbol then
4. begin move right; match remainder; move left end
5. else if meta (g) then
6. begin sub: - new phrase (g, sub);
200 OLE-JOHAN DAHL AND C. A. R. HOARE
7. while good do
8. begin match remainder; call (sub) end;
9. sub: - sub.left
10. end
11. end
Comments.
Line l. The kth symbol of the right hand side number j is called g for brevity.
Line 2. If g is the terminator symbol the whole right-hand side has been
successfully matched to the input. The object reports back to its
master. Line 2 does not alter the syntax tree or the position of the
reading head.
Line 4. Since we have a match the object moves the reading head to the next
symbol. After having matched the remainder in all possible ways the
object restores the position of the reading head. Thus, according to
assumptions, line 4 has a null net effect.
Line 6. Since g is a meta-symbol, a new phrase object is generated to
identify sub-phrases of the syntax class g. It becomes the new
rightmost sub-phrase. Its left neighbour phrase is the old rightmost
sub-phrase.
Line 7. We have assumed that an object when failing sets "good" to false.
Line 8. Since "good" is true, a sub-phrase has been identified matching g.
After having matched the remainder in all possible ways, "sub" is
called to identify the next possible sub-phrase. Since we want to
match g in all possible ways, line 8 is repeated until the sub-phrase
object fails.
Line 9. According to assumptions a phrase object which has failed, has had
a null net effect. The total effect of lines 6-8 is thus to add an
object •o the syntax tree. Line 9 restores the syntax tree to its
original state.
The comments show that the block above matches the symbol g followed by
the remainder of the jth right-hand side of i in all possible ways and has a null
net effect. Consequently the block itself satisfies the assumptions made for the
"match remainder" statement. It follows that the whole matching algorithm
may be expressed in a simple way by a recursive procedure. The whole
computing process is described by the following class declaration.
HIERARCHICAL PROGRAM STRUCTURFS 201
class phrase (i, left); integer i; ref (phrase) left;
begin integer j; ref (phrase) sub;
procedure match (k); integer k;
begin integer g; g: = G[i,j, k];
if g = ".l" then begin good: = true; detach end
else if g = symbol then
begin move right; match (k + l); move left end
else if meta (g) then
begin sub: - new phrase (g, sub);
while good do
begjn match (k + l); call(sub)end;
sub: - sub.left
end
end of match;
for j: = 1 step 1 untiljm[i] do match (l);
good: = false
end of phrase
A master program could have the following structure
ref (phrase) tree;
followed by some symbol outside the alphabet, say ".L ", and the master
program might have the following structure
ref(phrase)parse; Boolean good ;
parse: - new phrase (start, none);
while good do
begin if symbol = ".L" then inspect successful parse;
call(parse)
end
It is a remarkable feature of the phrase class that the result it yields on
each call is a tree whose nodes consist in phrase objects which have been
activated recursively and not yet terminated. Each of these phrase objects
plays a dual role, both as a part of the syntactic tree which is to be inspected
by the master program, and as the set of local variabfos for the recursive
activations of other phrase objects. It is this close association of data and
procedure which permits the algorithm to be so simply and concisely
formulated.
Notice that each phrase object is the nucleus of a separate stack of recursive
activations of its local "match" procedure. At the time when a detach is
issued on behalf of an object, signalling a successful (sub-) parse, its stack
has attained a terr Jorary maximum depth, one level for each symbol in the
current right-hanil side, plus one level corresponding to the rhs terminator .L,
which issued the detach.
Thus the whole dynamic context of a successful parse is preserved. When an
l)bject is called to produce an alternative parse a backtracking process takes
place, during which the "match" stack of the object is reduced. At a level
corresponding to a meta-symbol in the rhs the match procedure calls on the
corresponding phrase object to produce an alternative sub-parse (line 8) and
so on. (cf. the row of officers in Chaplin's Great Dictator).
6. PROGRAM CoNCATENATION
In the preceding sections we have seen how the class mechanism is capable of
modelling certain simple concepts, by specifying data structures and defining
operations over them. In this section, we develop a method by which more
elaborate concepts can be constructed on the basis of simpler ones. This will
establish potential hierarchies of concepts, with complex concepts sub-
ordinate to the more simple ones in terms of which they are defined. The
structuring technique gives a new method of composing a program from its
constituent parts, and is known as concatenation.
ffiERARCffiCAL PROGRAM STRUCTURES 203
Concatenation is an operation defined between two classes A and B, or a
class A and a block C, and results in formation of a new class or block.
Concatenation consists in a merging of the attributes of both components,
and the composition of their actions. The formal parameters of the con-
catenated object consist of the foimal parameters of the first component
followed by formal parameters of the second component; and the same for
specification parts, declarations and statements (if any).
·A concatenated class B is defined by prefixing the name of a first com-
ponent A to the declaration of B:
A class B(h 1 , h 2 , ••• ); ••• specification of h's;
begin ... attributes of B . .. ; ... actions of B . .. end
Suppose the class A has been defined:
class A(a 1 , a 2 , ••• ); ••• specification of a's ... ;
begin ... attributes of A . .. ; ... actions of A . .. end.
According to the concatenation rules, the effect of the prefixed declaration
for class B is the same as if B had been declared without a prefix thus:
class B(a 1 , a 2 , ••• , h 1 , h 2 , ••• ); ••• specification of a's ...
specifications of h's . .. ;
begin ... attributes of A ... ; ... attributes of B . .. ;
... statements of A ... ; ... statements of B . .. end;
x. into(L), where z +-+ L. The result is z +-+ x +-+ L, which implies x == L. last.
If x was a list member, xis first removed from that list.
Any use of out, precede, or into not satisfying the above assumptions, is
either textually illegal or leads immediately to a run time error and program
termination caused by an invalid remote identifier. E.g. the operation
x. precede (y) sets x. pred to none if x = = y or y is not a list member.
Consequently the remote identifier pred. sue in the body of precede is invalid.
Notice that x. into (L) is a "safer" operation, since x e link, LE list implies
that x =/= Land L.pred =/=none.
The assertions (l) and (2) provide a guarantee that our lists are well
behaved, provided that no explicit assignment to any variable sue or pred
occurs. The construction TWLIST is thus a reliable "mental platform,"
which in a certain sense cannot break down, whatever programming errors
are made. When programming on top of TWLIST one is entitled to ignore
the list processing details involved in manipulating the circular two-way lists.
Each list object may be regarded as representing an ordered set of link
objects, with the proviso that a link object may be member of at most one
such set at a time. The last fact is reflected in the design of the procedures
into and.precede. Explicit use of the attributes sue and pred, e.g. for scanning
through a list, may, however, require the user to be conscious of the fact
that the "last" member has a successor and the "first" member a predecessor,
which are both identical to the list object itself. A design alternative is to
suppress this fact by declaring the following procedures as attributes to link.
ref (link) procedure successor;
inspect sue when list do successor: - none
otherwise successor: - sue;
ref (link) procedure predecessor;
inspect pred when list do predecessor: - none
otherwise predecessor: - pred ;
Note the construction
inspect r when C do ..•
enables the programmer to test whether the object referenced by r belongs
to one of its possible subclasses C.
7. CONCEPT HIERARCHIES
At the outset of a programming project there is a problem, more or less
precisely defined and understood in terms of certain problem oriented con-
cepts, and a programming language, perhaps a general purpose one, providing
some (machine oriented) basic concepts, hopefully precisely defined and com-
HIERARCHICAL PROGRAM STRUCTURES 209
pletely understood. There is a conceptual distance between the two, which
must be bridged by our piece of program. We may picture that distance as a
vertical one, the given programming language being the ground level.
Our difficulty in bridging such gaps is the fact that we have to work
sequentially on one simple part problem at a time, not always knowing in
advance whether they are the right problems.
In order to better overcome such difficulties we may build pyramids.
Unlike the Egyptian ones ours are either standing on their heads (bottom-up
construction) or hanging in the air (top-down construction). The construction
principle involved is best called abstraction; we concentrate on features
common to many phenomena, and we abstract away features too far remo\ied
from the conceptual level at which we are working. Thereby we have a
better chance of formulating concepts which are indeed useful at a later stage.
In the bottom-up case we start at the basic language level and construct
abstract concepts capable of capturing a variety of phenomena in some
problem area. In the top-down case [8, 9] we formulate the solution to a given
problem in terms of concepts, which are capable of being implemented (and
interpreted) in many ways, and which are perhaps not yet fully understood.
In either case system construction may consist of adding new layers of
pyramids (above or below) until the conceptual gap has finally been bridged.
Each such layer will correspond to a conceptual level of understanding.
For instance, given some problem which involves queueing phenomena,
we could take TWLIST of the preceding section as the first step of a bottom-up
construction. Then, for the remainder of the construction we are free to think
and express ourselves in terms of dynamic manipulation of ordered sets of
objects.
Layers of conceptual levels may be represented as a prefix sequence of
class declarations. For example, it is possible to construct a series of class
declarations, each one using the previous class as prefix
class C 1 ; •••• ;
C 1 class C 2 ; •••• ;
where start refers to the process with which the simulation starts, and finish
gives the time limit for the simulation. Any process requesting to be held
beyond this limit may be ignored. Presumably, the start process will activate
other processes to participate in the simulation.
We now proceed to implement the mechanism described above. It will be
implemented as a class MINISIM, which is intended to be used as a prefix
to a simulation main program. In order to take advantage of the two-way list
mechanism, the MINISIM class must be prefixed by TWLIST. This ensures
that TWLIST is also available in any simulation program which is prefixed
by MINISIM.
A class of objects which are to be capable of participating in a simulation
should be declared as a subclass of the "process" class. This will make
available to it the necessary time control and queueing mechanisms. Each
process must have the capability of inserting itself into a two-way list;
therefore the process class itself must be declared as a subclass of the class of
links.
Processes waiting for the elapse of their holding interval are held on a
unique two-way list known as the sequencing set (SQS). The processes are
ordered in accordance with decreasing reactivation times. A specially created
finish process is always the first link in SQS, and the last link is always the
one that is currently active. Its time represents the current time of the
system. When it goes inactive, its predecessor in the SQS wil,. (usually)
become the last, and its local time has already been updated to the time at
which that process was due to be reactivated.
We are now in a position to give the general structure of MINISIM,
omitting for the time being the procedure bodies.
TWLIST class MINISIM
begin ref (list) SQS;
ref (process) procedure current;
current: - SQS.last;
link class process;
begin real time ;
procedure hold (T); real T;
..... '
procedure wait (Q); ref (list) Q;
hold:
begin ref (process) P;
P:- pred;
comment the holding process is necessarily active, and
therefore also the last member of SQS. Since the
finish process never holds, there will always be a
second-to-last process on SQS
if T > 0 then time:= time+ T;
comment set local reactivation time,
time should never decrease;
if time ;;:i: P. time then
begin comment this process must be moved in SQS;
out; comment of SQS, now Pis current;
P:- SQS.first; comment the finish process;
if time < P. time then
begin comment reactivation time is
within the time limit;
while time < P. time do P: - P. sue;
comment terminates since
time ;;:i: current. time;
precede (P)
end; comment ignore a process that would
exceed the limit;
resume (current)
end;
end of hold;
Notice that a process object is allowed to relinquish control simply by
saying detach or by passage through its end. In both cases control returns to
the standard sequencing mechanism of the simulate procedure. The basic
activation instructions call and resume, however, should not be explicitly
applied to process objects; that would illegally bypass the timing mechanism.
for each order to be generated. Each value T defines the arrival time of the
order. It is assumed that the T values are in a non-decreasing sequence.
The JOB SHOP goes as follows.
1. begin integer nmg; nmg: = inint;
2. MINISIM begin
3. class machine group (nm); integer nm;
4. begin ref (list) Q;
5. procedure request;
6. begin nm:= nm - 1; if nm < 0 then current. wait (Q) end;
7. procedure release ;
8. begin nm: = nm + I ; if nm ~ 0 then current. activate (Q. first) end;
9. Q: - new list
10. end of machine group;
11. ref(machine group) array mgroup [l:nmg];
12. process class order (n); integer n;
13. begin integer array mg[l :n]; array pt[l :n]; integers;
14. ref (machine group) M;
15. hold (inreal-time); comment arrival time is now;
16. for s: = 1 step l until n do
17. begin mg[s]: = inint; pt[s]: = inreal end;
18. if 1lastitem then activate (new order (inint)):
19. comment generate next order, if any;
20. for s: = l step 1 until n do
21. begin M:- mgroup [mg[s]]; M.request; hold (ptfs]);
M. release end
22. end of order;
23. integer k; real lim; lim: = inreal;
24. fork:= 1step1 until nmg do mgroup [k]: - new machine group (inint);
25. simulate (new order (inint), Jim);
26. comment initial time is zero by default;
27. end of program;
HIERARCIIlCAL PROGRAM STRUCTURFS 219
The model above should be augmented by mechanisms for observing its
performance. We may for instance very easily include a "reporter" process,
which will operate in "parallel" with the model components and give output
of relevant state information at regular simulated time intervals.
process class reporter (dt); real dt;
while true do
begin hold (dt);
give output, e.g. of
mgroup [k].nm v (k = 1, 2, ... ,nmg)
end of reporter;
The first order could generate a reporter object and set it going at system
time zero.
activate (new reporter (inreal))
Output will then be given at system time t, 2t, 3t, .•• , where t is the actual
parameter value.
As a further example we may wish to accumulate for each machine group a
histogram of waiting times of orders at the group. Then define the following
subclass of machine group, redefining the operation "request".
machine group class Machine Group;
begin ref {histogram) H;
procedure request;
begin real T;
T: = time; nm: = nm - 1;
if nm < 0 then wait (Q);
H. tabulate (time - T)
end of new request;
H: - new histogram (X, N)
end of Machine Group;
It is assumed that "histogram" is the class defined in section 3.1, and
that array X[l : N] and integer N are nonlocal quantities. Now replace the
lower case initials by upper case in the class identifier of lines 11, 14, and 24.
Then all machine groups will be objects extended as above, and since the
qualification of the reference variable M is strengthened, the "request" of
line 21 refers to the new procedure. Thus a histogram of waiting times will be
accumulated for each group.
Finally it should be mentioned that the "machine group" concept might
have considerable utility as a general purpose synchronisation mechanism for
220 OLE-JOHAN DAHL AND C. A. R. HOARE
REFERENCES
(1) Naur, P. (ed.) (1962/63). Revised Report on the Algorithmic Language.
ALGOL 60. Comp. J., 5, pp. 349-367.
(2) Dahl, 0.-J., Myhrhaug, B., Nygaard, K. (1968). The Simular 67 Common
Base Language. Norwegian Computing Centre, Forskningsveien IB, Oslo 3.
(3) Wang, A., Dahl, 0.-J. (1971). Coroutine Sequencing in a Block Structured
Environment. BIT 11, 4, pp. 425-449.
(4) Dahl, 0.-J., Nygaard (1966). Simula-an Algol-Based Simulation Language.
Comm. A.C.M. 9, 9, pp. 671-678.
(5) Dahl, 0.-J. (1968). Discrete Event Simulation Languages. "Programming
Languages" (ed. Genuys, F.). pp. 349-395. Academic Press, London.
(6) Hoare, C. A. R. (1968). Record Handling. "Programming Languages" (ed.
Genuys, F.). pp. 291-347. Academic Press, London.
(7) Conway, M. E. (1963). Design ofa Separable Transition-Diagram Compiler.
Comm. A.C.M. 6, 7, pp. 396--408.
(8) Naur, P. (1969). Programming by Actions Clusters. BIT 9, 3, pp. 250-258.
(9) Dijkstra, E. W. (1972). Notes on Structured Programming. "Structured
Programming". pp. 1-82. Academic Press, London.
(10) Knuth, D. E., McNeley, J. L. (1964). SOL-A Symbolic Language for
General-Purpose Systems Simulation. IEEE Trans. E.C.
(11) IBM, General Purpose Systems Simulator.
(12) Dijkstra, E. W. (1968). Co-operating Sequential Processes. "Programming
Languages". pp. 43-112. Academic Press, London.