What are abstract data types?
An abstract data type is a collection of values and operations on those values, considered without reference to how such values might be represented or how such operations might be implemented. The implementation is what is being taken away or eliminated by abstraction.
When dealing with abstract data types, do we or do we not need to consider implementation? And what isn't an abstract data type? It seems like they all fit this mold...
The term `abstract' is not used to classify data types -- it's not that
some of them are abstract and others are concrete. An abstract data type
is one that is being considered in a particular way -- without reference to
its representation, without considering how it would be implemented. In
the handout on characters, it looks at first glance as if I'm going over
the same ground twice -- what's the difference between the operation
upcase (as specified in the first half of the handout) and the
Pascal function Upcase (as defined in the second half)? The
answer is that in the first half of the handout, I'm considering
characters as an abstract data type -- just the values and the possible
operations on them -- and in the second half I'm showing how to implement
this type fully in Pascal (at which point it's no longer an abstract data
type, because I'm no longer considering it without reference to its
representation).
Considering the data type first as an abstraction is actually a helpful programming technique, because it enables you to think about and design a selection of operations that reflects the nature of the type itself. Unless instructed to adopt this approach, most programmers ``design'' whatever they think will be easiest to implement, which is a good short-term strategy for building small programs but fails badly in the longer run -- irrelevant and accidental characteristics of a particular programming language, operating system, or networking environment show through too much.
I understand how the computer tells which integer, character, or real number it is reading. What I don't understand is how the computer knows if it is reading a character, an integer, or a real. Is there some part of a word that identifies what data type it is?
In some programming languages, such as Scheme, it is necessary for each value that is stored in memory to be tagged in the way you suggested with an indication of the type to which it belongs. Such tags are not needed in Pascal, since all the necessary type information is already available when the program is compiled.
When translating a call to the Read or ReadLn
procedure, the Pascal compiler examines each variable for which a value
must be read in and infers the type of that variable from its declaration.
(Since every Pascal variable must be declared before it is used, the
compiler will already have seen the declaration and stored the variable's
type in a ``symbol table'' that it builds and maintains during
compilation.) The compiler then generates different machine instructions,
depending on whether the program calls for the reading of a
Char, Integer, or Real value.
In other words, although it appears to the Pascal programmer that there is
only one Read procedure, there are actually three different
ones, and the compiler decides which one to use for each value that is read
by inspecting the type of the variable in which the value will be
placed.
Where does the computer get the ability to distinguish different data types?
The computer has no such ability. If the programmer stores a value of type
Real in a particular location in memory, and then lies to the
computer by telling it that there is a value of type Integer
stored there, the computer will happily interpret the pattern of zeroes and
ones that occupies that storage location as if it were an integer.
Pascal makes it rather difficult for the programmer to tell such a lie (and specifies that it is an error for him to do so). In some programming languages, it is completely impossible to express the lie; in others, it is extremely easy and indeed routine -- the programmer is simply made responsible for the consequences of lying to the computer.
Why is Pascal so strictly typed? Having learned Scheme two years ago, I had grown accustomed to being able to put nearly whatever I wanted into a variable, etc. I miss that flexibility, and though I recognize that most languages cannot be so dynamic, Pascal seems to taking typing to an extreme.
There are three main reasons:
For global variables, the compiler simply starts at 0 and allocates
successive locations for successive variables. Often, as on the HPs, a few
locations may be skipped in order to allow advantageous alignments; for
instance, if the address of the next available location is 390, a variable
of type Integer might be given the address 392, leaving bytes
390 and 391 unused, because an integer can be transferred more rapidly from
the memory to a register in the central processing unit if its address is
divisible by 4.
When the compiled program is executed, the global ``addresses'' generated by the compiler are actually interpreted as offsets from a base address established by the operating system.
The addresses of parameters and local variables are handled similarly, except that their ``base addresses'' are established during program execution by the Pascal run-time system rather than by the operating system.
Since the character digit-zero is represented by the bit-pattern 00011000 in ASCII, and the integer 48 is also 00011000, and the forty-ninth component of an enumerated type is also 00011000, how can the computer distiguish one from another?
Actually, an integer value requires thirty-two bits under HP Pascal, so the representation of the integer 48 is actually 00000000000000000000000000011000. But in general your question is a good one: If values of different data types have the same bit-pattern and occupy the same amount of storage, how can the computer tell them apart?
The answer is that it cannot. If the programmer stores the forty-ninth
value of an enumerated type in a particular location in memory, and then
lies to the computer by telling it that there is a value of type
Char stored there, the computer will happily interpret the
pattern of zeroes and ones that occupies that storage location as
digit-zero.
Pascal makes it rather difficult for the programmer to tell such a lie (and specifies that it is an error for him to do so). In some programming languages, it is completely impossible to express the lie; in others, it is extremely easy and indeed routine -- the programmer is simply made responsible for the consequences of lying to the computer.
Pascal, you have said in class, makes it difficult for the programmer to examine data as types other than Pascal thinks they are. An integer cannot be read as a real, or a char. Why is this worth mentioning? Why would somebody want to look at data as if it were something it's not?
Because sometimes it's more efficient to bypass the interface that the
designer of the data type set up. For instance, suppose that you want to
know whether the value of the Integer variable
Position is evenly divisible by 4. The designer of Pascal's
Integer data type wants you to write
Position mod 4 = 0to find this out. This involves doing a division, which is the most time-consuming of the arithmetic operations. If you happen to know that on HPs an integer is divisible by 4 if, and only if, bits 1 and 0 of its internal representation are ``off'' -- zeroes -- and if your programming language allows you to test these bits using machine instructions that are faster than the division operation, you will be able to speed up your program by looking at the integer as a sequence of bits instead.
Another example: Suppose that you have determined that the value of the
variable Ch, of type Char, is a lower-case
letter, and you want to replace it with the corresponding capital letter.
In Pascal, you write something like
Distance := Ord ('a') - Ord ('A');
Ch := Chr (Ord (Ch) - Distance)
for this purpose. The C programming language encourages programmers to
perform arithmetic directly on characters, as if they were integers. If
you could do this in Pascal, you would write
Distance := 'a' - 'A'; Ch := Ch - DistanceWhich is better? A good compiler would generate the same machine instructions in either case. The Pascal version is perhaps clearer but more cumbersome.
I was talking with my friend Greg from Madison, and the issue of Pascal's strong typing came up. I remembered you talking about how hard it is to get a sorting procedure to get more than one type of input, and Greg had an idea of how to solve it. He thought of a function that could be placed which would help with the process. Before the sorting procedure is called, a function stores the data, be it chars or reals, into a binary file of that type. The function would as need to take in a symbol of what type the binary file contained. Then the function would have a case statement that would call the sorting procedure with the correct type, adding a code to tell it how to unravel it. I would maybe try to write this, and want your opinion on whether it is possible, or even worth the time it would take as a constant before sorting.
Let's see. You could write data of any type into a binary file, then have the operating system attach the same file to a different Pascal variable that would treat the data in the file as being of some neutral type, perhaps an array of bytes, distinguished only by its length. You could read the file into an array of objects of the neutral type, sort it, and put it back in the file, then have the operating system reattach the file to the original Pascal file and read the sorted data back in from it.
This could work. In the middle of the process, you're lying to Pascal about the nature of the data being sorted, but Pascal will never know the difference. A similar approach along the same lines, which would be more efficient because it would avoid the need for file operations, would be to define a variant record type with two variants, one the original data type, the other the byte array of the appropriate length. Store the data in the first place using the first variant; call a sorting procedure in which the data is treated as being of the type of the second variant; output the data using the first variant again.
In HP Pascal, still another possibility is available whenever the data
values are accessed through pointers. (As you may have inferred from the
modules we've recently studied, this is often the case in real Pascal
programs.) By declaring the parameter of a sorting method to be an array
of values of HP Pascal's LocalAnyPtr type, one can use the
sort with any array of pointers, regardless of their real base type.
Should I worry about remembering the difference between the types of parentheses and brackets in BNF grammar if I am comfortable with syntax diagrams?
If you can find an accurate syntax diagram for every syntactic construction that you're interested in, and if you think that such diagrams are more readable than BNF productions, then you don't need to be too concerned about the mechanics of BNF. Besides, the particular conventions that authors use to write BNF vary, so that every time you run across a BNF grammar you may have to study the context in order to figure out exactly what the parentheses, brackets, and braces mean.
However, the conventions that Cooper and Walker use aren't all that hard to learn: Parentheses are solely for grouping alternatives. Brackets enclose optional constituents. Braces enclose constituents that can be repeated 0 or more times.
To put it in terms of syntax diagrams: Parentheses disappear in syntax diagrams. Brackets correspond to temporary forks in the track, one line containing the optional construction, the other one empty; the two lines merge again afterwards. Braces correspond to loops in the track; one can go past the loop on the main track, or around the loop any number of times before proceeding.
How important is it to understand both syntax diagrams and BNF grammars? Is BNF more widely used?
BNF is far more widely used than syntax diagrams, mainly because it's easier to store BNF grammars in text files and to write programs that will read, parse, and do useful things with them.
Why does Pascal require you to compile a program before you run it? I learned to program in BASIC, and it translated as it ran. Is it that much more efficient to compile it beforehand? Or is it done to catch errors before the program is run?
Efficiency is the main reason. There used to be several Pascal processors that used interpretation rather than compilation; they would translate from Pascal to an intermediate ``P-code,'' which the user would then execute on a ``Pascal virtual machine.'' The Pascal virtual machine was a piece of software that read and interpreted P-code. The whole process was similar to the process of saving and later running a BASIC program on, say, an Apple II; BASIC programs on the Apple II were stored in a reduced form that was interpreted by the RUN command.
The translation part of the Pascal interpreter ran faster than a full compilation; the difficulty was that the Pascal virtual machine was usually very slow, perhaps five to ten times slower than compiled code. The interpreter was therefore used primarily during program development. The finished version would be run through the compiler before being released.
Pascal interpreters have now almost disappeared, because Pascal compilers are so much faster on modern computers. When translation of a thousand-line program took ninety seconds under pi (a P-code generator) and five minutes under pc, it was worth while for a student pressed for time to use pi. But when pi takes a second and a half and pc takes five seconds, what's the point?
Is it better in general to write a long program that uses lots of very simple procedures from a library or to do the same job with a short program containing very efficient but very specialized procedures?
Usually it is better to write the first version of the program with general procedures that are already available. If this first version is too inefficient or clumsy, you can rewrite it later to use more specific procedures, but in most cases rewriting is superfluous -- the first version works well enough.
When describing a procedure should I mention procedures it calls?
Only if it simpifies or clarifies the description of the current procedure. It's a question of style, not correctness; but in general the description will better reflect the modularity of the procedure if you avoid gratuitous references to other procedures.
If procedure A calls procedure B, and the
definition of procedure B is completely nested within the
definition of procedure A, then there's a somewhat sounder
argument for mentioning procedure B in the opening comment for
A; but even in this case it is more usual for the relevant
facts to be placed in the comment at the beginning of the definition of
procedure B instead.
Suppose in my program I have two layers of procedures. The first layer is
Procedure1 and Procedure2. The second layer is
Procedure11 and Procedure12,
both of which belong to Procedure1 and have nothing to do with
Procedure2. There is a data type that I want to use in
Procedure1 and as the type of a parameter of
Procedure11. This data type has nothing to do with
Procedure2 or Procedure12. Do you think I can
just declare this data type in Procedure1?
That's the ideal place for it. In general, if you have any identifier that is used only inside a procedure, it should be defined or declared inside that procedure.
Variable names give me trouble, especially as these programs grow longer
and more complex. Is there a set of guidelines that we might want to
follow? I miss the ability to give variables names like
player_r for player record -- somehow playerr
doesn't do it, and PlayerRecord is too many characters.
HP Pascal allows you to use the underscore as a break character, as you prefer. And it's fairly easy to write a utility that will strip all the underscores out of a program when you're ready to port it to some less tolerant implementation of Pascal.
In general, global identifiers should be very explicit, even at the expense of more typing; an abbreviation is more confusing if the point at which it is used is far away from the point at which it is defined, as often happens with globals. If you're going to use a short, cryptic identifier, be sure to attach an explanatory comment at the point where it is defined.
How important are variable names? I have noticed that some programmers use very elaborate variable names, making the program easier to decipher yet a lot messier and harder to read, while other programs I have seen in books are written with very basic variable names, often just letters. Do you feel it is far better to use elaborate variable names? If so, do you tend to take points off for programs that don't use elaborate names?
Choosing meaningful identifiers is a fundamental part of good programming style. Often I have found that an author's poor programming style undermines an otherwise excellent textbook -- and the most common error in such cases is the use of cryptic identifiers, often arbitrarily selected single letters. A case in point is Robert Sedgewick's Algorithms, which we've tried to use twice in different courses in the department, because the prose explanations are so good. Both times, the students found the accompanying source code unintelligible -- useless at worst, unhelpful at best..
The original cause of the problem is that the first programming language of many programmers was either FORTRAN or some rudimentary form of BASIC. In FORTRAN, it's a rule of the language that no identifier can be more than six characters long. Also, many programs directly reflect the mathematical notation in which the problems they solve are specified; since mathematicians use single letters as variables, FORTRAN programmers tend to do so as well. In some dialects of BASIC, including the one I first learned, the interpreter was capable of recognizing an identifier only if it was either a single letter or a single letter followed by a digit. Such restrictive programming languages produced a generation of programmers with the bad habit of using single-letter identifiers for everything.
The rule that I go by is that every identifier should be self-explanatory unless it is used only within a few lines of its point of definition. This means that I tend to use long identifiers in global definitions and declarations but sometimes use short ones locally. I also avoid abbreviations and acronyms; too often they mystify the reader.
However, I generally take points off for this error of style only if it makes it more difficult for me to understand a student's paper. Usually this happens only in combination with other stylistic errors (insufficient documentation, over-long procedures, irregular indentation, etc.).
After every occurrence of end, should I indicate in braces
what is ending, thus:
end {for loop}
It's up to you. A lot of programmers seem to find comments of this kind helpful, especially when control structures are deeply nested. I don't object to them, but I don't find them particularly helpful either, because I'm very careful and regular about indentation and can almost always match up the beginning and end of a compound statement by observing which lines are indented to the same column.
Do you have any general guidelines for comments in source code we turn in? (My high school CS teacher had a very standard format that she demanded we follow, so I was wondering if you had a similar policy.) My main concern is that I might be overdocumenting my source code.
It is practically impossible to overdocument source code, though it is possible to document it too mechanically, without giving the comments enough thought. (The usual symptom of this is comments that are redundant, repeating information that is immediately obvious from the code itself rather than explaining the purpose of a variable or a procedure or the rationale for a programming choice.) Quality is more important than volume.
I have two habits that I recomment to students: (1) I write a long opening comment at the beginning of each program, in which I describe the problem that the program is supposed to solve and the general approach to a solution that is used in the program. (2) I also write a one- or two-line comment on every definition and declaration in the program, explaining what the identifier that is being defined is for. As a result, I seldom have to include comments in the executable part of a program, procedure, or function.
For instance, I've finished my solution to exercise 1. It begins with a comment of forty lines, and after this opening comment about 42% of the subsequent lines are inside comments.
When does it become appropriate to use HP Pascal's Assert
function to enforce preconditions?
Please use it as soon as you have learned its syntax and the meanings of its parameters, which are discussed in chapter 9 of the HP Pascal / HP-UX reference manual.
My question is about the Assert function and the exception
codes declared in the sequence module. (Or any of the modules in your
handouts, for that matter.) You declare an ExceptionException
constant, but the only place this enters into the code is to check to see
if the exception code generated is valid. If you only call the
Assert function with valid codes, why is it necessary to check
the incoming exception code? Is it just a design principle? Would you
ever expect to crash out of the program with this exception code? What
could cause the program to crash with this error?
In principle, it's not necessary to check the incoming exception code.
Since the SequenceExceptionHandler procedure is not exported,
one can see all the places at which it can possibly be invoked just by
reading through the implement section of the module in which
it is defined. In each of these places, it is invoked with a valid
exception code. This means that the program cannot possibly generate the
ExceptionException error and that, in principle, it is
pointless to provide for such an error.
The problem with this principle is that it is too fragile; it wouldn't take
much of a change in the module to break it, and it depends on the
``voluntary cooperation'' of a lot of separate procedures and functions
that may in the future be rewritten, by me, by you, or by someone who
borrows the code from the WWW site. Providing the
ExceptionException is a kind of defensive programming --
overdesigning the software to compensate for the fact that very frequently
during the execution of real-world programs things that ``can't happen''
happen.
I'm having trouble writing Assert calls in my own programs.
From a defensive-programming standpoint, would you prefer to see an
Assert call in the following procedure block, or not?
procedure InsertIntoTree(var Tree : TreePtr;
NewInfo : DataType);
procedure InitEntry(var Ptr : PtrType;
Data : DataType);
begin
Ptr^.Info:=Data;
Ptr^.Left:=NIL;
Ptr^.Right:=NIL;
end;
begin
if Empty(Tree) then begin
new(Tree);
InitEntry(Tree);
end
else
if LessThan(Tree^.Info,Data) then
InsertIntoTree(Tree^.Left)
else InsertIntoTree(Tree^.Right);
end;
Since InitEntry is a local procedure to
InsertIntoTree, it can only be called from
InsertIntoTree, which properly checks the implicit
precondition that Ptr is not NIL. Is there a
need for an Assert statement? On one hand, dereferencing a
pointer should always be checked by an Assert procedure, but
if InitEntry is only called when its preconditions have
already been checked and cannot be called from another procedure, is there
really a purpose in providing an Assert call, other than
perhaps somebody who doesn't understand the library will come and revise
it, and not properly check the precondition before invoking
InitEntry? Would you recommend checking preconditions anyway,
just in case I were to go and add to the library later on and forget to be
careful?
No, in this case I don't see the need for a call to Assert. I
might feel slightly differently if the call to InitEntry were
located farther way from the definition of that procedure, but in this case
it's easy to detect the relationship between the two procedures.
However, your question illustrates a curious phenomenon in programming:
When you're not sure whether an assertion is needed, or (in other cases)
when you try to write an assertion and find that it's very cumbersome, it's
often a sign that you're trying to do the wrong thing -- usually, that
you're trying to modularize the program incorrectly. In the particular
case you cite, you can avoid the whole problem by moving the call to
New into the InitEntry procedure where it
belongs:
procedure InsertIntoTree (var Tree: TreePtr; NewInfo: DataType);
procedure InitEntry (var Ptr: PtrType; Data: DataType);
begin
New (Ptr);
Ptr^.Info := Data;
Ptr^.Left := nil;
Ptr^.Right := nil
end;
begin
if Empty (Tree) then
InitEntry (Tree, NewInfo)
else if LessThan (Tree^.Info, NewInfo) then
InsertIntoTree (Tree^.Left, NewInfo)
else
InsertIntoTree (Tree^.Right, NewInfo)
end;
In the code for Walker's procedure to print the information for a
baseball player, he declares Ind as a variable parameter.
Since the procedure is printing (and thus not changing) information, what
is the rationale for declaring Ind as a variable
parameter?It makes the mechanism for the procedure call more efficient. When a large data structure is passed by value, it must be copied into the storage allocated for the parameter; this copying process takes an amount of type proportional to the size of the structure. When the same data structure is passed by reference, only its address is actually copied into the storage set aside for the procedure. Since addresses are small and have a fixed size, this mechanism is faster.
I am not too clear on the distinction between an error and a violation. Page 1 of Standard Pascal makes them sound like one and the same, and page 100 draws a distinction between them.
There are two kinds of violations of the rules of standard Pascal: those that can be detected by inspecting the text of the alleged program, without trying to execute any of it, and those that can only be detected by executing part or all of the alleged program. Violations of the latter kind are errors; violations of the first kind don't have a separate name, but when, on page 100, Cooper contrasts errors with ``violations,'' he means violations of the first kind.
A standard Pascal processor is required to detect and report violations of the first kind. It is not required to detect all errors if its documentation lists the classes of errors that that it does not detect.
Could you go over the difference between implementation-defined and implementation-dependent?
Sure. First, here's what the standard says:
3.3. Implementation-Defined. Possibly differing between processors, but defined for any particular processor.For example, HP Pascal provides both3.4. Implementation-Dependent. Possibly differing between processors and not necessarily defined for any particular processor.
MaxInt and
MinInt; these are pre-defined constants, the greatest and the
least values of the Integer data type. MaxInt is
implementation-defined: Every standard Pascal processor must recognize
this identifier as a constant, but it may denote different values in
different Pascal systems. MinInt is implementation-dependent:
Some standard Pascal processors will not pre-define it, and those that do
may give it different values.Are we allowed to use the "non-ANSI-Pascal" features of pc for our programs? In other words, can we invoke pc without the -A option which comes defined with pgo in the standard MathLAN account?
Yes. For example, the Pascal standard says that it's the job of the
operating system to attach files for input and output to the relevant
Pascal file variables. Consequently, the Reset and
Rewrite procedures in standard Pascal take only one argument.
But the designers of HP Pascal instead left it up to the programmer to
attach such files, and hence provided two-argument versions of these
procedures:
Reset (Source, '/users/spelvin/frogs.dat'); Rewrite (Target, 'frogs.out');In this case, I encourage you to use the two-argument forms even though they are non-standard. It is practically impossible to ensure the portability of calls to
Reset and Rewrite anyway,
since there is so much variety in the mechanisms that are used to attach
files to Pascal file variables.Is it possible, using the HP compiler, to nest comments?
No. Nesting of comments is contrary to the standard (see Cooper, p. 7). Some implementations of Pascal allow the nesting of comments, but this is a mistake.
The following test program can be used to determine whether a given implementation of Pascal allows nesting of comments:
program Comments (Output);
const
{ { }
Nestable = False;
First = '} Nestable = True; {';
Second = '} Third = '''{' { };
begin
WriteLn ('It is ', Nestable, ' that comments are nestable.')
end.
Under HP Pascal, this program produces the output
It is FALSE that comments are nestable.I'm a little confused about program parameters. I know that
Input and Output are necessary to read from the
keyboard and write to the screen. It seems that any other parameters are
somewhat superfluous, since you have to redefine them in the VAR section of
the program block anyway.In the original implementation of Pascal, the parameters in the program header were supposed to correspond to command-line arguments. This requirement was abandoned before the language was standardized, since Pascal was implemented under some operating systems that either had no notion of a command-line argument or did not provide easy access to them, but many implementations of Pascal continue to make some special use of the program parameters, so simply deleting them from the language would invalidate a lot of Pascal code.
Would it be possible to invoke a program within the program, like a procedure within the procedure, thus allowing us to make programs themselves recursive?
The only way to do this, within Pascal's syntax, is to move the body of
your main program into a Control procedure, replacing it with
a single-statement program body that is a call to this procedure, and then
to invoke Control recursively when you want to ``invoke the
program.'' There is no way to re-invoke the main program body from within
a procedure or function definition in Pascal.
Is there a random number generator in HP Pascal? I couldn't find any mention of it in the LaserROM manual.
No, there isn't. Here's a canned one that I can recommend:
var
RandomSeed: Integer;
{ the current value in the sequence of integers produced by the
generator; this variable must be initialized so as to provide a
starting point from which to develop values }
{ The Randomize procedure sets the value of RandomSeed to an initial
value that depends on its parameter. }
procedure Randomize (Offering: Integer);
begin
RandomSeed := 1 + Offering mod MaxInt
end;
{ The Random function uses the linear-congruential method to generate a
pseudo-random value in the range (0.0, 1.0]. The particular generator
mentioned here is proposed as a standard by Stephen K. Park and Keith
W. Miller, in ``Random number generators: Good ones are hard to find,''
COMMUNICATIONS OF THE ACM 31, 1192--1201. To avoid integer overflow, the
modulus is separated into two parts, Quotient and Remainder, such that
Modulus = Quotient * Multiplier + Remainder, and similarly RandomSeed is
separated into a high segment and a low segment such that RandomSeed =
Quotient * HighSegment + LowSegment. The new value to be computed is
then
Multiplier * RandomSeed mod Modulus
= (Multiplier * Quotient * HighSegment + Multiplier * LowSegment)
mod (Multiplier * Quotient + Remainder)
= (Multiplier * Quotient * HighSegment + Remainder * HighSegment
+ Multiplier * LowSegment - Remainder * HighSegment)
mod (Multiplier * Quotient + Remainder)
= (Multiplier * LowSegment - Remainder * HighSegment)
mod (Multiplier * Quotient + Remainder)
which can be computed without overflow, since the highest possible value
of Multiplier * LowSegment - Remainder * HighSegment, even assuming a
HighSegment value of 0, is less than Multiplier * Quotient, which is less
than Modulus, which is equal to MaxInt, and the lowest possible value of
the same expression, even assuming a LowSegment value of zero, is
-Remainder * HighSegment, which is greater than -Multiplier * Quotient,
etc.
The article cited above recommends that 16807 be used as the value of
Multiplier, and consequently 127773 as the value of Quotient and 2836 as
the value of Remainder. In a subsequent note (in ``Technical
Correspondence,'' COMMUNICATIONS OF THE ACM 36, number 7, 108--110), Park
et al. suggest the multiplier 48271 instead. This change has been made
in the code below. }
function Random: Real;
const
Multiplier = 48271;
Modulus = 2147483647; { = 2^31 - 1 }
Quotient = 44488; { = Modulus div Multiplier }
Remainder = 3399; { = Modulus mod Multiplier }
var
HighSegment: Integer;
{ the number of times Quotient goes into RandomSeed }
LowSegment: Integer;
{ the remainder when RandomSeed is divided by Quotient }
Test: integer;
{ RandomSeed * Multiplier mod Modulus, possibly ``wrapped around'' to a
negative number that is off by Modulus }
begin
HighSegment := RandomSeed div Quotient;
LowSegment := RandomSeed mod Quotient;
Test := Multiplier * LowSegment - Remainder * HighSegment;
if 0 < Test then
RandomSeed := Test
else
RandomSeed := Test + Modulus;
Random := RandomSeed / Modulus
end;
Is there a relationship between the possible size of Random
and the size of the seed number?
Standard Pascal doesn't provide a Random function, or indeed
any kind of a random-number generator, so I'll take this as a question
about the random-number generator that Walker develops on pages 106 through
109 of the text.
The range of possible values returned by a Random function is
independent of the range of values of the seed, but the particular value
returned on any one call to Random is proportional to the
current value of the seed.
Could you explain big-O notation?
Take any two functions, f and g, that take positive integers as arguments and produce positive real numbers as values. The statement that f is of order g -- in symbols, f(n) = O(g(n)) -- means that, once the arguments are sufficiently large, the ratio between the values produced by f and those produced by g is bounded by a constant, so that in effect f grows no more rapidly that some fixed multiple of g.
Formally, f(n) = O(g(n)) is defined to mean that there is a positive integer m and a positive real c such that, for every integer n greater than or equal to m, f(n) <= cg(n).
In the context of the classification of algorithms, the function g that describes the order is some simple function like n^2 or lg n, and the function f is intended to characterize the running time of the algorithm as a function of the size of its input.
Is there a good way to evaluate average-case efficiencies of algorithms?
Sometimes. There is no universally applicable method for analyzing algorithms, any more than there is a single method for proving mathematical theorems. Often it is easier to analyze the worst-case efficiency of an algorithm than to analyze its average-case efficiency; it may even be difficult to decide how to take an average for, say, a sorting algorithm: Should one assume that every initial permutation of the elements of an array is equally likely, or should one try to weight the average in favor of cases that might arise more frequently in practice?
In the bignum handout, you say that the division algorithm is proved correct. Is there a formalized method of proving the correctness of algorithms? It seems like something which mathematical inductive reasoning could apply to quite well. And if there is such a way, is it just too complicated to apply to windowing systems?
Yes, a lot of work has been done on methods for constructing formal proofs of correctness. Two good introductory books are A method of programming, by Edsger W. Dijkstra and W. H. J. Feijen (Reading, Massachusetts: Addison-Wesley Publishing Company, 1988), and The science of programming, by David Gries (New York: Springer-Verlag, 1981).
There are five reasons why correctness proofs are not routinely constructed for windowing systems:
No, but you can redirect the error messages to a file, and then use an
editor or a pager to inspect the file. To tell pc to store the
error messages in a file called errors, overwriting the previous
contents of that file (if any), add the redirection clause >&!
errors to the end of the command, thus:
pc -o myprogram myprogram.p >&! errorsBe sure to leave a space after the exclamation point.
What is a good way to track down run time errors on the HP's? My program compiles but won't run.
There's an interactive debugger named xdb. Here's how it works:
You use xdb when you have a program that is syntactically correct (the compiler can succeed in translating it) but semantically incorrect (it gives the wrong answers, at least part of the time). To prepare your program for use with xdb, you must compile it with the -g option:
pc -o frogs -g +N frogs.pThe -g option directs the compiler to provide the ``hooks'' that xdb requires.
Subsequently, you can start up xdb to debug an executable called frogs by typing
xdb frogsin an hpterm window.
When you activate xdb, it takes over your hpterm window and divides it into two subwindows, separated by a status line. The subwindow below the status line records your interactions with xdb; the one above the line displays part of the source code for the program that you're working on. The status line itself tells what file contains the part of the source code that is being displayed, what line of that file contains the statement that will be executed next, and what function that statement belongs to.
The text in the display subwindow cannot be edited; it passively exhibits the source code from which the executable was compiled. If you want to debug interactively, making changes in your code, recompiling, and reloading the debugger, you'll need at least two windows: Emacs to do the editing and recompiling, and xdb-in-hpterm to do the debugging. (To compile from within Emacs, click on the meshing-gears icon, third from the right on the toolbar, and type in the command that performs the compilation.)
You issue commands to xdb at the prompt, a greater-than sign, in the interaction subwindow. Output from the program is done by default by default in the same subwindow, which is sometimes confusing. To arrange for the program's output to be done in a different window, proceed as follows: Start up a different hpterm window and type tty at the hpterm prompt; you'll see a file name, something like /dev/tty/ttyp8. This is the operating system's way of identifying the window as a source of input and a receiver for output. Start xdb with the -o and -e options, specifying this file name after each one:
xdb -o /dev/tty/ttyp8 -e /dev/tty/ttyp8 frogsThis will direct xdb to connect frogs's Output and standard error facilities to the specified window. (You could even connect them to different windows if you prefer.)
There is also an -i option that can be used to connect stdin to a different window, but it's less satisfactory because it doesn't arrange for the input to be echoed -- you won't be able to see what you're typing.
Starting the program. The r command starts executing the program at the beginning. Execution continues until the program crashes or a breakpoint (see below) or the end of the program is reached. Unless you're trying to determine the point at which your program is crashing, you generally want to set some breakpoints before you type r. You can pass command-line arguments to your program by typing them after the r; subsequent uses of r provide the same command-line arguments automatically, unless you override them with new ones.
Executing the program one statement at a time. The s command executes exactly one statement in the program, the one on the line that is marked (with a greater-than sign) in the display subwindow. The S command does the same thing, except that it treats a function call as a single step, while s breaks the function down into its component statements. If you supply an integer after either s or S, the specified number of statements is executed as a group.
Viewing a different section of the source code. The v command adjusts the display so that a specified line of code is visible. An unsigned integer after v requests the line at that position in the currently displayed file. You can ask for a different file by typing the file name after the v, and for a particular line of that other file by attaching a colon and the line number after the file name. To move the display up or down by a specified number of lines, use the + command (down) or the - command (up). In either case, write the number of lines after the sign. The V command returns the display to the line that will be executed next.
Setting a breakpoint. To stop program execution at a specific point in the program, use the b command: b followed by a line number (or a file name, a colon, and a line number) marks that line as a breakpoint, so that execution stops every time that line is reached. The command bb sets a breakpoint at the beginning of the currently executing function. The command bp sets a breakpoint at the beginning of every function. The command lb displays a list of the breakpoints that have been set. The command db removes all those breakpoints; if followed by an integer, it removes only the breakpoint with that serial number (as shown by lb).
Continuing from a breakpoint. To resume program execution after a breakpoint, use the c command. (Alternatively, you could start over again with r, or advance cautiously a step at a time with s.)
Displaying the value of a variable. The p command prints out the value of a variable; type the variable after the p. The variable can be a simple identifier, an array reference, a structure or a field of a structure, or a dereferenced pointer expression. After you have displayed the value of an array element, the command p+ displays the next element of the array, and p- displays the previous element.
Changing the value of a variable. To modify the value of a variable, use the p command, but type an equal sign and the new value after the variable.
Exhibiting the run-time stack. The t command shows the names and parameter values of functions that have been invoked but have not yet exited. The T command shows the same information, along with the values of all local variables in each of those functions.
Exiting from xdb. The q command shuts down xdb in an orderly way.
Could you please explain what exactly a bus error is?
A bus is a communication pathway connecting two or more devices, such as the central processor and the memory of a computer. A bus error occurs when a program tries to use the bus without meeting the preconditions for its correct use. For example, the central processor recovers a value from a storage location in memory by sending its address to the memory unit by means of a bus. If it generates a bogus address -- one that does not denote any location in the actual memory -- it's possible for a bus error to occur. One common cause of such an error is dereferencing a pointer variable to which no value has ever been assigned; the random bit pattern in the pointer variable is then treated as an address, and such an address is very likely to be bogus.
Why is it that so often changing the order of seemingly unrelated statements will completely change the workings of a program?
Usually, the explanation is that each of the statements has side effects, and the effects of whichever statement comes first change the conditions under which the subsequent ones are executed.
I didn't understand the 'read' statement that was listed with Boolean functions. Can you, for instance, say 'IF READ (X) THEN ...'? If so, when would READ (X) return a false value? Could you please give me an example of this usage of 'read'?
In Pascal, you would implement the read operation for Booleans as a procedure with the header
procedure ReadBoolean (var Source: Text; var Legend: Boolean; var Success: Boolean);and you would invoke it in some such context as this:
Reset (Source, RosterFileName);
VoterNumber := 0;
while not EOF (Source) do begin
VoterNumber := VoterNumber + 1;
ReadString (Source, Roster[VoterNumber].Name);
ReadBoolean (Source, Roster[VoterNumber].Registered, Success);
if not Success then begin
WriteLn ('Error in line ', VoterNumber: 1, ' of source file:');
WriteLn ('Incorrectly formatted value in Registered field')
end
else { ... }
The error messages might appear if the character following the voter's name
in the roster file was neither T for true nor F
for false.The handout on characters says that by adding nine leading zeros to an ASCII code, one gets the equivalent Unicode character. Does this only cover the alphabet, or does it include the entire ASCII table?
It includes the entire 128-character ASCII set.
Does each Unicode script have its own punctuation marks or is there one common set which covers punctuation for all scripts?
Not all scripts use punctuation. Among those that do, a few have simply adopted the punctuation marks used in the Latin script, but most have their own punctuation marks.
It was mentioned that some manufacturers use 8-bit ASCII. Is there a standard for it?
There are various competing versions of eight-bit ASCII, some of which are arguably standard, but none has been accepted as widely as the seven-bit ASCII set.
Does ASCII have any way for dealing with accented letters, such as one would find in most European languages? If not, is there a European equivalent to ASCII or does each language pretty much have to create its own standard?
Various organizations and computer manufacturers have proposed extensions to ASCII in which the eighth bit in each byte is used to make it possible to represent 128 additional characters. Unfortunately, these schemes are not consistent either from language to language, or from country to country, or from manufacturer to manufacturer.
You mentioned that there is no standard for an 8-bit ASCII code. Is ISO-Latin-1 an attempt at a standard, or a stopgap, or just a commonly used option? Does this have anything to do with the general decay of standards recently (e.g. many companies introducing proprietary HTML tags)?
ISO-Latin-1 is a standard (``ISO'' is the International Standards Organization), just one that hasn't been accepted as widely as ASCII.
I don't perceive a ``general decay of standards'' -- it's always been difficult to get people to accept and observe them, even when it is demonstrably to everyone's advantage. In computing, at least, standards that have failed to achieve widespread acceptance are far more common than success stories like seven-bit ASCII. Most computer professionals share the cynical attitude expressed in Andrew Tenenbaum's dictum ``The nice thing about standards is that there are so many of them to choose from.''
Does Chr (0) have a standard glyph, or do we always refer
to it as Chr (0)?
There's no glyph for it in the Latin script. Some display devices show it as ^@, reflecting the fact that on many keyboards, including the HP's, you can generate it by pressing <Control/@> (that is, <Control/Shift/2>). Unicode calls it null, and the conventional ASCII abbreviation for it is NUL. I usually call it ``the null character.'' It's a control character, but on many devices it is implemented as having the interesting effect of doing absolutely nothing.
I don't doubt that Unicode is necessary for inclusiveness of the world's many languages, but do you think that it will be difficult to find what you want quickly with such a large set of characters? It seems a bit unwieldly to me.
The theory is that special-purpose editors or editing modes will be developed that facilitate the creation and processing of files of Unicode characters for users of particular scripts. You'll fire up your Tamil editor when you want to write in some language that uses Tamil, put Tamil keycaps on your keyboard, and proceed to type. The editor will look up and store the appropriate Unicode characters in the file that it creates.
What is the purpose of all of those little functions in the assigned handout? What do they demonstrate?
They show how to define names for the non-graphic ASCII characters in a
portable way. For instance, if you want to refer to the ASCII
escape character, the expression Escape is a lot
clearer than Chr (27); but you can't write
const
Escape = Chr (27);
in standard Pascal, because function calls are not allowed in constant
definitions. You could make Escape a variable of type
Char, but then you have to remember to initialize it before it
is used and never to change it. Writing a zero-argument function with the
name Escape allows you to write things like
if Source^ = Escape then Get (Source)just as if
Escape were a constant, but without violating the
Pascal standard.What is a negative acknowledge character used for?
It might be used in a system for sending data over a noisy line. After transmitting some fixed number of bytes, the sender inserts a checksum; the receiver also generates a checksum from the data as received and compares it with the sender's checksum. If they match, the receiver sends back the acknowledge character, and the sender proceeds to the next group of bytes; if they don't match, the receiver sends negative-acknowledge, and the sender repeats the same group of bytes.
On the HPs, does the <Enter> key return two nonprintable characters, like form-feed and carriage-return? I remember you saying different machines do different things when the <Enter> key is pressed.
Strictly speaking, the HP keyboard doesn't generate ASCII characters at all; the signals produced by pressing and releasing the keys are in a completely different code at the level of hardware. (For example, the <Enter> key on the typewriter-layout part of the keyboard and the <Enter> key on the numeric keypad have different hardware keycodes.) The software that mediates the keyboard's interactions with a running Pascal program converts hardware keycodes into ASCII characters.
The HP operating system uses the ASCII line-feed character as a line terminator when text files are stored. In displaying text on screen, the HP terminal emulator places the two-character sequence carriage-return line-feed at the end of each line -- the carriage-return to move the cursor to the left edge of the display and the line-feed to drop it down a line. Still other characters are generated if the text already displayed has to be scrolled upwards in order to make room for the new line.
How can I generate an ASCII form-feed character from the keyboard? How can I insert one into a text file?
From the keyboard, press <Control-L>. In XEmacs, press
<Control-Q> <Control-L>. You may want to write a small
Pascal program just to generate appropriate test data; to have Pascal write
a form-feed to a text file, use Page (DataFile) or
Write (DataFile, Chr (12)).
In the handout you say that Pascal can accommodate EBCDIC, which has gaps
between consecutive alphabetical letters. When you have a case such as
this, what happens to the Succ and Pred
functions? Do they just produce a garbage character?
Exactly. On an EBCDIC machine, for instance, Pred ('J') is
'}' (the right brace or right-curly-bracket).
Why does EBCDIC have gaps between letters? This seems silly to me.
Because the character codes were chosen to make the translation between the pattern of punches representing a character on a punched card and the bit pattern stored in memory as simple, straightforward, and efficient as possible.
A punched card of the era in which EBCDIC was designed could be punched in any of 960 positions, arranged in a rectangle of eighty columns and twelve rows. The rows were conventionally numbered (from top to bottom) as 12, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, and 9; usually the row numbers 0 through 9 were displayed at every punch position across the card, but rows 12 and 11 were left unprinted.
To store a character in one column of a punched card, one would punch out a particular combination of rectangular holes in that column. Digit-zero through digit-nine were represented by single punches in the correspondingly numbered column. Capital letters were represented by pairs of punches, one in row 12, 11, or 0 (at the top of the card) and the other in one of the rows 1 through 9. (Choosing rows that were far apart made it less likely that the card would be torn or otherwise damaged between punches in adjacent rows.) Capital-letter-a was 12-1 -- that is, holes were made in rows 12 and 1 of one column on the card to represent this letter. Capital-letter-b was 12-2, capital-letter-c was 12-3, and so on to capital-letter-i, which was 12-9. Then capital-letter-j was 11-1, capital-letter-k was 11-2, and so on to capital-letter-r, 11-9. Finally, capital-letter-s was 0-2 (avoiding 0-1 because they were adjacent rows), capital-letter-t was 0-3, and so on to capital-letter-z, 0-9.
When these punches were translated into EBCDIC character codes, the
designers decided to have the last four bits of the EBCDIC character
indicate which of the rows 1 through 9 was punched out, with
0001 for a punch in row 1, 0010 for a punch in
row 2, 0011 for a punch in row 3, and so on up to
1001 for a punch in row 9. The first four bits would be set
to 1100 for a punch in row 12, to 1101 (less
logically) for a punch in row 11, and to 1110 for a punch in
row 0. So the EBCDIC code for capital-letter-i is
11001001 -- 1100 for the 12-punch,
1001 for the 9-punch.
However, this leaves a numerical gap between capital-letter-i
(11001001) and capital-letter-j
(11010001), and another between capital-letter-r
(11011001) and capital-letter-s
(11100010). The designers of EBCDIC had a mildly plausible
scheme for filling in these positions with characters that had more exotic
punch combinations. For instance, right-curly-bracket was punched
(on some IBM card punches, anyway) as 11-0, so it was natural to put it
right before capital-letter-j (11-1).
This is probably more than you wanted to know. The real point is that the designers had a sensible motive that became obsolete when punched cards ceased to be a viable input medium.
I'm not quite clear on what the difference is between the letters being arranged in traditional alphabetical order and the letters being adjacent. How can they be in alphabetical order if they have other characters thrown in between?
In EBCDIC, the letters within either case (capitals or lower case) are in alphabetical order with respect to one another -- that is, if one letter precedes another alphabetically, then that letter precedes the other in EBCDIC as well. This is enough to guarantee that lexicographic sorting will work, provided that it involves only letters (of the same case).
If you wish to convert an ASCII character into a 7-bit binary representation, what is the most efficient method in Pascal? It seems to me, since Pascal is so isolated from the hardware and the operating system, that one would have to use a case statement:
case ch of: 'A' : write "1000001"; 'B' : write "1000010"; ...Is the most efficient way to accomplish this? And is it necessary to add the parity bit onto the representation--and if so, is the parity bit 0 or 7?
The parity bit is bit 7. Whether it's necessary to display the parity bit depends on what you're trying to achieve -- showing ASCII code equivalents or exhibiting the contents of memory.
The better way to write out the bitwise representation of an ASCII character is to exploit the fact that it can be deduced from the character's ordinal value:
procedure WriteCharAsBits (Ch: Char);
var
OrdinalValue: Integer;
Bits: packed array [0 .. 6] of Char;
BitNumber: Integer;
begin
OrdinalValue := Ord (Ch);
for BitNumber := 0 to 6 do begin
if Odd (OrdinalValue) then
Bits[BitNumber] := '1'
else
Bits[BitNumber] := '0';
OrdinalValue := OrdinalValue div 2
end;
for BitNumber := 6 downto 0 do
Write (Bits[BitNumber] : 1)
end;
Is it possible for me to read in whatever is entered from the keyboard as
a Char? If it is, then it seems like I can always avoid an
crash caused by entering data in a type which is different from what it
should be, by reading it in as characters and then transforming it into
the type we need (or sending out an error message, if it's not in the right
form). Is that right?
Yes, it is. The only reason for using Read and
ReadLn to read in values of any type other than
Char is that the transformation is sometimes rather difficult.
Recovering a value of type Real from its string representation
is especially tricky and requires a lot of thought about all the cases that
can arise.
What are the 32-bit binary representations of ASCII characters?
When a full thirty-two-bit word is set aside for an ASCII character -- for example, when it is loaded into a register -- usually bits 31 through 7 of the word are cleared (that is, turned off, made zero) and the seven bits of the ASCII character proper, as described in the handout on characters, are stored in bits 6 through 0.
I am curious about how EOLn and EOF are stored
in relation to one another in a text file that is ended with a carriage
return in comparison to one that is not.
The byte-by-byte format of a text file varies from one computer and
operating system to another; Pascal provides EOLn and
EOF functions precisely to impose a layer of abstraction
between the programmer and these differing implementations.
I'll give three examples of text file formats: the one the HPs use, the one the academic VAX uses, and the one that is used on IBM PCs and clones.
On the HPs, a text file is stored as an unstructured sequence of ASCII characters, and the ASCII line-feed is used as a line terminator. In this system, it is possible for a text file to end in the middle of a line: this simply means that the last character in the text file is something other than line-feed. There is no special character to signal the end of the file; instead, the operating system keeps track of the exact number of bytes stored in each file and refuses requests for additional characters after that number of bytes has been released.
On the VAX, a text file is stored as a sequence of ``line records,'' each beginning with a four-byte integer that indicates how many characters of text appear on the line. The text characters are then lined up after this initial count. No character acts as a line terminator or as a file terminator. It is impossible for a file to end in the middle of a line.
On PCs, a text file is stored as a sequence of ASCII characters, with the two-character sequence carriage-return, line-feed as a line terminator. When a text file is created, the bytes between the end of the file and the end of the storage block on the disk are all conventionally filled with the ASCII substitute character, and many text-file applications treat substitute as an end-of-file signal. In that case, a text file can in principle end in the middle of a line, if the last two characters preceding substitute are not carriage-return and line-feed.
Is there a way to peek at the next character of standard input? This
feature seems like it would have made the last assignment much easier to
code. A classmate told me that Input^ accomplishes this task,
but he we always encountered problems in his program when he tried to
utilize this feature. If you can peek at standard input, what are the
limitations/perils of doing that?
Your classmate is correct; Input^ returns the next character
from standard input, without actually advancing past it. (A subsequent
call to Read will still pick up that character.)
There are two main limitations of this method. (1) If input is arriving
from the keyboard, the evaluation of the expression Input^ may
``hang'' the program until the user actually types in the character that
has to be inspected in order for the expression's value to be determined.
(2) You cannot detect the end of the file by checking to see whether the
value of Input^ is the end-of-file character. There
is no end-of-file character in ASCII. (In an interactive Pascal
program under HP-UX, the user can signal the end of interactive input by
pressing <Control/D> at the beginning of a line, but there is
absolutely no way for the program to detect this character -- it is
stripped out before the input is submitted to the program. If input is
redirected so that it comes from a file, <Control/D> is not involved
at all; instead, HP-UX just keeps track of the number of characters in each
file and refuses to give the program a character when they have all been
read. So, in particular, <Control/D> is not an end-of-file
character.)
Do you think enumerated types are more trouble then they are worth?
No, I like enumerated types and believe that they prevent more trouble than they cause. The alternative, which is used in languages that have neither enumerated types nor symbol types, is to define a lot of numerical constants, like this:
const
Juggler = 0;
HighPriestess = 1;
Empress = 2;
{ ... }
Universe = 21;
type
MajorArcanum = Juggler .. Universe;
This is a little cumbersome, but the real problem with it is that one can
then do arithmetic on the values of the data type, and it's too easy to
start making mistakes by performing arithmetic operations that make no
sense.It is a lot of trouble to write input and output procedures for enumerated types, if one uses symbolic names for them, but one would have exactly the same trouble with the input and output of symbolic names if one used integer constants instead.
Is there an easier way to change the character '1' to the integer 1 than:
Ord('1') - 48 { 48 = Ord ('0') }
or, say, to change the string '12345' to the number 12345?No. But if you write a procedure to do this, it will come in very handy in a lot of the Pascal programs you write. Start building a library of useful procedures and functions that can be reused with little or no change in many programs.
What kind of process does Pascal use to translate a series of digits into an integer value?
It examines the digit characters one by one, from left to right. It
recovers the value of each digit character as indicated in the previous
question, by applying Ord to the digit character and
subtracting Ord ('0') from the result. It combines these
individual digit values by multiplying each one by the appropriate power of
ten. The basic loop looks roughly like this:
Value := 0;
while not AtEndOfNumeral do begin
Ch := NextCharacterOfNumeral;
Value := Value * 10 + (Ord (Ch) - Ord ('0'))
end;
That is: Adding one digit to the end of an integer involves multiplying its
previous value by ten and adding the digit.
The actual Read procedure is more complicated than this,
because it has to deal with leading spaces, the possibility of a sign
before the numeral, the possibility that the numeral will be greater than
MaxInt, and so on.
In the handout on numeration, how do the Evaluate and
Express procedures work?
Evaluate works by accumulating the numeric value of the part
of the numeral that it has so far inspected, processing each digit by
multiplying the previous value of the accumulator by the base of numeration
and adding in the numeric value of the new digit.
Express works by recovering the last digit of a given integer,
expressing it as a character to be placed at the end of the numeral, and
then using recursion to deal with an appropriately reduced integer that
contains all but the last digit of the original.
What are some of the practical applications of understanding machine data types, for programmers?
If you understand the machine representations of the data types, you know their limitations and can program around them. Here are two examples of what can go wrong if you don't, from Peter G. Neumann's book Computer-related risks (Reading, Massachusetts: Addison-Wesley Publishing Company, 1995), pages 34 and 169:
During the Persian Gulf war, the Patriot [anti-missile defense] system was initially touted as highly successful. In subsequent analyses, the estimates of its effectiveness were seriously downgraded, from about 95 percent to about 13 percent (or possibly less, according to MIT's Ted Postal; see SEN [Software Engineering Notes] 17, 2). The system had been designed to work under a much less stringent environment than that in which it was actually used in the war. The clock drift over a 100-hour period (which resulted in a tracking error of 678 meters) was blamed for the Patriot missing the [S]cud missile that hit an American military barracks in Dhahran, killing 29 and injuring 97. ... A later report stated that the software used two different and unequal versions of the number 0.1 -- in 24-bit and 48-bit representations (SEN 18, 1, 25). (To illustrate the discrepancy, the decimal number 0.1 has as an endlessly repeated binary representation 0.0001100110011.... Thus two different representations truncated at different lengths are not identical -- even in their floating-point representations.) ...Learning about machine representations makes it less likely that you will be the unfortunate programmer responsible for a mistake like these.In this section, we consider accidental financial mishaps ... One of the most dramatic examples was the $32 billion overdraft experienced by the Bank of New York (BoNY) as the result of the overflow of a 16-bit counter that went unchecked. (Most of the other counters were 32 bits wide.) BoNY was unable to process the incoming credits from security transfers, while the New York Federal Reserve automatically debited BoNY's cash account. BoNY had to borrow $24 billion to cover itself for 1 day (until the software was fixed), the interest on which was about $5 million. Many customers were also affected by the delayed transaction completions (SEN 11, 1, 3-7).
Could you explain how to multiply binary numbers and give an example?
Sure. The multiplication table is very easy, of course: 0 * 0 = 0, 0 * 1 = 0, 1 * 0 = 0, 1 * 1 = 1. If you're calculating on paper, you can set out the work just as if you were working in decimal numeration. For instance, to multiply 42 by 23, you could write
101010
x 10111
-------
101010
101010
101010
0
101010
----------
1111000110
The only hard part is to get the carries right when adding up a long series
of partial products.A computer can do much the same thing, except that it's likely to keep a running total of the partial products instead of adding them all up at the end. Here's how the algorithms might look if they were written out in Pascal:
const
WordSize = 32;
WordSizeMinusOne = 31;
type
Bit = 0 .. 1;
Word = packed array [0 .. WordSizeMinusOne] of Bit;
procedure MakeZero (var W: Word);
var
BitNumber: Integer;
begin
for BitNumber := 0 to WordSizeMinusOne do
W[BitNumber] := 0
end;
procedure Add (Augend, Addend: Word; var Sum: Word);
var
BitNumber: Integer;
Carry: Bit;
ColumnSum: Integer;
begin
Carry := 0;
for BitNumber := 0 to WordSizeMinusOne do begin
ColumnSum := Augend[BitNumber] + Addend[BitNumber] + Carry;
Sum[BitNumber] := ColumnSum mod 2;
Carry := ColumnSum div 2
end
end;
procedure ShiftLeft (var W: Word);
var
BitNumber: Integer;
begin
for BitNumber := WordSizeMinusOne downto 1 do
W[BitNumber] := W[BitNumber - 1];
W[0] := 0
end;
procedure Multiply (Multiplicand, Multiplier: Word; var Result: Word);
var
BitNumber: Integer;
begin
MakeZero (Result);
for BitNumber := 0 to WordSizeMinusOne do begin
if Multiplier[BitNumber] = 1 then
Add (Result, Multiplicand, Result);
ShiftLeft (Multiplicand)
end
end;
Actual processors use a lot of short cuts to speed up the process -- in
particular, they do various parts of the computation in parallel instead of
sequentially.Why is division the most time-consuming operation?
Because almost none of the steps can be done in parallel; the computation of each bit of the quotient depends critically on the outcome of the computation of the previous bit.
For the Negative and Positive procedures in
the integers handout, what happens if the integer is zero?
Both procedures return False in that case.
In the Zero function, you defined the result as comparing
the argument to 0.0. This seems a bit silly to me -- it seems that
round-off errors could easily make a result that's supposed to be zero
return a value of false. Wouldn't a construction like Zero :=
Operand < Tolerance be more appropriate, where Tolerance is an
appropriately defined (small) constant?
That's not a bad suggestion. It would be still better to make
Tolerance an input to the function, and to allow for rounding
errors in either direction:
near-zero
Input: operand and tolerance, both real
numbers.
Output: result, a Boolean.
Preconditions: tolerance is not negative.
Postcondition: result is true if operand
differs from 0.0 by an amount less than or equal to
tolerance (in either direction), false if it differs by
more.
function NearZero (Operand: Real; Tolerance: Real): Boolean;
begin
{ Assert (0.0 <= Tolerance); }
NearZero := (Abs (Operand) <= Tolerance)
end;
A similar NearEqual function, for determining whether two real
values are equal, to within a specified tolerance, would also be useful.
Whether NearZero and NearEqual should
replace Zero and = as implementations of
the zero and equal operations, or whether it would be better
to add them as additional primitive operations, is not so clear. I think
that programmers would find it frustrating to discover that some values
were both ``positive'' and ``zero,'' while others were both ``negative''
and ``zero,'' so I guess I'd favor the latter alternative.
Some of the functions described in the reading don't seem that useful --
the Zero function, for example. It would be just as clear to
write
if A = 0 ...as to write
if Zero (A) ...so it seems to me that the function takes up unneccessary space without adding much clarity. Is there some other reason for this sort of function?
If the compiler recognizes the function, it may be able to generate more
efficient code for the special case that the function deals with than for
the expression it replaces. In the case of Zero, for
instance, many processors have a special instruction that determines
whether all the switches in a given register are off; a compiler can direct
such a processor to place the argument A in a register and
then execute the all-switches-off test. This may be more efficient than
placing A in one register, zero in another, and testing
whether the results are equal.
However, current Pascal compilers will actually generate more efficient
code for the equality test than for a call to Zero, so you're
probably right in thinking that the function is a little pointless. It's
actually there just for pedagogical reasons: I want people to think about
the abstraction first and the implementation second, rather than trying to
guess prematurely (while designing the data type) what the target machine
will do.
In the integers handout, what does Modulo do? How does this
differ from the mod operation?
Modulo extends the mod operation. It is an error
for the second operand of mod to be negative; if the second
argument to Modulo is negative, it still returns a value
between zero (inclusive) and the modulus (exclusive).
What exactly does the modulo operation do?
It determines which residue class the moduland is a member of. A residue class for a given modulus is a set of integers, all differing from one another by multiples of the modulus. The set of natural numbers can be exhaustively partitioned into a number of residue classes equal to the absolute value of the modulus; for instance, if the modulus is 3, the residue classes are {..., -10, -7, -4, -1, 2, 5, 8, ...}, {..., -9, -6, -3, 0, 3, 6, 9, ...}, and {..., -8, -5, -2, 1, 4, 7, 10, ...}.
Each residue class contains exactly one integer in the range lying between zero (inclusive) to the modulus (exclusive), which uniquely identifies the residue class and is the value returned by the modulo operation.
Note that it is negative only if the modulus is negative and does not evenly divide the moduland.
Why doesn't the specification for the integer module include a
DeallocateInt procedure?
It's not in the description of the abstract data type because it's an
operation on storage, not on integers. It's not listed on the assignment
sheet because perhaps not everyone will want to define Int as
a pointer type. However, you should certainly add it if you do use a
pointer type.
Which method do the HPs use to store integers?
Twos-complement representations in thirty-two bits.
In the 2's complement method of storing integers, how does the computer distinguish between positive and negative? It looks like any byte could be either a small positive integer or a large negative integer, or vice versa.
The leftmost bit of the representation indicates the sign. If it is zero (off), then the number represented is either positive or zero; if the sign bit is one (on), then the number represented is negative.
I was reading a book on microprocessors of the late seventies, and it explained twos-complement encoding as such: Ones-complement is the result of switching all the bits in sign-magnitude encoding, and twos-complement is the result of taking ones-complement and adding one (doing any necessary carries). I don't remember it being this simple - is it, or is the author oversimplifying something he doesn't think is important?
Well -- the statement of it that you've cited here is a little confused. I'll try to clear it up.
Signed-magnitude, ones-complement, and twos-complement representations are
three different systems for storing integer values that may be positive,
zero, or negative. All three of them use identical bit patterns for every
positive integer (up to MaxInt, which is the same in all three
systems).
However, they store negative numbers differently, and one can explain the difference concisely by describing the way the negative (or additive inverse) of an integer is computed. In the signed-magnitude system, toggle the sign bit. In the ones-complement system, toggle every bit. In the twos-complement system, toggle every bit and then add one, carrying as necessary -- or, equivalently, toggle every bit to the left of the rightmost 1-bit.
When I explained this in class, I skipped over ones-complement representations, because they are now very rarely encountered and in my opinion just get in the way of the explanation. So it may have sounded as if finding the negative of a twos-complement integer was more complicated than ``taking ones-complement and adding one.'' But it's not complicated.
Also, this book discusses doing math on 32-bit representations of real numbers using eight-bit busses and registers. This seems to me so difficult as to not be worth doing. If doing math with real numbers was so important, why not make the tradeoff and use a bigger, more expensive, 32-bit chip?
Because if you had put a thirty-two-bit processor in an Apple II in 1983, you would have increased its cost by a factor of a hundred.
Why is it that VAXen allocate storage with the half-words ``backwards''? What advantages does this lead to?
The main advantage was that data storage on the VAX was more nearly compatible with data storage on earlier Digital machines. This is why that representation was chosen.
Has the method of representing integers changed much in the last ten years? Is it likely to change in the next ten?
Twos-complement representations were very common -- clearly in the majority -- ten years ago, are overwhelmingly common now, and will be even more common ten years from now. By then it will be difficult to find a functioning computer that does not use twos-complement representations.
However, the number of bits in a word changes more frequently. Ten years ago, if I recall correctly, every computer at the College used either a sixteen-bit word or an eight-bit word. Today, machines with thirty-two-bit words are commonplace. Ten years from now, I expect sixty-four-bit words to be standard.
On page 600 of the Walker textbook, he discusses word length and its effect on integer ranges. I understand the restrictions on the range for 16 bit machines, i.e. -32768 to 32767 or something similar, but I am confused by the next paragraph, when he states that a 32-bit machine can encode about 5 x 10^9 numbers. If I raise 2 to the 32nd power, I only get 4,294,967,296, which is not 5 x 10^9. Also, if one of those bits is a positive/negative flag, then the integer range would be even more limited. I do understand that he is not necessarily talking about integer ranges, but rather storage locations ... but I am wondering where 5 x 10^9 came from.
Your figure is correct; Walker's was a rough estimate. (It came from reflecting that 2^10 is a little more than 10^3, so 2^30 should be a fair amount more than 10^9, so 2^32, or 4 * 2^30, should be a fair amount more than 4 * 10^9; Walker made a guess about the ``fair amount'' that turned out to be a little high.)
Using one of the bits to indicate the sign does not reduce the range of
representable values of type Integer, provided that none of
the integers that are marked as ``negative'' is equal to any of the
integers marked as ``positive.'' For instance, in the twos-complement
representation used on the HPs, there are 2147483648 negative values and
2147483648 ``positive'' ones (including zero); since they are all
different, the total number of values represented is still 4294967296.
In class in Monday you mentioned a datatype that can store an infinite amount of integers. How?
By allocating storage for an integer dynamically, using pointers to link together enough blocks of fixed size to hold all of the digits of the integer.
Are there physically little switches inside the computer turning on and off?
Yes, but the switches don't have moving parts like light switches. Modern computers are completely electronic, which means among other things that the two states of each switch are distinguished not by the physical positions of its components but by some electrical characteristic (low and high capacitance, for instance). Some early computers, however, were electro-mechanical and used mechanical switches operated by relays.
Why are bits in a byte or word numbered from right to left rather than left to right?
Because when a sequence of bits is used to represent a natural number, each bit position corresponds to a power of two (just as in decimal numeration each digit position corresponds to a power of ten). Bit 0 corresponds to 2^0, or 1 (it's the ``units place''), bit 1 to 2^1, or 2 (the ``twos place''), bit 2 to 2^2, or 4 (the ``fours place''), and so on. The bit number matches the exponent.
Walker talks about data being stored in separate bytes or words because computers often have an easier time dealing with whole bytes or words. The trade-off is wasted space. With memory being so cheap, is this a general trend in computing?
Yes. Packed structures are used much less now than when I began teaching computer science in 1983. Programmers now generally think of arranging the fields of a record to conserve storage as an optimization that one performs only on programs that are memory-intensive; formerly it was usual to take alignment problems into consideration whenever one wrote the definition for a record type.
Why is it that some machines can't easily access the individual bits as opposed to just a whole word and others can? Is overall speed greater if this accessing is made trickier? Or is this just an older design?
Partly it is a question of efficiency. If it takes the same amount of time to transfer one bit, eight bits, or thirty-two bits from memory to the processor or vice versa, why bother having a separate address for each bit or even each byte? Instead, just bring in the word containing the relevant byte and have the processor recover the part of the word that is needed.
A second consideration is the number of bits required for an address. Suppose you're designing a machine that can be equipped with 2^k bytes of internal memory. If each byte has a separate address, the addresses will themselves contain at least k bits. If each bit has a separate address, addresses will have to contain k + 3 bits apiece; if the machine is word-addressible, k - 2 bits will suffice (if a word is four bytes). This can make a difference in the complexity and cost of the circuitry.
Is there a way to precedurally determine MaxInt without
already knowing how many bits are in that computer's word?
Of course:
program FindMaxInt (Output);
begin
WriteLn ('MaxInt = ', MaxInt : 1)
end.
What exactly am I looking at when I view a file of integer using a pager
such as more or less? (It looks like gibberish to me.)When the file is created, each integer is copied to the file exactly as it exists in memory, as a thirty-two-bit twos-complement representation. The bits are stored in the file exactly as they are in memory.
When a text-oriented tool such as a pager takes hold of this file, it tries
to deal with it as a sequence of lines, each consisting of ASCII characters
and terminated by a line break -- on our systems, the
line-feed character, Chr (10). It picks up bits
from the file in groups of eight and identifies the ASCII character in each
one. If the ASCII character happens to be a graphic, it displays that
character; if it is a control character, the window performs the
appropriate control operation -- e.g., 00000111 causes a beep,
00001010 causes the cursor to move to the beginning of the
next line, and so on. The result is gibberish, because the bit
patterns that were stored have nothing to do with ASCII characters.
Does the hexadecimal system of numeration serve us any useful purpose? Will we ever need to use this in programming, or is this just an example to help us understand what the computer goes through in processing numbers?
You'll need it. For example, you'll eventually be learning to use debugging tools that can display the values of pointers; conventionally, they are written out in hexadecimal numerals (because the bit pattern of an address enables the programmer to determine whether the storage accessed through the pointer is aligned on a word boundary). Also, programmers occasionally want to read assembly-language listings of their programs, to find out what exactly a particular machine is doing in a frequently executed loop; base-16 numeration is heavily used in such listings.
Are you allowed to enter a number in scientific notation if it is of the
type Real in Pascal, or do you have to set up some procedure
to convert the notation first?
The Read and ReadLn procedures can cope with
scientific notation when reading in a value for a Real
variable -- one more reason why one would prefer to use those built-in
procedures instead of defining one's own.
Can you define a long real in Pascal like you can in C?
You can in HP Pascal; the LongReal type is equivalent to the
double type in C, the Real type to
float. Of course, LongReal is non-standard.
In implementations of Pascal that provide only one floating-point type, it
is usually equivalent to C's double rather than C's
float.
Would a program using fractions of inches be more accurate than a program using metric measurements, since metric is decimal and inches are commonly expressed in fractions of base two (1/16, 1/2, etc.)?
Yes, it would, if it never performed any operation that resulted in a fraction in which the denominator was not a power of two.
Is there a real number equivalent to MaxInt? What would
happen, for example, if I tried to read in a real number whose exponent
component would require more than eight bits of storage?
The nearest analogue of MaxInt is the greatest number that has
an exact IEEE single-precision representation, 2^128 - 2^104; let's call
this number BigReal. If the HP Pascal Read
procedure encounters a numeral for a value slightly larger than
BigReal, it will round the value down to BigReal;
if it encounters a numeral for a value much greater than
BigReal, it will ``round it up'' to the IEEE positive
infinity. Other implementations of Pascal may crash when trying to read in
the numeral for an outsized value.
Is real number storage standardized, or are there many ways of doing this, too?
Unfortunately, there are even more ways of representing real numbers than of representing integers. There is a standard, or rather a small family of closely related standards, that the Institute of Electrical and Electronics Engineers labored over for years. They have had some success in getting computer manufacturers to adopt this standard, but it still isn't as popular as ASCII for characters.
The Java programming language, which is rapidly increasing in popularity, requires that real numbers be represented according to the IEEE standard; if the hardware for a machine that runs Java programs does not represent reals in this way, it is supposed to provide a software simulation of IEEE reals when running Java programs. Possibly this will lead to greater acceptance of the IEEE standard in the next generation of machine designs.
How does the machine store a real number into memory? Does it just round it to a certain number of decimals and then store each digit of the number as an integer, or is there something more complicated that it does?
The story is more complicated; the handout on IEEE representations of reals gives the full account, but I can sum it up briefly by saying that a variant of scientific notation is used: A real number is represented as a coefficient times a power of two, a * 2^b. Part of the storage allocated for a real value is used for the coefficient, a, and the rest for the exponent b. Neither a nor b is stored in exactly the way an integer value is, though, because it turns out to simplify the circuitry that performs arithmetic operations on real numbers if slightly different conventions are used.
After reading Appendix B.2 in Walker's book, I am still unclear about how exponents in real numbers are stored -- in particular, how the machine differentiates between a negative and a positive exponent.
The numeration system that is used for storing exponents is a
``biased-magnitude'' system. Suppose, for example, that we're storing an
HP Pascal Real in the IEEE single-precision format. The
exponent for such a value can be any integer in the range from -126
to 127. A fixed ``bias'' of 127 is added to the exponent,
and then the eight-bit binary numeral for the result is actually stored.
So, for instance, the exponent 5 would be stored as
10000100, which is the eight-bit binary numeral for 132
-- the true exponent, 5, plus the bias, 127.
The bias in this case happens to have been chosen in such a way that the
leftmost bit of the representation of the exponent is 1 if the
exponent is positive, 0 if it is zero or negative, so you
could use that bit to determine the sign of the exponent. In practice, it
is almost never important to know the sign of the exponent without knowing
its actual value.
Is the leftmost bit the least significant bit in the mantissa, or is it the mantissa's rightmost bit?
The rightmost, bit 0, is the least significant.
Why is the greatest number in exact IEEE single precision 01111111011111111111111111111111 and not 01111111111111111111111111111111?
When all the bits in the exponent field are turned on, the IEEE conventions
is that no real number is represented; instead, such a bit pattern
indicates that an operation on real numbers was attempted when one of its
preconditions was not met. The second of the bit patterns shown above, for
instance, might arise as the result of the evaluation of the Pascal
expression 0.0 / 0.0.
My question is about the range of IEEE double-precision real numbers. I understand the origin of the lower limit, 2^-1074, since the exponent comes from -1022 and the 52 bits stored in the mantissa. I don't understand why the upper limit is not 2^1075.
Single-precision reals have a similar restriction on the upper limit (2^128 - 2^104). Why is it not 2^150?
If you consider only normalized representations of real values --
those that don't exploit the all-zeroes setting of the exponent field, and
so have an implicit 1. at the left of the mantissa field --
the lower bound is actually 2^-1022 for double-precision representations
and 2^-126 for single-precision ones. You can get closer to zero only by
using unnormalized representations, with all zeroes in the
exponent field and an implicit 0. at the left of the mantissa
field. There is no analogue of unnormalized representations at the high
end of the system, so you have to stop when you get to the largest
available setting of the exponent and mantissa fields -- there is no way to
shift the implicit binary point any farther to the right.
How widely accepted are the IEEE standards for real-number representation?
I'd guess that more than half of the machines that have Pascal compilers use some variation of IEEE single- or double-precision reals -- fewer if you consider Turbo Pascal to be a form of Pascal (I don't).
Why don't you consider Turbo Pascal to be a form of Pascal?
In the language of the Pascal standard, it is not ``a processor complying with the requirements of this standard'' -- it does not provide all of the standard procedures, does not recognize all of the standard syntax, and does not detect all of the violations that the standard requires it to report.
Also, the object-oriented extensions that Turbo Pascal now incorporates have fundamentally changed the computational model that it embodies. A Pascal computation is a sequence of operations performed by a processor on inert data; a Turbo Pascal computation is an interaction among active objects, each embodying a state. So successful programming in Turbo Pascal is quite different from successful programming in Pascal -- one designs objects rather than procedures, functions, and data types.
Is it very inefficient for the computer to have all of the special cases
in the IEEE real representation system? I know that plugging things like
error messages in so as not to waste the bit combinations when the
exponent is 11111111 saves bit space, but does this slow the
computer down at all? It seems like it would be easier to be able to
perform the same operations on all patterns and to be able to treat them
all as legal values. is this slowdown at all comparable with the greater
number of combinations?
It's not obvious when you look at it for the first time, but the IEEE floating-point representations are actually quite cleverly designed so that detecting and handling these special cases hardly slows the computation down at all. If you're going to use an exponent-and-mantissa representation at all, you have to treat 0.0 as a special case anyway; the marginal cost of handling unnormalized numbers as well is small. Similarly, in order to ``perform the same operations on all patterns and ... to treat them all as legal values,'' you need to have some way of handling the results of division by 0.0; reserving an easily recognized pattern of exponent bits for them is actually the most efficient way to do it.
Why do IEEE representations of real numbers use a biased exponent? Is there some way in which this improves efficiency at the circuit level?
Yes. Since all the biassed exponents are positive, you can find out whether one of them is greater than, equal to, or less than another without paying any attention to signs. Twos-complement and signed-magnitude representations do not have this property and therefore require more complicated algorithms (and hence circuits) for comparisons.
You subtracted one from the cardinality of the Real data type because -0 and 0 are the same. Why do we not do the same for the Integer data type?
Because only one of the 2^32 possible bit patterns is used to represent
0 as an integer; there is no separate bit pattern for a ``negative
zero.'' In the Real type, the distinct bit patterns
00000000000000000000000000000000 and
10000000000000000000000000000000 are both used to represent
0.0.
Is the only thing that is unusual about longreal numbers the fact that they use L instead of E? That hardly seems worthy of calling them a different style of numeration.
That's the only difference in the numeration system that is used to
represent LongReal values in HP Pascal source code. The
internal representation of such values -- the fact that the
IEEE double-precision representation rather than the IEEE single-precision
representation is used -- is more consequential: It means that the range
of LongReal values is much larger than the range of
Real values, and that most LongReal values are
stored to a precision of fifty-three significant binary digits rather than
twenty-four.
Do real-number approximation problems (e.g., round-off error) become more or less pronounced as the number of bits used to represent them increases? In other words, are 32-bit reals less susceptible to round-off error than 16-bit reals?
You're probably meaning to contrast ``double-precision'' representations of real numbers (typically, sixty-four bits) with ``single-precision'' representations (typically, thirty-two bits). I don't know of any system that represents real numbers in sixteen bits.
Rounding errors are just as frequent with double-precision real numbers as with single-precision ones, but they tend to be much smaller, in the sense that the absolute value of the difference between the correct value and the value that is actually stored is less. For instance, the exact value of the single-precision representation of 7/5 is 11744051/8388608, which is too small by 1/41943040; the double-precision representation is 6305039478318694/4503599627370496, which is too small by 1/11258999068426240. A rounding error occurs in either case, but the distortion is less extreme if double-precision representations are used.
How significant can the errors which accumulate as the result of rounding errors be, when the lower limit of a real is 2^-149 and a longreal far, far greater? In what types of applications would such precision be required?
2^-149 is the least positive representable value of type
Real, not the amount of rounding error in the representation
of a typical real. The magnitude of the rounding error is proportional to
the magnitude of the number represented; a normalized value of type
Real can differ from the number it is trying to express by as
much as 2^-24 times the magnitude of the value. So, for instance,
if you're expressing the national debt of the United States (as I write,
$5230766368737.51) as a real number of dollars, the value that is actually
stored is 5230766325760.00 -- an error of almost forty-three thousand
dollars.
In the postcondition for the round operation on real numbers, you
say that if there are two integers that differ from operand by
0.5, result is the even one. In my understanding of rounding,
one would round an operand differing from two integers by 0.5, not to the
even integer, but to the larger.
Different authorities recommend different rounding policies. Standard
Pascal's predefined Round procedure always rounds the halfway
values away from 0.0, which may be what you had in mind as well.
(If the operand is -2.5, is -3 or -2 the ``larger''
value? Standard Pascal rounds it to -3.) I see that I got this
exactly wrong in the handout, so I'm glad to have the opportunity to make
the correction.
The reason I recommend a round-to-even policy in the abstract data type is that I think that it is less likely to produce accumulations of rounding errors that are all in the same direction. In many applications, positive values with a fractional part of 0.5 occur frequently; rounding a long series of such values and then finding the sum can produce a very large rounding error, exaggerated by the fact that all the rounding is in the same direction (upwards). The round-to-even policy will produce rounding errors that tend to cancel each other out in such circumstances.
Walker's text book suggests that to write a binary number in scientific notation we must move the radix point as far to the left as possible, so that it is just to the left of the first 1. This is different from what you lectured on in class as the IEEE representation -- you said that they should be written as 1. something rather than 0.1 something. Is this just a peculiarity of IEEE representation?
It's more a difference in perspective. Walker's book places the notional binary point to the left of the first 1, but claims that the exponent has a bias of 128. The handout I wrote places the binary point to the right of the first 1, but claims that the exponent has a bias of 127. You get the same result in either case, but I think it's easier to add the explanation of how unnormalized numbers work if you present the numeration system as I did.
All this binary stuff we've been doing makes me wonder...is it possible to create machines that are more than bistable (tristable, quadstable, etc.)? If there were three memory states in each bit, the processing power would be much greater. Is this kind of device even remotely feasible?
Yes, but there are two problems with multistable devices: speed and reliability. Binary switches can change state very quickly, and it's also comparatively easy and fast to determine the state of a binary switch. Some mechanical and electromechanical computers of the forties and early fifties, used decimal numeration internally, like old-fashioned adding machines and desk calculators.
According to Knuth, a ternary system of numeration ``was given serious consideration along with the binary system'' during the development of early electronic computers in 1945 and 1946, at the Moore School of Engineering; the somewhat greater complexity of the arithmetic circuitry is partially offset by the greater concision of the representation. (On the average, the number of ``trits'' in the ternary representation of a number is about 63% of the number of bits in its binary representation). Knuth concludes, ``Perhaps the symmetric properties and simple arithmetic of this [balanced ternary] number system will prove to be quite important someday -- when the `flip-flop' is replaced by a `flip-flap-flop' '' (Seminumerical algorithms, volume 2 of The art of computer programming, p. 192).
What is a structured variable? The definitions of different types of variables, such as buffered variables, confused me too.
A structured variable is a variable that has other variables as components -- an array variable or a record variable. In other words, the region of a computer's memory that an array occupies can be divided up into smaller regions, one for each element of the array, and the region that a record occupies can be divided into smaller regions, one for each field of the record.
The discussion of the several kinds of variables on pages 69-71 of the Cooper book boils down to the fact that there are only five ways to refer to storage locations in Pascal:
with-statement, using the field name by itself (in which case
it is a field-designator, the other kind of
component-variable);
type
IntArray = array [1 .. 10] of Integer;
Direction = (North, NorthEast, East, SouthEast, South, SouthWest,
West, NorthWest);
Weather = record
Temperature: Real;
WindSpeed: Real;
WindDirection: Direction
end;
Access = ^Weather;
var
Alpha: Integer;
Vec: IntArray;
Today: Weather;
Tomorrow: Access;
Target: Text;
and that Source has been opened for output, here is an example
of each of the five kinds of variable:
Alpha (entire-variable)
Vec[3] (indexed-variable)
Today.WindSpeed (field-designator)
Tomorrow^ (identified-variable)
Target^ (buffer-variable)
Note that these are exactly the kinds of expressions that can appear on the left-hand side of an assignment statement -- in that position, you're referring to a storage location, so you need a ``variable-access'' expression.
Note also that although a set in Pascal is a structured value, a set variable is not a structured variable -- there's no way to refer to any part of the storage location that is occupied by a set, or to adjust a single element of a set without touching the rest of it. Another way of stating the same point is to say that not all data structures correspond to storage structures.
In what kind of situation would using arrays of more than two dimensions be appropriate? I could see using a three-dimensional array to model a 3-d board game. What about four-dimensional arrays? Are there any real-world problems that use this?
The number of dimensions in an array reflects the number of independent ways of classifying the objects that the array elements count or describe. For instance, an clothing store's inventory program might keep track of the number of men's slacks on hand by updating the elements of a five-dimensional array of the type defined below:
type Waist = 30 .. 42; Inseam = 28 .. 36; Weight = (Light, Medium, Heavy); Fly = (Button, Zipper); Color = (Blue, Brown, Black, Gray, Green, Tan); SlacksInventory = array [Waist, Inseam, Weight, Fly, Color] of Integer;Is there some sort of general rule for when it's best to use arrays, as opposed to linked lists, for data storage?
Yes. It's better to use an array when you need ``random access'' to elements of the structure -- that is, when the order in which the program will examine those elements is unpredictable -- and when you either know in advance how many elements the structure will contain or can at least fix an upper bound that is likely to be approximately correct and certain not to be exceeded. It's better to use a linked list when the elements of the structure will usually be accessed sequentially, from first to last, or when almost all the accesses will involve a few identifiable elements (which can be moved to the front of the list); and when the number of elements in the structure will not be known, even approximately, until the program is running.
Why not implement arrays in such a way that subscripts could be added to them after they are created initially? That way they could grow in the same way as pointer structures, but wouldn't have pointers flying all over.
That's a good idea, and there are several languages -- C++, Java, Common Lisp, and Icon for instance -- in which you can do exactly that. Of course, they all use dynamically allocated structures hooked together with pointers internally, but the details aren't visible to the programmer. But such a data type goes against Pascal's design philosophy of staying close enough to the real machine that the student programmer is directly aware of the mechanics of storage allocation.
The reading for today discussed memory storage of arrays. However, isn't the storage scheme different for different implementations of Pascal? More importantly, isn't it different for diffferent computers?
Some of the details (such as alignments) are different. The general scheme of using base addresses and computed offsets is surprisingly uniform.
Why isn't there a term for ``word-aligned''? It seems useful to define that to me, just because it would be less cumbersome to say that something is word-aligned than 4-byte or 8-byte aligned, depending on machine architecture.
The phrase `word-aligned' is sometimes used. The HP documentation uses `2-byte-aligned', `4-byte-aligned', and `8-byte-aligned' so as not to mislead programmers who are trying to migrate onto HPs from machines on which the word size is different.
What's the point of calling something bit-aligned? You can't set half a bit.
True, but that just means that bit-alignment is the least restrictive of all alignment possibilities, not that it's a pointless concept. It still contrasts with byte-alignment, 2-byte-alignment, and so on. (A value in storage is bit-aligned if it can begin at any bit position within any byte; sometimes this is an important thing to know about it.)
I suppose the complaint is that there's nothing that a bit-aligned value need really be aligned with and no possibility for it to be somehow misaligned (overlapping the boundaries between bits). OK, so perhaps it's a misnomer.
What factors make a single bit in memory easier or harder to access? You gave examples about what makes single bytes easier and harder to access in class on Friday; is the answer to this just an extension of those factors?
Not quite. To recover the value of a single bit from memory, a processor will transfer the contents of smallest independently addressable unit of memory that contains that bit into a register, then ``mask off'' (that is, set to zero) all the other bits in the word, and finally shift the surviving bit rightwards in the register until it becomes the least significant bit. The masking operation takes the same amount of time regardless of where the bit is within the register; on some machine architectures, however, the shifting operation takes longer if the bit starts out farther to the left, so that the leftmost bits are the hardest ones to access.
When declaring variables in our programs, is it important to be thinking about byte alignment and about in what order we declare our variables?
It wouldn't hurt. The commonest situation in which it's really important to think about alignment is when you're writing the type definition for a large array of records; laying out the record in such a way as to minimize padding can make a big difference in the number of bytes required for the array.
How exactly are packed arrays implemented? I was reading your code to get the machine representation of a datum, and you declared a packed array of bits for the machine's unit. Does this mean that packing crams things together as close as possible, and you have to do bitwise operations on registers to recover the part of the machine's word that you want?
Different implementations of Pascal handle the packed keyword
differently. As I mentioned in class, Sun Pascal simply ignored it and
stored ``packed'' arrays in exactly the same way as any other kind of
array. HP Pascal tries to place several elements of a packed array into a
single byte, if they will all fit, but will not store any element in such a
way that it crosses a boundary between two bytes of memory; it will leave
some of the bits in a byte unused if too few bits remain in a byte to
accommodate another element. (However, HP Pascal also offers ``crunched
packing,'' in which not even one bit may be left between elements, even if
some elements have to be stored across byte boundaries.)
Array elements that have been packed more than one to a byte must indeed be extracted by means of bitwise operations before they can be operated on.
I believe I understand how normal arrays are stored in computers, but what about packed arrays? I imagine the answer is implementation-dependent, since some machines are byte-addressable and others word-addressable. Does one simply calculate the offset using the number of bits in an element rather than number of elements?
As I mentioned in class, some implementations of Pascal simply ignore the
keyword packed and store all arrays in the same way. Under HP
Pascal, however, it is possible for two or more small array elements to be
packed into the same word or even into the same byte. The compiler decides
how many elements to pack into one unit of storage (the packing
factor). When the compiler then translates a reference to an element
of the array, its computation of the offset to be added to the base address
includes an extra step; after figuring the difference between the value of
the array index and the lower bound of the index type, the compiler divides
by the packing factor, keeping both the quotient and the remainder. The
quotient is the offset; it is added to the base address to obtain the
address of the unit of storage that contains the array element. The
remainder indicates the position of the element within that storage
location; the compiler uses the remainder to figure out how, after moving
the contents of the storage location into a storage register, it should
mask and shift the bits to strip away all the irrelevant data stored along
with the desired element.
Walker makes it seem as though every single word of storage space has an individual address, like an enormous array. Is this true? Why then don't we simply address the array in the same way the computer does, with the offset being something that we must simply know?
In some programming languages, you can do exactly that. In fact, Pascal is one of them -- a pointer in Pascal is simply an index into the memory considered as an array. This is disguised, in Pascal, by the fact that the language doesn't allow you to write out a string representation of a pointer value or to perform any arithmetic operations on it. In some other languages, such as C and Bliss, there are no such restrictions.
However, the picture of memory as an array of bytes or words, indexed by natural numbers, is not quite correct for some computers, such as IBM PCs and their clones. A memory location on the PC is identified by a combination of two indices: a sixteen-bit ``segment'' value, which identifies some contiguous group of 65536 bytes in the PC's memory, and a sixteen-bit ``offset,'' identifying one particular byte within the segment. Unfortunately, the segments are overlapping rather than mutually exclusive, so that a particular byte of memory can have many different but equivalent addresses -- byte 0 of segment 28 is the same physical collection of eight switches as byte 48 of segment 25, for instance.
In C, array subscipts must start with 0. I thought that this would save a lot of computing time, but in lecture you showed that arrays with subscripts starting from non-zero numbers are negligably harder to initialize than those starting with subscipts of zero. Why does C choose to do this? It seems very inconvenient.
If the subscripts for an array start with 0, the base address of the array and its virtual origin are equal and interchangeable. This simplifies all the computations that are done with the addresses of elements of the array. In Pascal, all the address computations are performed inside the compiler and the author of the compiler is expected to deal with any complications. In C, on the other hand, it is possible to determine the address of any variable or array element and store it in a pointer variable, and also to operate arithmetically on pointer values. The designers of C decided that the extra flexibility offered by array types in which the indices started at some non-zero value was not worth the extra trouble that would be caused by making the distinction between base addresses and virtual origins visible to the programmer.
If I have a packed array and I want to fill it with blanks, can I use the assignment
type
X = array [1 .. 14] of Char;
var
Y: X;
{ ... }
Y := ' '
or do I need to specify each character separately?
The only thing standing in the way of the correctness of the assignment is
that the string of spaces is a packed array of characters, while
the variable Y is just an array of characters. Put the
keyword packed into the definition of type X and
the assignment will be correct.
When I converted a string into a value of an enumerated type, I used a
long if-statement. The string is defined to be 14 characters
long. Since some of the strings I wanted to convert were not so long, I
put spaces in the rest of the positions of the array. Since strings can be
compared only if they have the same length, I also added spaces to the
string constants with which I was comparing the input strings. It looked
stupid. So instead I tried to pad the arrays with null characters
(Chr (0)), deleting the space from the string constants in the
if-statement. But this attempt failed. Is my original method
the only way to solve this problem or is there a better solution?
You can compare two strings only if they have the same length; all the characters of a string are included in this length, even null characters, so padding with null characters is no better than padding with spaces -- in fact, it only makes things more difficult, since there is no way to include a null character in a string constant.
The best way to do this in standard Pascal is to write a special comparison
procedure that allows strings of different lengths to be compared. The
following function returns True whenever it is given two
strings that are alike except possibly for different numbers of trailing
null characters:
function EqualAsStrings (
LeftOperand: packed array [LeftLow .. LeftHigh: Integer] of Char;
RightOperand: packed array [RightLow .. RightHigh: Integer] of Char):
Boolean;
var
Position: Integer;
EqualSoFar: Boolean;
begin
Position := 0;
EqualSoFar := True;
while EqualSoFar and (Position < LeftHigh) and
(Position < RightHigh) do begin
Position := Position + 1;
if LeftOperand[Position] <> RightOperand[Position] then
EqualSoFar := False
end;
while EqualSoFar and (Position < LeftHigh) do begin
Position := Position + 1;
if LeftOperand[Position] <> Null then
EqualSoFar := False
end;
while EqualSoFar and (Position < RightHigh) do begin
Position := Position + 1;
if RightOperand[Position] <> Null then
EqualSoFar := False
end
EqualAsStrings := EqualSoFar
end;
This procedure uses a feature of Pascal called ``conformant array
parameters'' that you may have seen only briefly, in the Cooper book. I'll
discuss conformant array parameters in more detail a little later in the
semester.How does one call a procedure containing conformant array parameters?
There's nothing distinctive about the syntax of the call. The argument corresponding to the parameter must be an array in which the base type matches the base type of the parameter and the index type matches the type of the index constants in the parameter.
The only restriction is that if a conformant array parameter is also a value parameter (as opposed to a variable parameter), the corresponding argument may not itself be a conformant array parameter.
How does the compiler represent conformant arrays in memory? Or does the conformant array specification apply only to parameters passed to subroutines?
Only parameters can be conformant arrays; declared variables, both global and local, must have a fixed size.
When the compiler translates an invocation of a procedure or function that has a value parameter of a conformant array type, it deduces the size of the array that is needed from the type of the corresponding argument and writes out machine instructions that allocate the necessary amount of storage for the duration of the execution of the procedure or function. Some details of the suggested implementation of this method can be found on page 94 of the Standard Pascal user reference manual.
A variable parameter of a conformant array type is handled like any other variable parameter; during the execution of the procedure or function, it is an alias for the corresponding argument. Only the address of that argument is actually passed to the procedure or function.
The worst-case scenario for the quicksort is O(n^2). Obviously, O(n^2) is bad, but is it an efficient or an inefficient O(n^2) method (``efficient'' and ``inefficient'' being relative; O(n^2) is something to be avoided if possible). How would it compare to the other O(n^2) sorts such as the selection sort or the bubble sort?
The worst case of quicksort is bad even for an O(n^2) sort -- worse than selection sort (same number of comparisons, more data movements), though probably not quite as bad as bubble sort.
How does choosing a value that should be the middle speed up a quick sort?
Quicksort examines an element only once and moves it no more than once during each partitioning step on the part of the array that contains that element, so the number of partitions performed on any part of the array is the critical quantity in determining the algorithm's performance. If the pivot element is always in the middle of the range of values, so that the partitioning step always divides the array segment into two equal parts, no part of the array of size n will be partitioned more than lg n times. But if the pivot is always at one end or the other, then there will be some part of the array that always winds up in the larger partition and is therefore exposed to n partitioning steps.
When I was taught quicksort last year in CS1, we were told that one of
the things that made quicksort so quick was the small number of swaps. In
the Walker text, the example of Quicksort includes a number of calls to the
Swap procedure in the two procedures on p. 421,
CheckUp and CheckDown. Instead of
Swap which presumably takes three assignment statements each
time executed, wouldn't it be much faster (especially in a very long list)
to instead store one extra Temp variable of type
DataType, store the first element in the list in this, and
have CheckUp and CheckDown make one assignment
statement?
It might be somewhat faster. Of course, you'd also have to set up a variable of the array's index type to keep track of the ``hole'' in the array -- the position into which the next out-of-place item is to be moved.
Actually, the rendition of quicksort that I usually show to people wh