John English Ada 95 The Craft of Object-Oriented Programming PDF
John English Ada 95 The Craft of Object-Oriented Programming PDF
Next
John English
(originally published by Prentice Hall, 1997)
Permission is given to redistribute this work for non-profit educational use only, provided
that all the constituent files are distributed unchanged and without charge.
This book, originally published by Prentice Hall in 1996, was taken out of print in 2001 and the rights to
the book were subsequently returned to me by Pearson (the successor company to Prentice Hall). I have
decided to make it available online in HTML format, and at the same time I have corrected several errata
which were present in the printed editions of the book. Its perfectly possible that I might have missed
some, or even introduced some brand-new ones, as part of the process of transforming the text into
HTML. If you spot any mistakes, please let me know so I can correct the master copy, which can be
found at http://www.it.bton.ac.uk/staff/je/adacraft/.
Contents
Preface
1. Programming concepts
1.1 What is a program?
1.2 Readability, maintainability, portability and reusability
1.3 Specifications and implementations
1.4 Abstract data types
1.5 Generics
1.6 Inheritance and polymorphism
2. Fundamentals of Ada
2.1 Hello, world!
2.2 Names in Ada
2.3 Program layout
2.4 Context clauses
2.5 Strings
2.6 A simple calculator
2.7 Procedure specifications
Exercises
3. Statements
3.1 If statements
3.2 Assignment statements
3.3 Compound conditions
3.4 The case statement
3.5 Range tests
3.6 The null statement
3.7 Loops
3.8 The calculator program revisited
3.9 Exception handling
Exercises
4. Procedures, functions and packages
4.1 Zeller's Congruence
4.2 Declaring procedures
4.3 Declaring functions
4.4 Scope and lifetime
4.5 Separate compilation
4.6 Subprograms as library units
4.7 Packages
4.8 Child packages
Exercises
5. Defining new data types
5.1 Standard data types
5.2 Integers
5.3 Subtypes
5.4 Derived types
5.5 Modular integers
5.6 Real types
5.7 Numeric literals
5.8 Constants
5.9 Enumerations
5.10 The type Boolean
5.11 The type Character
5.12 Renaming declarations
Exercises
6. Composite data types
6.1 Record types
6.2 Strings
6.3 Declaring array types
6.4 Unconstrained types
6.5 For loops revisited
6.6 A simple sorting procedure
6.7 Multidimensional arrays
6.8 Discriminants
6.9 Limited types
6.10 Using packages with data types
Exercises
7. Exceptions
7.1 The importance of exception handling
7.2 Declaring exceptions
7.3 Re-raising exceptions
7.4 Getting information about exceptions
7.5 File input/output
Exercises
8. Program design and debugging
8.1 Stepwise refinement
8.2 Initial design
9. Private types
9.1 The need for abstraction
9.2 Package design
9.3 Private types
9.4 Full and partial views
9.5 Deferred constants
9.6 The package body
9.7 Overloaded operators
9.8 Use type clauses
9.9 A word of caution
9.10 The package Ada.Calendar
Exercises
10. Designing with abstract data types
10.1 The design process revisited
10.2 Separating out the user interface
10.3 Designing the model
10.4 Defining the view package
10.5 Implementing the ADT packages
10.6 Diary operations
10.7 Maintenance issues
Exercises
11. Dynamic memory allocation
11.1 Access types
11.2 Linked lists
11.3 Doubly linked lists
11.4 Iterators
11.5 Deallocating memory
Appendices
A. Language summary
A.1 Compilation units
A.2 Statements
A.3 Declarations
A.4 Type declarations
A.5 Exceptions
A.6 Expressions
A.7 Generics
A.8 Multitasking features
A.9 The Ada type hierarchy
B. Selected standard packages
B.1 The hierarchy of the standard packages
B.2 The package Standard
B.3 The package Ada.Text_IO
B.4 The package Ada.Sequential_IO
B.5 The package Ada.Streams.Stream_IO
B.6 The package Ada.Characters.Handling
B.7 The package Ada.Characters.Latin_1
C. Language-defined attributes
D. Package Listings
D.1 JE
D.2 JE.Appointments
D.3 JE.Appointments.Meetings
D.4 JE.Appointments.Deadlines
D.5 JE.Diaries
D.6 JE.Expressions
D.7 JE.Expressions.Spreadsheet
D.8 JE.Lists
D.9 JE.Menus
D.10 JE.Pointers
D.11 JE.Spreadsheets
D.12 JE.Spreadsheets.Active
D.13 JE.Stacks
D.14 JE.Times
Glossary
Revision history
$Log: contents.htm $
Revision 1.2 2001/11/17 12:00:00 JE
An astonishing number of errors in both the text and the formatting
have now been fixed. I am particularly indebted to Tad Ashlock and
Jeffrey Cherry for sending me extremely detailed lists of problems
and corrections.
Next
Preface
The word craft in the title of this book has been chosen deliberately. There have been books published
with titles such as The Art or The Science of programming, but I feel that neither of these titles is
really appropriate for a textbook like this. In Art, you have a blank canvas and are free to express
yourself as the spirit moves you in order to produce Beauty; in Science, you are constrained by the laws
of physics to produce Truth. A craft, on the other hand, is less free than Art but less constrained than
Science. Truth and Beauty are both involved in the process, but Art provides a blank canvas which is
rarely found in real-life programming. There is almost always some previous system which has to be
conformed to: an existing database or file format, or the peculiarities of a particular operating system.
Science searches for Truth, but there is little truth in most programs; only fairly trivial programs are
susceptible to correctness proofs at present. Most programs are unwieldy beasts, made like
Frankensteins monster from leftover fragments of previous generations of programs, and neither Art nor
Science takes account of this.
Craft skills are concerned with making the best out of available resources. If you are given some wood
and asked to make a piece of furniture from it, you have to work within the confines of what that wood
will allow. If the wood is knotty at a particular place, you may have to revise your initial plans in order to
work around the knots, or you may have to discard some of the wood and find some more which matches
the piece youve been given. Craftsmanship is concerned with the ability to work around problems like
this. In programming, you are rarely presented with a clean sheet and invited to design the next world-
beating program; you are usually presented with an existing program and a list of problems to be
resolved. How you choose to solve those problems is a measure of your craftsmanship. The existing
program will undoubtedly be full of knots which you have to work around. A good craftsman will be
able to produce an elegant result from a knotty problem. Elegant is often treated as a synonym for
artistic; I feel that the word crafty is, in its original sense, a more accurate translation.
Craftsmanship depends on having a good set of tools to hand. In the realm of programming, the tools are
techniques, algorithms, ways of doing things, ways of approaching particular types of problem, and of
course programming languages which support those approaches. Object-oriented programming is the
latest addition to the craftsman-programmers toolkit. It is not an ultimate solution in the Scientific sense
of ensuring that programs which use it will be guaranteed to be correct; it is not an Artistic solution that
frees you from the need to fit in with what already exists. It is a Crafty solution that allows you to design
things in such a way that when they are broken you will be able to take them apart and fix them, and
when they need extending you will be able to add on the extra features with a minimum of effort.
The book is in three parts. The first part deals with the fundamentals of Ada programming: how to do
input and output, how if statements and loops work, what procedures, functions and packages are all
about, how to define your own data types, how to handle exceptions. At this stage the examples are
necessarily tiny; although I deal with program design in chapter 3 when I reach the first example large
enough that design merits a separate discussion, the next few chapters concentrate more on introducing
the building blocks of Ada using variations on earlier examples. Instructors should of course continue to
emphasise the design techniques Ive introduced when students are expected to develop new programs as
exercises. At the end of the first part, an electronic appointments diary provides a larger example which
merits a chapter entirely devoted to design and debugging, although at this stage the design is
deliberately nave.
By the end of the first part, the reader will have been presented with two examples which will be carried
further in the next two parts: an electronic appointments diary and a simple desk calculator. These next
two parts concentrate on introducing maintenance problems and introducing new approaches and
techniques for dealing with these problems. In my experience the real problems of programming only
become apparent when you have to maintain an existing legacy of code, and you only learn to design
programs so that they are capable of being maintained in the future when you yourself have been on the
receiving end of maintenance problems.
The second part deals with abstract data types; it begins by taking the example from the end of the first
part and proposing some possible maintenance scenarios. I use these as an excuse for revising the designs
Ive already proposed in the first part. The early chapters in the second part deal with improving the
appointments diary from chapter 8, while the last chapter returns to the calculator example from the first
part. Some new implementation techniques are introduced: linked lists, generics, opaque types and
recursion are all investigated. The effort involved in dealing with the proposed maintenance scenarios
should reveal what a mess poor programming practices can get you into, and should hopefully provide an
incentive to master ways of designing programs so that this sort of effort will be minimised if you ever
have to make maintenance changes to programs that you write.
The third and final part is concerned with using the object-oriented programming features of Ada 95 to
develop extensible programs. The features of Ada described here provide the essential difference
between Ada 83 and Ada 95. This part considers maintenance scenarios which affect not just the
(re)implementation of existing features but also the capacity of the current design to accommodate new
requirements. What happens when you want your existing appointments diary to cope with different
types of appointment? What if at some future date you want to incorporate a new type of appointment
into your existing systems? What if you want to extend your existing calculator to cope with the extra
requirements of a spreadsheet? What if you want a spreadsheet that updates itself in real time? These are
the sort of maintenance problems that arise most commonly in the real world; implementation is
irrelevant to users, but the ability to add new features is crucial. And it is up to the craftsman-
programmer to reach into his or her toolbox and to craft a solution with the tools available; the final part
of this book shows how its possible.
In my opinion, the best way to learn any programming language is to use it to do something that interests
you. I first learnt to program because I was interested in John Conways Game of Life which was
popular in the early 1970s, and I wanted something that would let me find out the eventual fate of the R-
pentomino. I never succeeded in this because I got sidetracked into programming as an end in itself!
Electronic diaries and spreadsheets should be familiar enough applications to all readers, and I hope that
this will make them reasonably interesting. Theyre large enough to be challenging, but not so large as to
require a book twice the size of this one. Adopting a project of a similar size (e.g. a text editor) and
working on it in parallel with studying this book is the best way I can think of to become a good Ada
programmer. Consider the traps and pitfalls I introduce in my examples and think about similar traps and
pitfalls that might befall you in your own programs. Practice makes perfect, after all.
Since Im assuming that the average reader has no previous Ada experience, Ive completely ignored
Ada 83. Mentioning Ada 83 in a book for someone with no prior knowledge who wants to learn Ada 95
would just be confusing. This leaves readers who are experienced Ada 83 programmers in a delicate
position; the temptation will be to say `oh yeah, I know this stuff! and to skip to the next chapter. If
youre in this situation, be warned that there are lots of new things in Ada 95 that werent in Ada 83. Ive
included a summary of the syntax of Ada as Appendix A which also includes references to the chapters
where each feature of the language is covered; readers with a knowledge of Ada 83 can use this to locate
information about new features of Ada 95. However, you may well find that by skipping chapters to get
to the interesting bits you will miss the descriptions of some of the underlying features, in which case
youll have to go back for another look at the bits you skipped over the first time. Also, since the
examples are built up a little bit at a time, youll sometimes find you need to go back a few chapters to
look at the early stages of the examples.
Ada is a big language. I cant make any claims that this book provides a complete coverage of the
language; the closest youll get to a complete coverage is the Annotated Reference Manual. I can,
however, guarantee that this book is easier to read than the Annotated Reference Manual! The examples
in this book have been carefully chosen to allow me to use them to introduce most of the features of Ada.
The topics Ive omitted are by and large minor details; the final chapter of the book tells you what Ive
left out and why, and it points you towards several different sources of information if you want to find
out about the things I didnt tell you. Despite the omissions, I hope you will find this an interesting,
informative and enjoyable book.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the
constituent files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Part One:
Fundamentals
1. Programming concepts
2. Fundamentals of Ada
3. Statements
4. Procedures, functions and packages
5. Defining new data types
6. Composite data types
7. Exceptions
8. Program design and debugging
This part introduces you to the basic structure of Ada programs. I use a handful of small
examples to illustrate how programs are structured and the way that fundamental
statements can be put together to process data. Much of the work in many Ada programs
can be done by reusing existing program components, so I use the standard packages that
Ada implementations always provide wherever I can. This allows me to use the examples
to illustrate the statements that you have to use to bind these building blocks together into a
working program.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the
Chapter 1:
Programming concepts
A journey of a thousand miles
must begin with a single step.
Proverb
In this book youre going to learn how to write programs in Ada, a general purpose programming
language originally commissioned by the US Department of Defense. The first standard for the Ada
language was finalised in 1983 and was later revised to produce a new updated standard in 1995. The
earlier version is now known as Ada 83 and the later version (the one covered in this book) as Ada 95.
Ada programs use a somewhat stilted kind of formal English in order to specify the operations you want
the computer to perform. You provide some declarations which specify the objects that the program is
going to deal with. You also provide a sequence of statements specifying the actions you want
performed on those objects and the computer will perform them in order, one after the other. A set of
declarations together with a sequence of statements like this makes up a procedure which you invent a
name for. For example, you might write a procedure which clears the screen and call it Clear_Screen.
Computers do not understand Ada directly; the text of your procedure (the source code) must first of all
be translated (compiled) into an internal form by an Ada compiler. The compiler protects you against
your own stupidity by detecting a lot of common errors such as those which arise from typing mistakes,
and these must be corrected before you can go any further. Once a procedure has been compiled
successfully it is added to your program library. Whenever you want to perform that sequence of
statements you can just refer to the procedure by the name youve given it. For example, you might write
another procedure which needs to clear the screen before displaying something on it. All it would have to
do would be to call on the services of the existing Clear_Screen procedure. This means that you dont
have to rewrite things youve already written, which reduces the amount of work you have to do both in
writing the programs and in testing them to make sure they work properly. You can also make use of
procedures written by other people without having to know all the details of how they work.
The next step for turning your procedure from a library unit into a working program is to link it (also
referred to as building or binding it). This combines it with any other library units it refers to. In
addition to any library units you may have written, the Ada language specification defines a number of
standard library units that all Ada systems must provide (e.g. operations like input and output) and it is
quite likely that your procedure will have used one or more of these, either directly or indirectly. The
linker takes your procedure, any library units it refers to, any others that they refer to and so on, and
binds them all together into a single executable program which you can then run.
Of course it is quite possible (almost inevitable, as you will discover!) that the program wont work the
way you expected; the compiler isnt omniscient, so the fact that your program compiled successfully
just means that its a valid Ada program. It doesnt guarantee that its the particular Ada program you
expected it to be, and its up to you to test it thoroughly to make sure it behaves the way you intended it
to. You might hope for a program which displays a five times table but end up with a program that
displays the number 5 over and over again, in which case its back to the drawing board: youll need to
track down the errors (known as bugs in the trade), correct them, recompile, relink, and try again.
Writing a program which produces the right answers from sensible input is only part of the story. You
also need to make sure that it still behaves sensibly with nonsensical input. For example, the program
might expect you to type in a number; what happens if you type in your name instead? If the program
responds with an error message, ignores the erroneous input and carries on working, fine. If the program
crashes (i.e. halts unexpectedly), this is not so good. You may never get the result that the program is
supposed to produce. Even worse, the program might get stuck in an infinite loop where it just ends up
doing the same thing over and over again until it is forcibly halted.
Obviously readability is a major factor in determining how easy it is to maintain a program. You need to
be able to read and understand what the program does in order to change it. As I said earlier, Ada
programs are written using a somewhat stilted formal English. Writing a program is in many ways just
like writing an essay. It can be well- presented and well-structured or it can be a tangled rambling mess.
If you want other people to be able to understand what its all about (or even yourself in six months
time!) you need to make an effort to present the program so that its structure and meaning is easy to
understand. If you dont, youll end up thinking what on earth does this bit do? and wasting a lot of
time trying to understand whats going on.
Maintainability is a measure of how easy it is to change a program. Ideally a single change should
involve changing just one part of the program. If you have to alter several parts of the program to effect
the change theres a risk that youll forget to change some of them or that youll make a mistake in some
places but not in others. For example, consider a payroll program that can handle up to 100 employees. If
the number of employees expands, the program will need changing to handle (say) 200 employees. If
there is a single line in the program that says that the number of employees is 100 and the rest of the
program refers to the number of employees rather than using the number 100 then there will be no
problem. If, however, the value 100 appears as a magic number throughout the program, every single
occurrence of 100 will need changing to 200. The chances of missing one are fairly high. Worse, some
places where 100 is used might be involved in calculating percentages, or it might refer to the number of
pennies in a pound, and we might accidentally end up with 200 pence to the pound or percentages
calculated wrongly. And what about the places where the magic number 99 appears? Should 99 be
Another situation that arises quite often is the need to make the program work on several different
systems. A program should ideally be portable from one system to another; all you do is recompile it
and, hey presto, it works! Unfortunately life is rarely that simple; there are usually differences from one
system to another such as the size of the largest possible number that can be handled or the capabilities of
the display screen. Although you can avoid assuming that your program is running on a system which
has particular characteristics it will sometimes be impossible to eliminate system-specific dependencies.
About the only thing youll be able to to do in such situations is to gather together all the system-specific
parts of the program so that its easy to locate them and change them.
After writing a few programs you normally discover that a lot of the time youre doing the same old
thing that you did before, at least in places. Input and output; sorting collections of data into order; trying
to find things in a collection; there are dozens of common features shared by different programs. After a
while a sensation of dj vu comes upon you and you realise youve done it all before. The second time
you do it better and/or faster than the first time. The third time you do it better and/or faster than the
second. The fourth time you start yawning. How easy is it to reuse what youve already written and
rewritten? Ada provides some mechanisms which allow you to write software which is flexible enough
that you can reuse it fairly easily in a variety of different situations. Learning to make the most of these
mechanisms is always useful but it becomes a necessity as soon as programs start to go above a few
thousand lines of code in size.
This separation also enhances maintainability and portability. As long as all you rely on when you use
something is a specification of what it does, it is easy to change how it does it. This can be done behind
the scenes with no visible effects other than the need to take any existing programs which rely on the
specification and relink them so that they use the new implementation. Portability is also aided by the
fact that you can say this procedure clears the screen as a specification but then provide different
implementations for different systems with different types of screen.
What you end up with is an abstract data type. Abstract data types provide no information about their
internal implementation; instead, all you know is the set of permissible values of the type and the
operations that can be performed on them. This is just like the way that numbers are handled in a
computer. You dont usually have access to the internal representation of numbers in the computers
memory; all you know about is the visible representation of numbers like 3.1416 and the fact that you
can perform operations like addition and subtraction on them. Ada packages provide the mechanism by
which programmers can define their own abstract data types.
1.5 Generics
One of the things that hinders reuse in most programming languages is that algorithms (methods of
solving a problem) are usually intertwined with the type of data that they deal with. For example, a
procedure which sorts a list of numbers into ascending order will normally need changing if you want to
sort a list of names instead. Even if the changes are straightforward, this means that the source code for
the procedure must be available for it to be changed. Commercially available software is not generally
supplied in source form so that trade secrets can be preserved and copyright enforced, so this can mean
reinventing the wheel yet again.
Ada allows you to separate algorithms from the data they deal with by allowing you to write generic
library units. For example, in order to sort a collection of data all you really need to know is how to
compare data items with each other to discover if they need reordering and how to move them to the
correct position in the collection if they are out of order. The same algorithm can be used regardless of
whether youre sorting numbers or names as long as these conditions are met. Ada allows you to write a
generic sorting procedure which, given a data type which satisfies the requirements of the algorithm, will
sort a collection of items of that type. All you have to do to be able to use it to sort items of a particular
type is to specify the item type to be used and the method for comparing two items. You can use the
same algorithm over and over again for different types of data without the need to modify it in any way.
A related problem is dealing with those situations where the different types of account behave
differently. Normally this involves checking the type of account at each point in the program where
different behaviour is possible and then choosing the behaviour appropriate to that account type. A new
account type will therefore involve a number of modifications at different places in the program with the
associated risk of missing a change or changing something incorrectly. Inheritance guarantees that all
bank accounts will share certain common characteristics such as the ability to deposit and withdraw
money. New types of bank account will either inherit the existing behaviour or provide a replacement for
it. In either case it will still be possible to perform those operations. Polymorphism reflects the ability to
provide different implementations of the same operation for different types and to select the correct
behaviour automatically for the actual type of object being dealt with. Rather than seeing a program as a
set of procedures which manipulate data, you can look at it as a set of objects which you can ask to
perform particular operations. Each object responds to a request to perform a particular operation by
executing the version of the operation which is appropriate for it. You dont have to check what type of
account youre dealing with when you say deposit some money; instead, you tell the account that you
want to deposit some money and it does it in the way thats appropriate for the type of account that it is.
As a result, you wont need to change any existing code when you introduce a new type of account.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the
constituent files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 2:
Fundamentals of Ada
What one fool can do, another can.
Silvanus P. Thompson, Calculus Made Easy
-------------------------------------------------------------
--
-- Program: Hello
-- Purpose: Display the message "Hello world!".
-- Author: John English ([email protected])
--
-------------------------------------------------------------
with Ada.Text_IO; -- 1
procedure Hello is -- 2
begin -- 3
Ada.Text_IO.Put ("Hello world!"); -- 4
Ada.Text_IO.New_Line; -- 5
end Hello; -- 6
Type in this program and then compile it and run it to ensure that you can successfully edit, compile
and run programs on your system.
Hello world!
The first few lines of the program are comments which are ignored by the compiler. You can use
comments to add extra descriptive details for the benefit of human readers of the program. A comment
begins with a pair of hyphens; when the compiler sees a pair of hyphens it simply ignores the rest of that
line. The comments at the beginning are used to explain the purpose of the program as well as who wrote it
and when. The other program lines have comments at the end; as in this example, Ill occasionally use
comments to number particular lines so I can refer you to them as I explain what they mean. Programs
should always give explanations of what they do, usually as comments; however, I dont use many
comments in this book because the explanations are in the text.
An Ada program consists of a procedure defining a sequence of actions to be carried out. In this case there
are two actions which are defined by the statements on the lines numbered 4 and 5; line 4 will display the
message Hello world! and line 5 will start a new line on the screen.
Names in Ada can be any sequence of letters and digits starting with a letter. You can also use underline
characters within a name to break it up into words, as in the names Text_IO and New_Line. Anything else
(such as a hyphen or a space) is illegal and the compiler will complain if you try to use anything like that in
a name. Names like Ada.Text_IO might make you think that full stops are allowed as well, but this is
actually the two names Ada and Text_IO joined together, and similarly Ada.Text_IO.Put is a combination
of the three names Ada, Text_IO and Put. Some words like with, procedure, is, begin and end are part of
the language. These are known as reserved words and cant be used as names. The following table gives a
complete listing of the reserved words in Ada:
The compiler doesnt care if you write names in capitals, lower case or a mixture of the two. As far as the
compiler is concerned, Hello and HELLO and hello and heLLO are all just different ways of writing the
same word. Ive adopted a standard convention in this book, which is that reserved words are always
written in lower case (like procedure and begin) and all other names are capitalised (like Hello and
New_Line). You should use the same convention as it makes programs easier to read. One thing you need
to be careful about is mixing letters and digits. The name HELL0 (hell-zero) is not the same as HELLO
although it looks very similar; as far as the compiler is concerned, the letter O and the digit 0 are
completely unrelated. This is one very good reason to avoid using capital letters throughout; its easy to see
that Hello and Hell0 arent the same, and since names cant start with a digit the compiler will complain if
you try to start a name with a zero instead of an O.
procedure X is
-- declarations go here
begin
-- statements go here
end X;
where X is the name of the procedure and the comments mark the places where the declarations and
statements which make up the complete procedure should go. Declarations will be described later. You can
see this general framework on lines 2, 3 and 6 of the Hello world program, although there are no
declarations between is and begin in this particular case. Lines 4 and 5 have been indented slightly to show
that they are enclosed within the begin ... end part of the procedure definition. Although the compiler
doesnt insist on this, consistent use of indentation improves readability. In this book I follow a standard
convention for laying out Ada programs and you should follow this standard in your own programs. There
are also programs available called source code reformatters or prettyprinters which will lay out the text
of a program in the standard manner. If you have something like this to hand you should use it to ensure
your programs are laid out properly. However, you are at liberty to rewrite the program like this:
and the compiler wont complain (although human readers undoubtedly will!). The compiler will, however,
complain by reporting a syntax error if the words and punctuation making up the program are not in the
correct order. Correct spelling and punctuation are essential if you want the compiler to be able to compile
your program successfully; it wont compile a program containing any error, however trivial it may be.
You have to say what you mean and mean what you say in Ada. Appendix A contains a summary of the
syntax of Ada for use as a quick reference if you have trouble remembering any of the details.
Introduce a few syntax errors into your copy of the Hello world program; miss out a semicolon
somewhere or spell something wrongly and see what your compiler says about it. Find out what the
error messages mean.
Notice that each part of the program ends with a semicolon. Lines 1, 4, 5 and 6 each have a semicolon at
the end. Lines 2 and 3 dont because they are not complete in themselves; they are parts of the procedure
definition which has a semicolon at its very end on line 6. This is a general rule in Ada; each complete
construct ends with a semicolon.
The procedures in Text_IO are defined just like the procedure Hello which makes up the main program;
they each specify a sequence of actions for the computer to perform in order to display text on the screen or
start a new line on the screen. So when we run this program, line 4 is executed; this is a procedure call
which causes the procedure Put from Text_IO to be executed. The statements making up the procedure Put
will be executed in sequence, and when we have finished executing them well go back to the main
program and execute line 5. The statements making up the procedure New_Line in Text_IO get executed as
a result, and we then return to line 6, which is the end of the program.
For the sake of convenience, you can add a use clause at the beginning of the program. If you insert the
line:
use Ada.Text_IO;
between lines 1 and 2 (after the with clause) or between lines 2 and 3 (in the declaration section between is
and begin) then the effect is as if all the names from Ada.Text_IO have been declared inside your
procedure and you can just refer to Put and New_Line instead of Ada.Text_IO.Put and
Ada.Text_IO.New_Line. This is what the program ends up looking like:
with Ada.Text_IO;
use Ada.Text_IO;
procedure Hello is
begin
Put ("Hello world!");
New_Line;
end Hello;
However, if you use a name that is very similar to one of the names in Ada.Text_IO you might end up
referring to the wrong thing as a result of a simple typing error. It also becomes unclear where a name is
defined, particularly if you are using a lot of different packages. It is generally a good idea to employ use
clauses fairly sparingly for these reasons.
Packages are not the only things you can access using context clauses; anything in your Ada library can be
accessed in exactly the same way. So, having compiled the procedure Hello, it can be used as a component
of a larger program like this:
This calls Hello three times (hence the name Hello_3) and so it will display Hello world! three times on
three separate lines. The output of this program should look like this:
Hello world!
Hello world!
Hello world!
It gives the same effect as if we had written the statements making up the procedure Hello three times over,
like this:
with Ada.Text_IO;
use Ada.Text_IO;
procedure Hello_3 is
begin
Put ("Hello world!"); -- first call to Hello
New_Line;
Put ("Hello world!"); -- second call to Hello
New_Line;
Put ("Hello world!"); -- third call to Hello
New_Line;
end Hello_3;
Notice that the original version of Hello_3 only uses a with clause to access Hello; it doesnt need to
reference Ada.Text_IO since it doesnt use Put or New_Line or anything else from Text_IO. When you
create a main program from Hello_3, the linker retrieves the compiled code for Hello. Since Hello
references Ada.Text_IO, the linker then retrieves the compiled code for Text_IO; this process continues
until everything referenced by any of the components that the linker deals with has been added to the final
program. Also, there is no use clause for Hello, since its a procedure rather than a package and so doesnt
contain any subcomponents that we can refer to.
2.5 Strings
Unlike New_Line, Put requires us to supply it with some extra information (a parameter), namely the text
to be displayed on the screen. Parameters to procedures are specified in parentheses after the procedure
name; in this case the only parameter is the message Hello world! enclosed in double quotes. A sequence
of characters enclosed in quotes like this is known as a string. Strings are one of the standard data types
that all Ada implementations provide. Note that the quotes are not part of the string itself and will not be
displayed; they are there to tell the compiler where the string begins and ends. If you wanted to display the
quotes as well, you might think that this would do the trick:
Try this and see what your compiler says about it.
If you try this, you will discover that the compiler wont accept it. The reason is that it sees the first two
quotes as marking the start and end of a string which is zero characters long (a null string) and it then
expects the closing parenthesis of the parameter list (or one or two other possible continuations). When it
sees the word Hello after the null string, it doesnt know whats going on and will complain about it. What
you have to do to display a quote mark as part of a string is to write two consecutive quotes, so that the
correct version of the line above would be:
The first quote marks the start of the string; the next two translate into a quote which is a part of the string;
the two after the exclamation mark translate into yet another quote which is part of the string, and the next
one marks the end of the string. This looks messy but its necessary, although fortunately the need to do
this doesnt often occur in practice.
procedure Sum is
First, Second : Integer; -- 3
begin
Put ("Enter two integers: "); -- 4
Get (First); -- 5
Get (Second); -- 6
Put ("The sum is"); -- 7
Put (First + Second); -- 8
New_Line; -- 9
end Sum;
Lines 4 to 9 are the statements which specify what the program will do. The program begins by displaying
a prompt (line 4) so that anyone using the program knows what they should type in. Whenever you want
the user to supply any input you should always display a prompt. There is nothing more frustrating than a
blank screen with the cursor flashing expectantly at you and no instructions!
The next step is to read in the values that the user types at the keyboard. We need to be able to refer to these
later on in the program, so we need to save them somewhere in memory and give them names to refer to
them by. Line 3 declares two variables called First and Second to store the numbers in. Variables are
declared between the word is and the word begin in a procedure. All variables must be declared before they
can be used. We have to tell the compiler the name of the variable so that it will recognise it when it sees it.
If we dont it will report an error when it sees the name First or Second on lines 5, 6 and 8.
We must also tell the compiler what sort of data First and Second are going to hold so that it knows how
much space in memory to allocate for them and what operations we are allowed to perform on them. Ada
provides a standard data type called Integer to hold integer values, so we declare First and Second to be of
type Integer. Note that we dont know what the value of First or Second will be at this point in the program;
declaring a variable simply reserves some space in memory but doesnt set it to any particular value. The
value is said to be undefined meaning that the language doesnt define what it will be. The variable may or
may not hold a valid value; it needs setting to a valid value of the appropriate type before you try to access
it. If you want to, you can set a variable to a specific value when you declare it:
This reserves space for the two variables and sets each of them to a value of 123. If you wanted different
values for the two variables, youd have to use two declarations:
What we want to do in this case is to read values typed in by the user into the variables. In order to do this,
we need to use another package. There is a standard package to handle the input and output of integers
called Ada.Integer_Text_IO, so the context clauses on lines 1 and 2 specify both Ada.Text_IO and
Ada.Integer_Text_IO (Ada.Text_IO is still required to allow us to display strings and start new lines). The
procedure Get used on lines 5 and 6 is one of the ones provided by Ada.Integer_Text_IO; it reads a number
from the keyboard and stores it in the variable whose name is given as its parameter. The effect of lines 5
and 6 will be to read two numbers typed at the keyboard and store them in the variables First and Second.
You can then get at the stored value by referring to the variable by name. You have to separate the two
integers with one or more spaces when you type them in, e.g. 123 456; if you just type 123456 without any
separating spaces, Get will just think its all one number. Alternatively you can start a new line after typing
in the first number; a line break is just as good as a space for separating the two integers. If you dont type
in integer values as requested, the program will halt immediately and display an error message.
Try typing something which isnt an integer (e.g. XYZZY) and see what error message the program
produces. What happens if you type in the first number correctly and the second one incorrectly?
Finally, lines 7, 8 and 9 display the result. Line 7 displays a string, and line 8 will display the integer result.
If you run the program and type in the values 123 and 456, this is what youll see on the screen:
Lines 7 and 9 use the version of Put defined in Ada.Text_IO for displaying strings, while line 8 uses the
version of Put defined in Ada.Integer_Text_IO for displaying integers. The compiler is able to distinguish
between the two different usages of the name Put simply by looking at the type of value supplied as a
parameter; if its a string we must be referring to Ada.Text_IO.Put which requires a string as a parameter,
whereas if its an integer we must be referring to Ada.Integer_Text_IO.Put which requires an integer as its
parameter. This is handy, since otherwise every single procedure would have to be given a different name
(e.g. Put_String and Put_Integer) which would mean having much more to remember.
The parameter to Put on line 8 is an integer expression, i.e. a set of values being combined in some way to
produce a new value. In this case the values that were stored in the variables First and Second by the calls
to the Get procedure on lines 5 and 6 are added together using + to produce another integer value. Since
the compiler has been told that First and Second are integers, it knows that + is a legitimate operation and
that the result will be another integer. This result is then passed directly to Put as the value to be displayed.
A full set of arithmetic operations is provided in Ada; subtraction is symbolised by , multiplication by
* (since would look like the name X) and division by /. There are also a few others which will be
discussed in chapter 5.
of time trying to solve a problem for which a ready-made solution already exists. This means that you need
to be able to read and understand the specification of packages like Ada.Text_IO. Ada.Text_IO contains a
set of declarations for the procedures and other things it provides; a complete listing of Ada.Text_IO is
provided in Appendix B. This is what the declaration for Put in Ada.Text_IO looks like:
Note that this is only a specification of the procedure; the implementation details between is and end have
been omitted. The implementation is kept in a separate package body which will already have been
compiled and added to your program library, so you dont get to see it. You dont need to; all you need to
know is what the specification tells you, namely that there is a procedure called Put which takes one
parameter called Item. The parameter must be a String; the reserved word in indicates that Item is an input
to Put. The value you supply for Item will be copied into Put so that Put can display it on the screen.
You can also use the parameter name when you call the procedure, like this:
This can be very useful when a procedure requires several different parameters; it helps to make it clear
what the parameters are for.
There are actually several procedures in Text_IO called Put, but they all have different parameter
specifications; the compiler can work out which one youre referring to by looking at the type of parameter
you supply in the procedure call. This is known as overloading; a procedure name is overloaded with
more than one meaning.
As you can see, New_Line has a parameter called Spacing. The parameter must be a value between 1 and
some upper limit whose exact value may vary from one system to another (its implementation defined).
This is specified by the type Positive_Count, which is also defined in Text_IO. To find the exact range of
values for Positive_Count, look in the Text_IO package provided with your compiler.
We didnt have to supply a parameter to New_Line when we called it earlier; the := 1 in the specification
is a default value which will be used if we dont supply a value for Spacing. We could have called
New_Line like this:
and the effect would have been exactly the same. If we want to start two new lines in quick succession (i.e.
leave a blank line on the screen) we could call New_Line like this:
If you look at the specification of Ada.Text_IO you will also discover the following procedure
specification:
Put_Line is the same as Put except that it starts a new line after displaying the string supplied as its
parameter; in other words, Put_Line is the same as Put followed by New_Line. This means that you could
rewrite the Hello world program like this:
Input and output of integers is a bit more elaborate than it is for strings. The declaration of Put in
Ada.Integer_Text_IO looks like this:
What this means is that there are two extra parameters called Width and Base which can be used when
displaying integers. Field and Number_Base are just the names of some more data types defined in
Integer_Text_IO. The Width parameter must be a value between 0 and some maximum value which varies
from compiler to compiler (its also implementation defined); it determines the number of characters which
will be displayed. The default is large enough to accommodate the largest possible value for an Integer
(typically either six or 11 characters wide); this means that small integers will be displayed with some
blank space in front. By specifying some other value you can get exactly the width you want; if the number
is too big for the width you specify it will take up as much space as necessary to display its value with no
leading spaces. In particular, specifying Width=>1 will always display an integer with no extra spaces.
Here are some examples:
Base is a value between 2 and 16 which specifies the number base to use for displaying the value, so that
Base=>2 specifies binary output and Base=>16 specifies hexadecimal. The default value (called
Default_Base) is defined to be 10, i.e. values will be displayed in decimal unless you specify otherwise.
Note that non-decimal numbers are displayed as based numbers, i.e. the base followed by the value
enclosed in hash marks (# .. #).
Here are some examples which use Width and Base together:
Note that you can specify parameters in any order if you use their names; the compiler is smart enough to
know what order they should go in and deal with them appropriately. You dont have to use their names (as
shown in the last example above), but if you dont you must specify the values in the correct order; its
often easier and more readable to specify them by name.
This tells us that Get has a parameter called Item which is an output from the procedure (as specified by
out); you have to supply an Integer variable to store the output in when you call Get. You have to supply a
variable name for an out parameter so the procedure can store the output it produces in it. Input parameters
dont suffer from this restriction; any value of the correct type can be provided for an input parameter (e.g.
the value of the expression First + Second on line 8 of the Sum program). Get also has an optional
parameter called Width; a value other than zero specifies the exact number of characters to be read
(although if there arent enough characters on the line it will just read up to the end of the current line).
Procedures are covered in more detail later, but as you will have gathered by now, procedures and packages
play a fairly central role in writing Ada programs. Dont worry about the details too much for now; it helps
to be able to look at procedure specifications in packages and work out how to use them, but the nitty-gritty
details involved in writing this sort of thing can wait until chapter 4.
Exercises
2.1 Ada compilers only allow a limited range of integers; nine or ten digits is typically the most you can
expect the compiler to handle. If you exceed the maximum value it can handle, the program will halt
with an error message. Find out what the largest integer is that your system can handle by typing in
larger and larger values for the Sum program until you get an error.
2.2 Write a program which uses Put_Line to display your initials in giant letters on the screen using
asterisks, something like this:
*********** ********
* *
* *
* ******
* *
* * *
**** ********
2.3 Modify the Sum program to display the difference, product and quotient of the two numbers you type
in as well as the sum. Think of a sensible name for the modified program instead of Sum.
2.4 Write a program which reads in three integers representing the length, width and height of a box in
centimetres and displays the volume and the surface area of the box. Be sure to check your results are
correct. For example, a box whose size is 1cm x 2cm x 3cm has a volume of 6 cm3 and a surface area
of 22 cm2.
Previous
Contents
Next
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the
constituent files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 3:
Statements
The statements was interesting, but tough.
Mark Twain, The Adventures of Huckleberry Finn
3.1 If statements
3.2 Assignment statements
3.3 Compound conditions
3.4 The case statement
3.5 Range tests
3.6 The null statement
3.7 Loops
3.8 The calculator program revisited
3.9 Exception handling
Exercises
3.1 If statements
Lets try a slightly more elaborate example than the ones weve seen so far. This one asks the user
whether its morning or afternoon and then replies Good morning or Good afternoon as appropriate.
Heres what it looks like:
Line 1 declares a variable called Answer to hold the answer that the user types in response to the
question asked on line 2. The answer will be a single character ('m' or 'a'), so Ive declared it to be a
variable of type Character, which is another standard data type. Character variables are capable of
holding a single character. The answer is read in using a version of Get defined in Ada.Text_IO which
has an output parameter of type Character.
Lines 4 through 8 are an if statement which allows us to choose between two alternative courses of
action. Like a procedure definition it is a compound construction with a semicolon at the very end of it
on line 8 and no semicolons on either line 4 or 6. Compound constructions like this always end with end
whatever-it-is in Ada, so that procedure X ends with end X and if ends with end if. The
statements on lines 5 and 7 are indented to make it visually obvious that they are enclosed by the if
statement.
When the if statement is executed, the condition after the word if is tested and, if it is true, the statements
between then and else are executed, which in this case is the single statement on line 5. If the condition
is false, the statements between else and end if (in this case the single statement on line 7) will be
executed. The effect is that if the value of Answer is the letter m, the message Good morning! is
displayed, otherwise the message Good afternoon! is displayed. Once the if statement has been
executed (i.e. either line 5 or line 7 has been executed), execution continues with whatever follows the if
statement. In this case it is line 9, which is the end of the program.
Note that you have to use single quote marks to enclose characters; 'm' is the letter m as a value of type
Character, whereas "m" is a string which is one character in length. The compiler takes this apparently
trivial difference quite seriously, since the particular style of quote is used to distinguish values of type
Character from values of type String, which in turn determines what operations can legitimately be
performed on them.
Try using double quotes instead of single quotes ("m" instead of 'm') and see what your compiler
says about it.
In its present form the program doesnt quite do what we really want. If you type anything other than the
letter m the program will respond with a cheery Good afternoon!. Ideally we would like to check that
the answer is in fact an a if it is not an m and display a less cheery error message if it isnt. We can do
this by using another if statement instead of the existing line 7:
This shows that the statements contained within an if statement can be any statements at all, including
further if statements. Note that each if statement has its own corresponding end if. If you have a lot of if
statements nested inside one another you can end up with an awful lot of end ifs, as well as indentation
problems as the if statements are going to be indented further and further to the right. To get around this,
we can write the if statement above a different way:
The reserved word elsif allows you to specify a secondary condition as part of the same if statement.
Since its all a single if statement now, only a single end if is required at the very end and there is no
problem with indentation. You can have as many elsif parts in an if statement as you want, but there can
only ever be one else part which must come at the very end and is only executed if all the conditions
specified after if and elsif are false. You can also leave out the else part completely if you dont want to
do anything when the conditions you specify are all false.
Note the missing e in elsif! A common beginners mistake is to spell it elseif, but the spelling was
deliberately chosen so that if the words else and if accidentally get run together as elseif as a result of
missing out the space between the two words, the compiler will immediately spot it as an error.
The if statement on lines 1 to 5 checks for the letter M or A and changes the value of Answer to m or a as
appropriate. It does this by assigning a new value to Answer on lines 2 and 4. The assignment
statement
Answer := 'm';
stores the letter m into the variable Answer, replacing the existing value of Answer. The symbol := is
usually pronounced becomes, so we can read this statement as Answer becomes m. You must give a
variable name on the left of :=, but you can have any expression you like on the right hand side as long
as it produces a value of the correct type when its evaluated.
Note that there is no else part in this if statement. If Answer is the letter M, line 2 is executed; if it is the
letter A, line 4 is executed; and if it is anything else, we dont do anything. As I mentioned earlier,
leaving out the else part of an if statement simply means that you do nothing if none of the conditions
The first line of this revised if statement checks if Answer is an m and also checks if it is an M. If either
condition is true the message Good morning! is displayed.
The or operator allows us to combine more than one condition into a single compound condition which
is true if either or both of the subconditions are true. It is tempting to try to write the first line of the if
statement as follows:
Try this and see what sort of error your compiler reports.
The reason is that or requires something which evaluates to either true or false (a Boolean expression)
on both its left and its right hand sides. Boolean is another one of Adas built-in types, and is named after
the English logician George Boole who first formalised the notion of an algebra of truth values. The =
operator compares two values of the same type and produces a Boolean result (true or false) so all will be
well as long as you use an expression of the form A = B on both sides of the or operator. The condition
above has a Boolean expression on its left but a value of type Character on its right, so the compiler will
be justifiably sceptical about it. Ada compilers are very strict about type-checking since confusion about
types generally indicates muddled thinking on the part of the programmer; the Ada view of data types
will be explored in more detail later on.
Depending on the value of Answer, one of the three alternatives of the case statement will be executed.
The vertical bar | can be read as meaning or so that choice 1 will be executed if the value of Answer is
M or m and choice 2 will be executed if the value of Answer is A or a. Choice 3 (others) is executed if
all else fails. The others choice must be the last one and is equivalent to the else part of an if statement. It
is only executed if none of the other choices apply. A case statement must have a choice for every
possible value of the controlling expression between case and is, so an others clause is usually
necessary.
case Answer is
when 'A' .. 'Z' | 'a' .. 'z' =>
This says that if the value of Answer is in the range A to Z or the range a to z, the message Its a letter!
will be displayed. If it isnt, the message Its not a letter! will be displayed instead.
You can also test if a value is in a particular range using the operators in and not in. The case statement
above could be rewritten as an if statement like this:
The null statement is provided for situations like this one where you have to say something but dont
want to do anything. A null statement has no effect at all except to keep the compiler happy by telling it
that you really do want to do nothing and that you havent just forgotten something by accident.
3.7 Loops
At the moment you only get one chance to answer the question that the program asks. It would be nicer if
you were given more than one attempt. Here is a program that does that:
This program contains a loop statement which starts at line 1 and ends at line 4. Again, its a compound
statement; it starts with loop on line 1 and ends with end loop and a semicolon on line 4. The sequence
of statements it encloses will be repeated over and over again when its executed. It will only stop
repeating when you execute one of the exit statements on lines 2 and 3. The exit statement terminates the
loop and execution of the program continues at the point after end loop. In this case it is line 5, the end
of the program.
Compile and run this program and then type something like XYZZY in response to the prompt.
What happens, and why?
Quite a lot of the time you want to exit from a loop when a particular condition becomes true. You could
do this using an if statement:
if This_Is_True then
exit;
end if;
but its a common enough requirement that Ada provides a special form of the exit statement:
Heres another way of writing the same program as before which illustrates the use of an exit when
statement:
The previous version displayed the message Good morning! or Good afternoon! from within the loop;
here, the loop just checks whether the answer is valid. As soon as it is valid, the exit statement terminates
the loop and execution continues at the next statement, namely the if statement at line 1 which is now
responsible for displaying Good morning! or Good afternoon!. By the time we get to line 1, we know
that Answer is either an m or an a in either upper or lower case and so the if statement only has to test if
its an m or an M; if it isnt it must be an a or an A.
Apart from anything else, this shows you that there is more than one way to solve a particular
problem. Can you think of any other ways of solving it?
In many cases the exit statement is the first statement in a loop; you will often want to test that
everythings all right before you do anything else:loop exit when End_Of_File; -- i.e. when there is no
more input -- get some input and process itend loop;
This is a common enough situation that theres a special form of the loop statement to cater for it:
The operator not inverts the sense of a condition; if a condition X is True then not X is False and vice
versa. Note that the condition in a while loop tells you when to repeat the loop whereas the condition in
an exit when statement tells you when to exit from it, so a loop that begins exit when X gets rewritten
as while not X loop ....
Another common requirement is to repeat a loop a fixed number of times. This is a frequent enough
situation that Ada provides another form of the loop statement to handle it (a for loop). Heres an
example which displays a line of 20 asterisks:
The range 1..20 specifies how many times the loop will be executed. The control variable N will take on
successive values from 1 to 20 each time around the loop (i.e. in this case, it will always give the number
of times the loop has executed so far). N doesnt need to be declared elsewhere; it is automatically
declared by its appearance in the loop heading. For loops are dealt with in more detail in chapter 6.
Main_Loop:
loop
...
end loop Main_Loop;
The label is a name followed by a colon immediately before the loop statement. Note that if you use a
loop label, the name must be repeated after end loop as in the example above.
The main reason for providing a loop label is so that you can specify which loop an exit statement should
exit from. This allows you to exit several loops at once:
Outer:
loop
Inner:
loop
...
exit Outer when Finished; -- 1
end loop Inner;
end loop Outer; -- 2
The exit statement at line 1 will exit from both the inner and outer loops, and execution will continue at
line 2 (after the end of the outer loop). This is something which is rarely required in practice; if it does
seem to be necessary, its worth having a good think about your design since there are usually better
ways to achieve the same effect.
One problem with this is that the operator is taken to be the first character after the first integer, which
doesnt allow for any separating spaces. The integers can be preceded by spaces because of the way that
Get works for integers, so that 123+ 456 will be accepted but 123 + 456 wont be. The answer is to
use a loop to skip spaces between the first integer and the operator:
Get (First);
loop
Get (Operator);
exit when Operator /= ' ';
end loop;
Get (Second);
The operator /= means not equal to, so the exit statement will be executed when Operator is not equal
to a space. This means that as long as it is a space, well go round the loop and get another character, thus
ignoring all spaces.
An even better idea is to extend the program further so that it can read expressions involving more than
one operator, e.g. 1+2+3. This will involve reading the first integer and making it the result, then
reading successive pairs of operators and integers and adding them (or whatever) to the result. This will
give a strictly left-to-right evaluation, so that 1+2*3 will come out as 9 rather than 7 as you might
expect; in a later chapter I will show you how to deal with issues like doing multiplication and division
before addition and subtraction. To simplify matters I will require the user to type in a full stop to
terminate the expression.
This is a slightly larger program than the previous ones; rather than just showing you the complete
program Im going to take you through a step-by-step design process. Reading and understanding the
programs Ive shown you so far is important for getting to grips with the facilities that Ada provides, but
at some point you have to start writing your own programs and for this you need some tips on how to get
started. A lot of people find it easy to understand a program once its been written but find it hard to
know where to start if they have to write it themselves.
In general you can break any programming problem down into three main components: some
initialisation to get things ready at the beginning, followed by the main processing involved, followed by
some finishing off at the end. In this case the initialisation will involve displaying a prompt and reading
an arithmetic expression, the main processing will involve evaluating the expression that the user has
typed in, and the finishing off at the end might just involve displaying the result. Well need an integer
variable for the result to be displayed. This gives the following as a first stab at the program:
begin
Put ("Enter an expression: ");
-- process the expression typed in by the user
Put (Result, Width => 1);
New_Line;
end Calculator;
The next thing to do is to decide how to break down the main processing. A good way to start is to
consider the structure of the input that the program will be expected to deal with. Here are some samples
of the input Id expect this program to accept:
2.
2+2.
2+2*2.
2+2*2-2.
What we have here is an integer followed by any number (zero or more) of arithmetic operators each of
which is followed by an integer, followed finally by a full stop. An old programming rule of thumb says
that the structure of a program tends to reflect the structure of its input; in this case well need to read the
first number, then repeatedly process operators and the numbers which follow them until we reach a full
stop. So this gives us a sequence of two steps: read the first number and then process the rest of the
expression. In the case where there are no operator/integer pairs after the first number, the result will be
the first number. This leads to the conclusion that the first number should just be read into the variable
Result:
Processing operator/integer pairs is a repetitive activity, so well need a loop statement. With any loop
you need to ask yourself when the loop will terminate; in this case it is when the final full stop is read, or
when something unexpected is read (which should be reported as an error). The result wont need to be
displayed if an error occurs, so we can display it when the full stop is encountered, rather than at the end
of the program as Ive got it above. This can be done by moving the call to Put into the loop and
Inside the loop well need to read the next character, which should be either an operator or the
terminating full stop, so well need a character variable (which Ill call Operator) to store it in:
As in the previous program well want to ignore spaces before the operator; to do that, Ill just use the
code which I showed you earlier without any further comment:
Processing the rest of the operator/integer pair involves reading the integer (which means we need
another Integer variable) and then applying the operator to the result so far and the integer that weve just
read:
Applying the operator involves a choice between a number of alternatives: if its an addition operator we
want to add the numbers together, if its a multiplication operator we want to multiply them, and so on.
Since we have a choice between several alternatives we have to use either an if statement or a case
statement. As you saw earlier, a case statement is a convenient solution in this case where all the choices
depend on a particular value, in this case the value of Operator. The first number is in Result and the
second is in Operand, so well need to evaluate Result+Operand, Result-Operand or whatever. This will
give us a new result which needs to be stored in Result so that its ready to be displayed when we get to
the end of the program, so we need each choice to be an assignment statement along the lines of:
Note that the old value of Result is used on the right hand side of := to calculate the new value of
Result. The right hand side of the assignment is evaluated by adding the old value of Result to Operand;
this value is then stored in Result, replacing the old value. This idiom is commonly used to add 1 to the
existing value of a variable, like this:
A when others choice will be needed in the case statement to cope with the fact that Operator might be
any Character value, not just one of the four operators were looking for. The good news is that the
compiler would complain if we forgot this little detail. If we get to the when others choice it means
theres an error in the input. An appropriate response to this is to display an error message and get out of
the loop with an exit statement. So here at last is the final program:
If you type 1+2*3. in response to the prompt, what will happen is that line 1 will read the value 1 into
Result. Line 2 is the start of the main loop; the first thing inside this loop is another loop (line 3) to skip
over any spaces in front of the operator character. We will end up at line 4 with Operator holding the
character '+'. Lines 5 and 6 will display the result and exit the main loop when Operator is a full stop, but
we havent got to that stage yet. So line 7 will read the value 2 into Operand, and then the case statement
will execute line 8 based on the value of Operator. Line 8 calculates the value Result+Operand (i.e. 1+2)
and stores the result (i.e. 3) in Result. The upshot of this is that Result has been altered from 1 to 3.
After line 8 has been executed we go round the main loop a second time. Operator ends up holding the
character '*' and Operand ends up holding the value 3. The case statement executes line 9, which
multiplies Result (3) by Operand (also 3) to give a new value of 9 for Result. Around the loop again, and
Operator ends up holding a full stop at line 4. The result (9) is then displayed by line 5 before exiting
from the main loop at line 6.
If an invalid operator character is typed in (e.g. 1&2.) the section of the case statement at line 10 gets
executed, which displays an error message. Line 11 then exits from the main loop. Notice how important
it is to think about what can possibly go wrong and to deal with it in a sensible way; its easy to write a
program that gives the right answer for valid input, but its much harder to write a program that can cope
sensibly with bad input as well.
where the program expects an integer, the program will halt with an error message. If youre unlucky it
will just say something like unhandled exception; some compilers are more helpful, and will also tell
you that the error was an exception called Data_Error, and possibly tell you which line of the program
you were at when it happened. A Data_Error means that the input is in the wrong format; another
common one is Constraint_Error, which youll get if you go outside the range of values allowed for
Integer on your system (try 1000000*1000000*1000000, which will almost certainly be too big to
handle).
A properly designed program should be able to cope with any input at all, not just correct input. To
manage this we need to trap Constraint_Error and Data_Error exceptions and deal with them sensibly.
Ada allows us to provide exception handlers to specify what happens if an exception occurs. This is a
topic Ill return to in more detail later, but its worth a brief introduction before we go any further so that
youll be able to start making the programs you write more robust.
You can put an exception handler into any block of statements enclosed by begin and end, e.g. a
procedure body:
procedure X is
begin
-- your code goes here as usual
exception
when Some_Exception =>
Do_This;
end X;
where Some_Exception is the name of an exception you want to handle and Do_This is the action you
want to take. The action can be any sequence of statements; it can be a null statement which does
nothing, which will have the effect of ignoring the exception, or it can be something more elaborate. In
this case a sensible action might be to print out an error message when a Constraint_Error or a
Data_Error occurs. Heres how to do it:
procedure Calculator is
Result : Integer;
Operator : Character;
Operand : Integer;
begin
Put ("Enter an expression: ");
... code to process the expression as before
exception
when Constraint_Error =>
Put_Line ("Value out of range");
when Data_Error =>
The exception handler section goes at the very end; its ignored if there arent any errors. If a
Constraint_Error or a Data_Error is reported (or raised, to use the correct terminology), you immediately
end up at the appropriate exception handler and do what it says. Once youve done this, youre at the end
of the procedure and the program terminates.
If you want the program to give the user another chance rather than terminating you need to be a bit more
subtle. Heres how you can safely read a value into an Integer variable called X:
loop
begin
Put ("Enter an integer: "); -- 1
Get (X); -- 2
exit; -- 3
exception
when Constraint_Error | Data_Error =>
Put_Line ("Error in input -- please try again."); -- 4
Skip_Line; -- 5
end;
end loop; -- 6
Note that you cant put an exception handler directly between loop and end loop; you have to put begin
and end around the section that you want to provide exception handling for, and then put the exception
handler section immediately before end. This is the only case in Ada where end is not followed by
something to say what it is the end of.
What happens here is that line 1 displays a prompt and line 2 attempts to read an integer. If an exception
is raised by Get, you wont get to line 3; instead, youll be whisked off to line 4 which displays an error
message. Line 5 calls a procedure Skip_Line from Ada.Text_IO to ignore the rest of the current line of
input so the user will have to type another line. If you dont call Skip_Line after a Data_Error youll just
end up reading the same bad data from the current line (which wont have been read since it wasnt
valid).
Try leaving out the call to Skip_Line and see what happens.
After this youll be at line 6, the end of the loop, so youll go around and redisplay the prompt and get
another line of input. When the user types in a valid value for X youll carry on past line 2 to line 3,
which will exit from the loop.
Note also that you can handle several exceptions with a single handler by separating them by a vertical
bar (|) in the same way as you would specify multiple choices in a case statement. Also as in a case
statement, you can provide a catch-all handler by specifying when others:
exception
when others =>
Do_Something; -- handle every exception the same way
As in a case statement, when others must come last if you have more than one exception handler. It
handles any exceptions not dealt with by the other handlers. If you use it as the only handler it will deal
with any exception that occurs. I dont recommend using when others unless you really need to; it might
disguise any real errors in your program due to undiscovered bugs which would otherwise be reported as
unhandled exceptions.
If you want an exception to be handled in different ways in different places, you need to enclose each
such place in a begin ... end block. For example, if you have two assignment statements which could
each raise a Constraint_Error:
you could enclose each one in a separate block with its own handler like this:
begin
A := A ** 2; -- might raise Constraint_Error
exception
when Constraint_Error =>
Put_Line ("Assignment to A failed");
end;
begin
B := B ** 2; -- might raise Constraint_Error
exception
when Constraint_Error =>
Put_Line ("Assignment to B failed");
end;
Exercises
3.1 Modify the Greetings program to say Good evening! in the evenings as well.
3.2 Modify the calculator program so that after evaluating an expression it asks the user if he or she
wants to evaluate another expression. If the answer is y or Y (yes), evaluate another expression;
if its n or N (no) exit from the program.
3.3 Write a program which asks the user to pick an animal from a list that you display (cat, dog,
elephant or giraffe) and then asks Is it a household pet? to distinguish cats and dogs from elephants
and giraffes. If the user says its a household pet, ask if it purrs; if not, ask if it has a long neck.
Finally, tell the user which animal you think was chosen. Try extending the program to include a
few more animals.
3.4 Write a program to count the number of vowels (A, E, I, O or U) in its input. Allow the user to type
in a sequence of characters (as many as they like) ending with a full stop and then display the
number of occurrences of each vowel as well as a grand total. This will involve using a set of
integer variables which are set to zero at the start of the program. You will then need to add 1 to the
appropriate variable whenever a vowel is typed in. Ignore case distinctions, so that a is treated as
meaning the same as A.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the
constituent files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 4:
Procedures, functions and packages
All are but parts of one stupendous whole.
Alexander Pope, An Essay on Man
Note that I use the British format for dates throughout this book (day, month, year). If you find this confusing,
reorder the calls to Get to read dates in the format youre used to (e.g. month, day, year for American readers).
For the sake of simplicity Ill assume for now that the user will always type in a valid date. Lets see how Zellers
Congruence works first. Heres the formula:
Here M is the number of the month, D is the day, Y is the last two digits of the year number and C is the century (the first
two digits of the year number). Integer division is used, so that 19/4 = 4 rather than 4.75. The mod operation gives the
remainder of an integer division, in this case the remainder from dividing by 7, i.e. a value between 0 and 6. Things are
made slightly more complicated by the fact that the months have to be numbered starting with March as month 1; January
and February are treated as months 11 and 12 of the previous year. We therefore need to adjust the month and year like
this:
The result of the formula is a number between 0 and 6, where 0 means Sunday and 6 means Saturday. Lets see how this
works using January 25th 1956 as an example. The value of D is 25 and C is 19. January 1956 counts as month 11 of
1955, so M is 11 and Y is 55. This gives us:
It will help to introduce an extra variable for the value C since its used twice in the formula above. Heres the latest
version of the program:
The Ada version of Zellers Congruence is practically identical to the original except that the single-letter variable names
in the original have been replaced by longer names. Also, all multiplication operators must be specified explicitly; while
the original formula had 26M in it, a direct Ada equivalent would have to be 26*M. The order of operations is the same as
in ordinary algebra; multiplication and divisions are done before additions and subtractions, with parentheses being used
to alter the order of evaluation where necessary.
The other thing to notice is that the value of Century is calculated after the if statement. This is because the if statement
might change the value of Year; January 2000 is treated as being month 11 of 1999, so the value for Century will be 19
until March 2000. Likewise Year only gets trimmed to its last two digits after the first two digits have been extracted into
Century. The order of events is quite important here.
The first thing to do is to decide what inputs it requires and what outputs it produces. There are three inputs: the day, the
month and the year. There is a single output, which is a value between 0 and 6. This is enough information to produce a
procedure specification:
What we have here is the specification for a procedure called Day_Of (since Zeller would be a bit cryptic) which has four
parameters. Three of them (Day, Month and Year) are Integer inputs, and the fourth (Result) is an Integer output.
Designing a specification is always a good way to start writing a procedure, but what we actually need here is a procedure
body (i.e. including the implementation details between is and end) rather than just a specification: specifications are
generally only used in connection with packages. Writing the body is fairly straightforward; the necessary code can be
extracted from the previous program:
Notice that the parameter declarations look just like variable declarations, and that they can in fact be treated as if they
were variables inside the procedure body. When the procedure is called, the actual values specified for the input
parameters are copied into the corresponding parameter variables in the procedure. The procedure body is then executed,
and it can make use of the input parameters just as if they were ordinary variables except that it cant alter them. Since
procedures cant alter the value of in parameters, we need to copy Month and Year into ordinary variables that can be
altered. Output parameters behave just like uninitialised variables; you have to set them to some value before you can use
them. At the end of the procedure, the values of the output parameter variables will be copied into the variables which
were used for the output parameters in the procedure call. In this case, the value of Result will be copied into the variable
which was supplied for the Result parameter.
As well as in and out, we can specify parameters as in out meaning that they act as both inputs and outputs. Like an out
parameter you have to supply a variable name for an in out parameter when you call the procedure so that the procedure
has somewhere to store its output; the difference is that the original value of the variable before the procedure call is
passed into the procedure as an input. Inside a procedure, an out parameter behaves like an uninitialised variable (you
dont get the original value of the variable supplied when the procedure is called) but an in out parameter behaves like an
initialised variable; it is initialised to the value of the variable supplied as the actual parameter when the procedure is
called.
Heres how you could call the procedure to find out the day of the week for January 25th 1956 and put the result in a
variable called R:
This will copy the values 25, 1 and 1956 into the three input parameters Day, Month and Year and then execute the
procedure. At the end of the procedure the value of the output parameter Result will be copied into the variable R.
You could also use the parameter names in the procedure call for the sake of readability:
Day_Of (Day => 25, Month => 1, Year => 1956, Result => R);
If you mix the two styles in this way the named parameters must come after any without names. One of the advantages of
using named parameters is that you can specify them in any order and the compiler will be able to arrange them into the
correct order automatically:
Day_Of (Result => R, Month => 1, Day => 25, Year => 1956);
We call Day_Of and put its result in a variable R. Then we have to pass R as a parameter to Put. Two statements are
needed, not to mention the extra intermediate variable R. An alternative solution would be to define Day_Of as a
function. A function is just like a procedure except that it evaluates to a result that can be used as part of an expression;
for example, a function call could be used as the value for an input parameter in a procedure call (or a call to another
function), or as part of an expression on the right hand side of an assignment statement. A function is restricted to
producing a single result, and only in parameters are allowed. Functions and procedures are collectively referred to as
subprograms.
Since there is a single result produced by the Day_Of procedure it would be easy to turn it into a function. This would
allow us to write something like this:
where the result of the function call is used directly as the parameter to Put. This eliminates the extra statement and the
extra variable.
Heres how you could rewrite the procedure specification above as a function specification:
Instead of an out Integer parameter to store the result in, you specify return Integer after the parameter list to show that
the function returns an Integer as its result. Note that you only have to say Integer instead of in Integer in the function
parameter declaration, since you arent allowed out or in out parameters to functions. Only in parameters are allowed, and
as a consequence you dont need to specify in.
C : Integer;
begin
if M < 3 then
Y := Y - 1;
M := M + 10;
else
M := M - 2;
end if;
C := Y / 100; -- first two digits of Year
Y := Y mod 100; -- last two digits of Year
return ((26*M - 2)/10 + Day + Y + Y/4 + C/4 - 2*C) mod 7;
end Day_Of;
The return statement specifies an expression whose value is the value to be returned as the result of the function. When
you execute a return statement, you evaluate the expression and then immediately exit the function. For example, if you
called the function like this:
the values of X, Y and Z would be copied into the parameters Day, Month and Year. The body of the function would then
be executed up to the return statement; the value of the expression in the return statement would then be returned from
the function and assigned to the variable I.
return;
All this does is to exit immediately from the procedure it is used in and return to the point where the procedure was called
from.
if M < 3 then
Y := Y - 1;
M := M + 10;
else
M := M - 2;
end if;
C := Y / 100; -- first two digits of Year
Y := Y mod 100; -- last two digits of Year
return ((26*M - 2)/10 + D + Y + Y/4 + C/4 - 2*C) mod 7;
end Day_Of; -- 4
At this point it is worth mentioning something about the declarations of the variables inside the Day_Of function. When a
variable declaration is executed (or elaborated, to use the correct technical term) some space in memory is reserved for
that variable and initialised if the declaration specifies an initial value. The variable exists until the end of the begin ... end
block which follows it, and the space allocated to it is then reclaimed so that it can be used elsewhere if necessary. This
means that when Day_Of is called at line 5 the variable D inside Day_Of is created when the declaration at line 3 is
elaborated. D is then destroyed at the end of Day_Of (line 4) before returning to line 5. The region from the declaration to
the end of the block it is defined in is known as the scope of the variable. Since the variable only exists inside its scope, it
cannot be accessed from elsewhere (i.e. from another subprogram) since it might not exist at the time; thus D cannot be
accessed from the main program since it only exists during the call to Day_Of at line 5. In other words, variables declared
in a subprogram are local to that subprogram and are only accessible from inside that subprogram since this is the only
time their existence is guaranteed. Parameters are treated the same way; they are effectively local variables which are
created during the subprogram call and destroyed when the call is complete.
Variables in different scopes can have the same name as each other; this means that you dont have to worry about
avoiding names which are used in other subprograms. Thus there is no confusion about using Day as the name of a
variable at line 1 and as the name of a parameter at line 2; the variable called Day at line 1 is local to the main program
while the parameter called Day at line 2 is local to Day_Of. When you refer to a name, the most local object of that name
is accessed, so that the version of Day being referred to at line 3 is the parameter declared at line 2 whereas the version
referred to in line 5 is the variable declared at line 1.
Each time a subprogram is called, the local variables it declares are created. This means that each variable will be
reinitialised every time the subprogram is called if its declaration specifies an initial value; if not its value will be
unpredictable. There is no guarantee that a particular variable will be allocated the same space in memory each time the
subprogram is called, so there is no way of knowing what the contents of the variable will be unless the declaration
explicitly specifies an initial value. In particular, variables do not retain their values from one subprogram call to the next.
D, M and Y will be initialised every time Day_Of is called; C will always start off with an unpredictable value. If you do
want to retain a value from one call to the next, you have to use a variable declared in the surrounding scope. In this case a
variable declared in the main program before Day_Of could be used to hold a value from one call of Day_Of to the next
since its scope would be that of the main program; since that scope includes Day_Of it would be accessible inside Day_Of
but it would not be destroyed until the end of the main program was reached.
Locality of variables is useful for isolating errors; if during debugging you found that the value of C was wrong you would
know that the fault lay somewhere inside Day_Of, since this is the only place where C is accessible. It would be perfectly
possible to declare C in the main program instead of inside Day_Of (it would still be accessible inside Day_Of) but you
would have to look at the main program as well as Day_Of if its value was found to be incorrect during debugging since
its scope would be the whole main program. It would also be wasteful of memory, since space would be allocated for C
even when it wasnt being used. The moral of the story is that variables should always be made as local as possible. If you
want to, you can localise declarations even further than the subprogram level by using a declare block:
declare
-- declarations local to the declare block
begin
-- statements using the local declarations
end; -- declarations go out of scope here
If you want to, you can also put an exception handler in a declare block like the one above:
declare
-- declarations local to the declare block
begin
-- statements using the local declarations
exception
-- exception handlers for the statements above
end; -- declarations go out of scope here
Although you may not have realised this, you met declare blocks for the first time at the end of the last chapter in
connection with exception handling inside loops. The reason you might not have noticed is that the declaration section
(everything before begin) was omitted, which is what you have to do if you dont want any declarations. If there are
declarations between declare and begin, they are local to the block. They are created when their declarations are
elaborated, they are destroyed at the end of the block, and they are only accessible within the block. As an example, heres
a version of Day_Of where a declare block is used to localise the declaration of C:
C is not created until line 1 and destroyed at line 2, whereas M and Y are created at the start of the function and destroyed
at line 3.
This is a lot clearer. The function is then put in a separate file, compiled separately and linked with the main program. We
have to tell the compiler where the function came from by including the line separate (Weekday) at the beginning:
separate (Weekday)
function Day_Of (Day, Month, Year : Integer) return Integer
is
D : Integer := Day;
M : Integer := Month;
Y : Integer := Year;
C : Integer;
begin
if M < 3 then
Y := Y - 1;
M := M + 10;
else
M := M - 2;
end if;
C := Y / 100; -- first two digits of Year
Y := Y mod 100; -- last two digits of Year
return ((26*M - 2)/10 + D + Y + Y/4 + C/4 - 2*C) mod 7;
end Day_Of;
This tells the compiler that function Day_Of is a function declared as being separate inside Weekday. The effect is
identical to the original version with the function embedded in the declaration section of the main program (in particular, it
is still within the scope of the main program), but the main program is now less cluttered and if you have to make any
changes to Day_Of, a sensible compiler will only require you to recompile the file containing Day_Of and then relink the
main program. The main program shouldnt need recompiling if it hasnt been changed. If the main program changes
youll still have to recompile both the main program and Day_Of in case the changes to the main program had some effect
on Day_Of (such as a change to the function declaration).
To get around these problems we could just compile Day_Of as an independent library unit in its own right, just like the
procedure Hello which was used as a component of the program Hello_3 in chapter 2. When Day_Of is compiled it gets
added to the library and it is then available for use as a component in a larger program. This is only possible if it doesnt
rely on anything which is not a standard built-in part of the language or else provided in a package such as Text_IO that
can be accessed using a with clause. In other words, it is restricted to accessing the same things as the main program is. In
this case the function doesnt rely on anything external so theres no problem:
Now we have to specify in the main program that Day_Of is a library unit that we want to refer to, so we need to name it
in a with clause:
4.7 Packages
At the moment the program doesnt check if the date that the user types in is valid. Its easy enough to write a function to
do this:
The function returns a Boolean result (True or False). It checks that the year is between 1901 and 2099 and that the day is
not zero or less; it then uses a case statement to distinguish between the various lengths of month and checks that the day
is not greater than the last day of the month. Note that the test for leap years (Year mod 4 = 0) is only adequate when Year
is guaranteed to be between 1901 and 2099; it would be better to generalise it in case the range allowed for Year ever
changes.
Modify the condition for leap years so that it works if the range allowed for Year ever changes. A year is a leap
year if it is divisible by 4 but not by 100, or if it is divisible by 400 (so 2000 is a leap year but 1900 and 2100
arent).
Each branch of the case statement returns a Boolean result, usually the result of comparing Day with the last day of the
month. For example, Day <= 31 is a Boolean expression which will give a result of True if the day is 31 or less and
False otherwise; the function just returns the result of the expression directly. The function can be used as the condition
part of an if statement (which must evaluate to a Boolean value):
This function is fairly closely related to the Day_Of function; there are probably dozens of date-related functions we could
dream up. It would make sense to gather these functions together into a package rather than having a library full of
functions with no apparent relationship to each other.
Another advantage of using a package is to avoid namespace pollution. Library units must have unique names, so the
more procedures and functions we put in our library the more restricted the choice of names becomes. It is far easier to
ensure that names are unique if collections of procedures and functions can be entered into the library under a single
package name. Also, as youll see later, you can minimise the number of unique names even further by using child
packages.
The first step is to generate a package specification. This needs to contain declarations for everything we want users of
the package to be able to access: variables, subprograms, whatever. In the case of subprograms, we only provide
specifications in the package specification; the actual code for subprograms goes into a separate package body. Heres a
first stab at the specification of a package to contain the Day_Of and Valid functions:
package Dates is
function Day_Of (Day, Month, Year : Integer) return Integer;
function Valid (Day, Month, Year : Integer) return Boolean;
end Dates;
The package specification goes in a file by itself and must be compiled and added to the library. Next we produce a
package body to provide the implementation of the two functions. The package body has the same name as the package
specification; the only difference is that it begins with the words package body rather than just package:
M := M - 2;
end if;
C := Y / 100; -- first two digits of Year
Y := Y mod 100; -- last two digits of Year
return (((26*M - 2)/10 + D + Y + Y/4 + C/4 - 2*C) mod 7);
end Day_Of;
The package body can now be compiled and added to the library, and then were finished. Note that we dont have to
provide a with clause for Dates at the start of the package body; a package body is automatically granted access to its own
specification.
We can use this package in any of our programs in exactly the same way as any other package:
if Dates.Valid(Day,Month,Year) then
Put (Dates.Day_Of (Day, Month, Year));
else
Put_Line ("Invalid date!");
end if;
end Weekday;
The program has to include the package in the with clause at the beginning; the declarations in the package specification
are then accessible so that we can refer to the functions Dates.Day_Of and Dates.Valid. We could of course name Dates in
a use clause so that Day_Of and Valid can be used without having to specify the package name as a prefix.
Note that only the package specification is accessed by a with clause; anything defined inside the package body is local to
the package and hence invisible to programs which use the package. The package body might contain extra subprograms
which are used to help implement the ones declared in the package specification; for example, there might be a function
called Month_Length which returns the number of days in a given month:
Since Month_Length is not declared in the package specification, it cannot be accessed by programs which use the
package; the only place it can be accessed is within the package body itself.
Note that any variables you might declare in the package specification will effectively be global variables rather than local
variables; they will be created before executing the main program when the with clause for the package is elaborated, they
will not be destroyed until after exiting from the main program, and they will be accessible wherever the package is
accessible. Variables declared in the package body itself rather than inside a particular subprogram in the package body
will also be created before the start of the program and destroyed after it finishes, although in this case they will not be
accessible from outside the package body.
The package body shown above consists entirely of subprogram declarations. You are also allowed to put a series of
statements in a package body in exactly the same way as you do in a subprogram (after any declarations, between begin
and end):
with Ada.Text_IO;
package body Dates is
... -- as above
begin
Put_Line ("This program uses Dates by John English");
end Dates;
The statements after begin are executed when the package body is elaborated, typically at the point where the package is
accessed by a with clause just before the main program is executed. They can be used to perform any initialisation that
may be needed before the package is used for the first time. This is not a terribly common requirement, but one use for it
might be to display a copyright notice of some sort at the start of the main program. In the example above, the message
This program uses Dates by John English will be displayed whenever any main program which uses the package starts
up.
will also have unique names. The number of unique names needed is thus reduced to a handful of parent packages. This
can also minimise the risk of bought-in package libraries from different suppliers having names that clash; each supplier
can just provide a parent package with a unique name (their company name, for example). Youve already come across
child packages: Ada.Text_IO and Ada.Integer_Text_IO are typical examples. In fact, all the standard packages in Ada are
children of one of three parent packages: Ada, Interfaces or System.
Im going to do the same thing in this book. Im going to define a parent package called JE (my initials) and make every
other package in the book a child of that. Here it is:
package JE is
-- an empty package!
end JE;
This package specification is only there to act as a parent for other packages. Since theres nothing in it, theres no need
for a package body. In fact, you arent allowed to provide a package body if it isnt needed (i.e. if there are no
subprograms declared in the specification). All we need to do is compile the package specification and add it to the
library. Now we can rewrite the previous package like this:
package JE.Dates is
function Day_Of (Day, Month, Year : Integer) return Integer;
function Valid (Day, Month, Year : Integer) return Boolean;
end JE.Dates;
All Ive done is to change the name of the package from Dates to JE.Dates. JE.Dates is effectively an extended version of
the parent package JE; everything declared in JE is also part of JE.Dates (which doesnt give you anything extra in this
case since JE is an empty package!). In other words, naming JE.Dates in a with clause automatically gives you access to
the contents of the parent package JE.
and the main program needs changing to refer to JE.Dates instead of Dates:
Get (Day);
Get (Month);
Get (Year);
if JE.Dates.Valid(Day,Month,Year) then
Put (JE.Dates.Day_Of (Day, Month, Year));
else
Put_Line ("Invalid date!");
end if;
end Weekday;
The main effect of this change is that only the name JE needs to be unique in the library; something else called Dates will
not clash with JE.Dates.
It is also possible to declare subprograms as children of packages; the function Day_Of could have been declared as a
child of the package JE like this:
All thats changed from the original version of the function is the name (JE.Day_Of instead of Day_Of). The main
program will have to have a with clause referring to JE.Day_Of to use it:
Exercises
4.1 The procedure Ada.Integer_Text_IO.Get skips any leading spaces and then reads an integer, but Ada.Text_IO.Get
reads a character without skipping leading spaces. Write a procedure called Get_Non_Space which skips leading
spaces and reads the next non-space character into the variable supplied as its parameter and write a simple program
which tests that it works properly.
4.2 Write a procedure which takes three integer parameters representing a date and a fourth integer parameter
representing a number of days and adds the specified number of days to the date (so that, for example, adding 7 to
December 26th 1995 will give January 2nd 1996). Add this procedure to the Dates package described in this chapter
and write a simple program which tests that it works properly.
4.3 Write a function to calculate the greatest common divisor (GCD) of two integers. You can do this using Euclids
algorithm: the GCD of two values X and Y is X if Y is zero, otherwise the process should be repeated using the
values Y and X mod Y instead of X and Y respectively (e.g. for X = 9, Y = 6, since Y isnt zero the process is
repeated with X = 6, Y = 9 mod 6 = 3; since Y still isnt zero its repeated again with X = 3, Y = 6 mod 3 = 0, and
this time Y is zero so the result is 3). The absolute value (positive magnitude) of the result should be returned (you
can get the absolute value of a number X using the expression abs X). Write a simple program which tests that it
works properly.
4.4 Many computers have an ANSI-compatible display. Write a package to do simple screen handling for an ANSI-
compatible display which contains procedures called Clear_Screen and Move_Cursor:
procedure Clear_Screen;
procedure Move_Cursor (Row, Column : in Integer);
You can perform these operations by using Put to display escape sequences consisting of the escape character
(which can be referred to using the name ASCII.ESC) followed by a string representing a screen management
command, e.g. "[2J" to clear the screen or "[5;6H" to move the cursor to row 5, column 6 on the screen.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are
distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 5:
Defining new data types
So careful of the type she seems.
Tennyson, In Memoriam
A data type specifies what values are possible for the corresponding real-world objects as well as specifying what
operations can be performed on those objects. Ada provides a number of built-in data types to get you started, but it
also recognises that it is impossible to provide data types to cater for every imaginable situation. The language provides
you with the ability to define your own data types so that your program can model reality as accurately as possible.
This also has the advantage that the more precise you are about the data in the real world that youre modelling, the
more the compiler can help you get your program right by checking it for errors.
The built-in data types are defined in a package called Standard which is always available automatically in every Ada
program. Appendix B contains a listing of Standard. Unlike other packages, you do not have to use a with clause or a
use clause to access the definitions provided in Standard. We have already met a few standard data types: the type
Integer for dealing with whole numbers, the type String for dealing with sequences of characters, the type Character for
dealing with individual characters, and the type Boolean for dealing with values which can be either True or False.
There are several other built-in types for dealing with numbers. Numbers in Ada are categorised as integers which are
exact values with no fractional part (e.g. 123) or real numbers which have a fractional part (e.g. 1.23) but will not
necessarily be represented with perfect accuracy; for example, the result of dividing 1.0 by 3.0 will be an infinitely long
fraction 0.333333333333333... which cant be represented exactly since we dont have an infinite amount of memory.
Also, since numbers are normally represented internally in binary, it will usually be impossible to represent 0.1 exactly,
since this is a recurring fraction in binary.
5.2 Integers
The basic built-in integer type in Ada is called Integer. The exact range of numbers that type Integer can cope with is
implementation defined; the only guarantee you have is that it will at least be able to hold values in the range 32767.
To allow you to find out the exact range of values available on a particular machine, all types in Ada have a set of
attributes which can be used to discover various details about the type. Attributes are specified by putting an apostrophe
(') and the attribute name after the type name. A complete list of attributes is given in Appendix C. In the case of
integer types, the most important attributes are as follows:
Thus Integer'Last will tell you what the largest value of type Integer is on your particular machine, and
Integer'Image(X) will convert an Integer value X to a string. The following statement could be used to display the
largest value of type Integer for a particular machine:
Ada.Text_IO.Put ( Integer'Image(Integer'Last) );
Integer types come with a full set of arithmetic operations, some of which youve already seen:
+ Addition - Subtraction
* Multiplication / Division
rem Remainder mod Modulus
** Exponentiation abs Absolute value
The + and operators can be used as binary operators which produce a result computed from two operands (e.g. 5 + 7
or 5 7) or as unary operators which produce a result computed from a single operand (e.g. +5 or 5). The abs operator
is also unary; it discards the sign of its operand leaving only the (positive) magnitude, so that abs 7 and abs 7 both
give a result of 7.
As was briefly mentioned in chapter 4, operators are evaluated in order of precedence; multiplications are done before
additions, and so on. The following table shows the precedence of all the operators in Ada (including some you havent
been introduced to yet):
Evaluation of a sequence of operators with the same precedence is done from left to right, so that 12/2*3 means
(12/2)*3 = 6*3 = 18, rather than 12/(2*3) = 12/6 = 2. If you are in any doubt, use extra parentheses to make it
absolutely clear what you intend.
The division operator produces an integer result when dividing integers, so that 7/5 would give a result of 1 rather than
1.4. The operators rem and mod allow you to find out the remainder resulting from a division, so that 7 rem 5 would
give you a result of 2. The same result could be obtained by using mod; 7 mod 5 would also be 2. The difference
between rem and mod is the way they deal with negative numbers: rem gives a negative result if the dividend (the left
hand operand) is negative, whereas mod gives a negative result if the divisor (the right hand operand) is negative. Here
are some examples which show the difference between them:
Note that 7 must be given in parentheses, as mod, rem and / are evaluated before . Without parentheses, 7 rem
5 would be interpreted as (7 rem 5).
With rem, the result is the conventional remainder, i.e. the difference between A and (A/B)*B. For example, 7/5 is 1.
Multiply this by 5 and you get 5. The remainder is the difference between 7 and 5, i.e. 2. With mod, a multiple of B is
added to the remainder if necessary so that the result is always between 0 and B, excluding B itself. So in the case of
(7)/5, the remainder of 2 produced by rem has to have 5 added to it so that the result of 3 is between 0 and 5.
The exponentiation operator raises a value to a given power, so that A ** B means A to the power B. For example, 4 **
3 is 43 (4 cubed), i.e. 64. The value of the right hand operand of ** cannot be negative since this will not give an
integer result.
The standard type Integer may or may not provide a large enough range of values for what you need. For example, you
might want to represent a time of day as a number of seconds since midnight, which requires a range of values between
0 and 86399. On some machines the type Integer might be able to represent values as high as 86399 but theres no
guarantee that it will. However, it is easy enough to define your own integer types; all you have to do is to write a type
declaration like this:
You now have a new data type called Time_Of_Day which is an integer type and which will have the same attributes
and operators as the built-in type Integer. However, as far as Ada is concerned, each type declaration creates a brand
new type which is unrelated to all the other types (even if the range of values is identical), so you cant mix Integer
values with Time_Of_Day values. In other words, if I is an Integer and T is a Time_Of_Day, the following statements
are illegal:
Operations such as + in the last statement above return a value of the same type as their operands (well, the base type
of their operands actually, as described later; this is basically the same thing except that the range of values might be
less restricted), so the addition I + I is legal and produces an Integer as its result. However, this cant be assigned to a
Time_Of_Day variable because the types dont match. The problem doesnt arise with integer literals such as 1 or 99;
these are universal integers which can be used with any integer type, so the following are all legal:
T := 99;
I := 99;
T := T + 99;
T := 99 + 99;
In the last example, adding two universal integers together gives a universal integer result.
Any numeric type can be converted to any other numeric type using a type conversion (which will involve rounding a
real value to the nearest integer if you are converting a real type to an integer type). A type conversion consists of the
name of the type you want to convert to, followed by the value to be converted enclosed in parentheses. The errors
shown above can be avoided by using type conversions where necessary:
T := Time_Of_Day (I);
I := Integer (T);
T := T + Time_Of_Day (I);
T := Time_Of_Day (I + I);
Of course, the value might be out of the legal range of the target type; for example, I might be negative, in which case
there is no corresponding Time_Of_Day value that it can be converted to. If this happens, a Constraint_Error exception
will be raised.
If you want to display Time_Of_Day values on the screen or read them from the keyboard, you cant use
Ada.Integer_Text_IO since this is for use with type Integer and type Time_Of_Day is quite distinct from this. You can
always use Time_Of_Day'Image to convert a Time_Of_Day value to a String that you can display with
Ada.Text_IO.Put:
Ada.Text_IO.Put( Time_Of_Day'Image(T) );
but a better way is to create your own input/output package for Time_Of_Day that provides the same facilities as
Ada.Integer_Text_IO. This is easy to do; heres the declaration of a package called Time_Of_Day_IO which will give
you the same facilities for Time_Of_Day values that Ada.Integer_Text_IO does for Integers:
These lines can be put in the declaration section of a procedure after the declaration of Time_Of_Day itself.
Ada.Text_IO must have been named in a with clause for this to work. What it does is to create a new package which is
a copy of the generic package Ada.Text_IO.Integer_IO. This provides input/output facilities for integer types in
general; all you have to do is to say which specific integer type you want to use it for. In the official terminology, we
have instantiated (i.e. created a new instance of) the package Ada.Text_IO.Integer_IO for use with Time_Of_Day
values. The standard package Ada.Integer_Text_IO is effectively predefined for use with Integers by the same process.
The listing of Ada.Text_IO in Appendix B contains the definition of Integer_IO, so you can refer to that to find out
what Ada.Integer_Text_IO provides.
5.3 Subtypes
The Ada notion of types is central to the issue of developing reliable software. If two integers represent quantities of
chalk and cheese, we dont really want to be able to mix them indiscriminately. We want the compiler to be able to say
sorry, you seem to be mixing chalk and cheese; what do you really mean?. And of course if thats what you really
want to do you can, but you have to tell the compiler that youre doing it deliberately rather than accidentally with an
explicit type conversion. This seems to be an unnecessary complication to people who are used to other, less strict,
programming languages, and you can usually spot code written by programmers who are new to Ada because they use
Integer for absolutely everything. It takes time to get used to the idea of analysing what youre representing and going
to the extra bother of writing a type declaration and doing type conversions where necessary. Once you do, the
compiler can help you to spot all sorts of silly mistakes arising from muddled thinking. The extra effort is beneficial
because it gives the compiler the means to check your programs for errors in a much more thorough way than it would
otherwise be able to do; but since its an extra effort it requires self-discipline on your part. However, self-discipline is
essential if youre going to be a good programmer!
Sometimes, though, we may want to specify that a particular type of variable should hold a restricted range of the
values covered by some other type without creating a brand new type which would need type conversion to be used in
conjunction with the original type. We want to be able to mix different types freely when theyre just different aspects
of the same thing, but we still want the benefits which arise from telling the compiler what were trying to do so that it
can check if were doing it right. For example, we want to specify that the right hand operand in an integer
exponentiation is non-negative so that we dont end up with a real result. Ada allows us to define subtypes of existing
types which behave just like the original type except that they have a restricted range of values:
These declarations define two subtypes of type Integer: Natural is an Integer which cannot be less than zero, and
Positive is an Integer which cannot be less than 1. In fact, both these subtypes are useful enough that they are already
provided as built-in types declared in the package Standard, and the exponentiation operator is defined so that it
requires a Natural value on its right hand side. Subtypes of a type can be used anywhere that the type itself or any of its
subtypes can be used, so you can use a Natural variable anywhere an Integer is required and vice versa; however, if you
use an Integer where a Natural is required the compiler will automatically insert checks into your program to ensure
that the Integer isnt negative. If it is negative, a Constraint_Error exception will be raised. This lets you separate out
the error handling into the exception handler section, which is more readable than having the error checking code
You dont have to restrict the range of values in a subtype declaration, as was done in the declarations of Natural and
Positive:
This means that Whole_Number has the same range of values as Integer, so it is effectively just another name for the
same type.
To avoid raising exceptions you can test if a value is within the range of a particular subtype using the in and not in
operators. For example, if I is an Integer variable and N is a Natural variable, you can test if I can be assigned to N like
this:
if I in Natural then
N := I; -- OK, I is in Natural's range
else
Put_Line ("I can't be assigned to N!");
end if;
All types in Ada are actually subtypes of anonymous types known as their base types, which I alluded to briefly above.
Since the base types are anonymous you cant refer to them directly by name, but you can use the 'Base attribute to get
at them; for example, Integer'Base is the base type of Integer. Base types may or may not have a wider range of values
than their subtypes; the only significance of this is that intermediate values in expressions like A*B/C use the base type
so that A*B might be able to exceed the limits of the type without raising an exception as long as the final result is
within the required limits.
This defines a new type called Whole_Number which has exactly the same properties as Integer. Whole_Number is
said to be derived from Integer; Integer is referred to as Whole_Numbers parent type. The range of values and the
operations available will be the same for Whole_Number as for Integer (Whole_Number is said to inherit all the
operations of its parent type), but unlike the subtype declaration for Whole_Number shown earlier, Whole_Number
will be a completely different type to Integer. However, it is always possible to use a type conversion to convert from a
derived type to its parent type and vice versa. This means that if you want to mix Whole_Numbers and Integers in an
expression, you will have to use a type conversion to convert one type to the other:
I : Integer;
W : Whole_Number := Whole_Number(I); -- convert Integer to Whole_Number
Now Age is the name of a new integer type derived from Natural but restricted to values between 0 and 150.
Derivation creates a family of related types usually referred to as a class; for example, all integer types belong to the
class of integer types. Also, youll see later that the class of integer types is part of a larger class, the class of discrete
types. The main reason for creating derived types is in situations where extra primitive operations have been defined
for a particular type. You could create a new type and then define an identical set of extra operations, but by using
derivation you automatically inherit versions of all the primitive operations of the parent type so no rewriting is needed.
Thus the class of discrete types provides the attribute 'First which all discrete types will inherit; the integer class adds
arithmetic operations like + which integer types then inherit in addition to the properties they inherit by being discrete
types. This is a subject that will be explored more fully in later chapters in connection with tagged types.
One way to do this would be to do all arithmetic modulo 86400 using the mod operator described earlier:
T := (T + 1) mod 86400;
This is a bit risky, since evaluating T + 1 might give rise to a constraint error (although its unlikely in this particular
case; 86400 will almost certainly be within the range of the base type). A better way to do this would be to define
Time_Of_Day as being a modular integer type:
Now all arithmetic on Time_Of_Day values is performed mod 86400 so that adding 1 to 86399 will wrap around
back to 0, and subtracting 1 from 0 will wrap around to 86399. As a result arithmetic on modular integers will never
raise a constraint error. However, the same is not true for type conversions. Attempting to convert a value outside the
range 0 to 86399 to a Time_Of_Day value will still raise a Constraint_Error. Modular types provide an attribute called
Modulus which gives the modulus of the type, so that Time_Of_Day'Modulus would give 86400. You can use this with
the mod operator to ensure that values are in the correct range before assigning them to Time_Of_Day variables.
Text_IO provides a generic package for input and output of modular integers called Ada.Text_IO.Modular_IO. The
following line can be used to instantiate Modular_IO for use with Time_Of_Day values, after which youll have Get
and Put procedures for Time_Of_Day values just like the ones for Integer values in Integer_Text_IO:
Here My_Float is a floating point type which is accurate to at least ten significant figures, and My_Fixed is a fixed
point type which is accurate to within 0.01 (i.e. to at least two decimal places) across the specified range. Decimal is a
decimal type with 12 digits which is accurate to two decimal places (i.e. capable of representing decimal values up to
9999999999.99). You can find out the digits value of a floating point type by using the Digits attribute (e.g.
Float'Digits) and the delta value of a fixed point type by using the Delta attribute (e.g. Duration'Delta). Many of the
attributes already described for integers (First, Last, Image, Value and so on) also apply to real types; for a complete
list of attributes which apply to real types, refer to Appendix C.
You can also have subtypes of real types; for example, there is a standard package called Ada.Calendar which defines a
subtype of Duration called Day_Duration like this:
The same arithmetic operators are available for real numbers as for integers, except that dividing two real numbers
gives a real result and so the mod and rem operators are not defined for real numbers. Also the exponentiation operator
can be used to raise a real number to any integer power; raising a real number to a negative power will produce a real
result, so the right hand operand is no longer restricted to belonging to the subtype Natural as it is for integer types.
There is a standard package called Ada.Float_Text_IO which provides Get and Put procedures for the standard type
Float. You can also create your own for use with other real types; Ada.Text_IO provides two generic packages for
input/output of floating point and fixed point values called Float_IO and Fixed_IO respectively. Ada.Float_Text_IO is
effectively just an instantiation of Ada.Text_IO.Float_IO for type Float, so you can look at the listing of Ada.Text_IO
in Appendix B to find out the details of Ada.Float_Text_IO. The specification of Put in this package is somewhat
different to Put for integer types; in Ada.Float_Text_IO it looks like this:
The optional parameters Fore, Aft and Exp can be used to control the layout of the values displayed on the screen. The
default for floating point types is to display the number with a three-digit exponent, so that 1234.5678 would be
displayed as 1.2345678E+003 (meaning 1.2345678 103); with fixed point values it would be displayed as 1234.5678,
possibly with some extra spaces before and zeros afterwards. Fore specifies how many characters to display before the
decimal point, Aft specifies how many digits to display after the decimal point (which will cause the value to be
rounded to that many places if necessary) and Exp specifies how many digits there are in the exponent; a value of zero
means that no exponent will be displayed. Here are some examples of how the version of Put for floating point values
works:
Note that displaying an exponent means that the value is normalised so that there is only one digit before the decimal
point. Also, unlike the version of Put for integer types, real numbers can only be displayed in decimal; there is no
equivalent of the Base parameter.
Its also possible to write numbers in binary or hexadecimal or any other base between 2 and 16. Here are three
different ways of writing the decimal value 31:
The letters A to F (or a to f) are used for the digits 10 to 15 when using bases above 10. If you mix an exponent (e) part
with a based number, the exponent is raised to the power of the base; thus 16#1F#e1 means hexadecimal 1F (= 31)
161, or 496 in decimal.
5.8 Constants
We can use the package Ada.Calendar mentioned earlier to improve on our good morning/good afternoon program.
Instead of asking the user to tell us whether its morning or afternoon, why not ask the computer? Ada.Calendar
contains (among other things) a function called Clock whose result is the current date and time. This result is of type
Time which is defined in Ada.Calendar. There is also a function called Seconds which takes a Time as its parameter
and extracts the time of day, returning the number of seconds since midnight as a value of type Day_Duration which
was mentioned earlier. Here are the specifications for Clock and Seconds:
This tells us that Clock is a function with no parameters which returns a result of type Time, and Seconds is a function
with a parameter called Date of type Time which returns a Day_Duration result. All we have to do is to use Seconds to
extract the time of day from the value produced by Clock and check if it is after noon (43200.0 seconds since
midnight). Here is the new version of the program:
The value 43200.0 in the new version of the program is remarkably uninformative. It would make the program much
more readable if we used a name like Noon instead of this magic number. This is generally true for practically all
numbers except 0 and 1. Numbers almost always represent a quantity of something, and should therefore be given
names which indicate what that something is. In many cases they are also subject to change as part of the maintenance
process and should therefore be defined at a single place in the program so that any necessary change can be
accomplished by altering just one line in the program. This can be done by defining named numbers like this:
These names can be used in exactly the same way as the numbers they stand for. They are universal integers just like
the numbers 60 and 3600 so that they can be used whenever an integer of any type is needed. Universal real numbers
can be used in exactly the same way. Although a named number declaration looks just like a variable declaration, the
reserved word constant indicates that these values cannot be altered:
Named numbers can be used anywhere that the corresponding numeric literal could be used, e.g. in a type declaration:
One thing to remember is that in order for a named number to be usable anywhere that the corresponding magic
number could be used, the compiler must be able to work out its value at compile time (i.e. when the program is being
compiled). This doesnt rule out using arithmetic expressions; for example, the declaration of Hour uses the expression
60*Minute as its value and Time_Of_Day uses 24*Hour as the modulus of the type. This is perfectly all right provided
that the compiler can work out the value of the expression; in particular, it needs to know how much memory a
Time_Of_Day object will require. An expression like this that can be evaluated at compile time is known as a static
expression, meaning that its value is not dependent on extraneous factors such as input from the user or the time of day
at which the program is being run. In this case the expression 24*Hour depends on knowing what Hour is at compile
time, which in turn depends on knowing what Minute is. Since the compiler knows that Minute means 60, it can work
out that Hour is 3600 and thus 24*Hour is 86400.
It is also possible to define constant values of a particular type by specifying the type name as part of the declaration:
Named numbers must be static, but constants of a specific type like Start can have values which arent known until run
time (i.e. when the program is run); in the case of Start, its value will be the time at which it is declared. Variables and
constants are collectively referred to as objects in Ada. Here is another version of the program, modified to show the
use of some constants:
Although this may seem quite long-winded, thats only because its such a short example. If you compare the if
statement above to the one in the previous example, Im sure youll agree that its much easier to see exactly what this
version is trying to achieve.
In many cases you can use attributes instead of constants to avoid using magic numbers. For example, you could use a
constant to define type Time_Of_Day like this:
and then you could use Maximum wherever you needed to refer to the largest possible Time_Of_Day value. But since
Time_Of_Day'Last will give the same value as Maximum, why not just define Time_Of_Day like this:
and then use Time_Of_Day'Last wherever you would use Maximum or (perish the thought) 86399?
5.9 Enumerations
In many cases numbers are unsuitable for representing the types of data required by a program. Consider the days of
the week as an example. We could use the numbers 0 to 6 or 1 to 7 to represent the days of the week, but they dont
lend themselves to this naturally. The natural way to represent days of the week is by using their names (Monday,
Tuesday, Wednesday and so on). We could of course define constants with the appropriate values, like this:
Which would be better for type Day_Of_Week, range 0..6 or mod 7, and why?
The disadvantage with this is that it would allow us to perform arithmetic on days of the week; for example, what do
the expressions Wednesday + Tuesday or Monday * 2 mean?
Enumeration types allow us to define types as a list which enumerates the possible values of the type. We could define
the type Day_Of_Week as an enumeration type like this:
This says that a Day_Of_Week object has seven possible values whose names are Sun, Mon, Tue and so on. You can
compare values of an enumeration type (using =, /=, <, <=, > and >=) and assign them, but operations like addition and
subtraction are not provided. For example, assuming D is a Day_of_Week variable, you can do the following:
D := Mon;
if D = Mon then
Put_Line ("Oh no, it's Monday again...");
end if;
The ordering of the values is that defined by the list of values you provide, so that Sun is less than Mon and so on.
There are a number of useful functions provided as attributes for enumeration types in addition to the ones mentioned
earlier for integer types (First, Last, Image and Value):
These can actually be used with integer types as well, but they arent a lot of use since Pos and Val will convert an
integer to itself and Succ and Pred can be replaced by addition and subtraction. They are only really useful for
enumeration types. Pos and Val allow you to convert enumerations to integers and vice versa, while Succ and Pred
effectively allow you to add or subtract 1 from an enumeration value. Here are a few examples:
Day_Of_Week'Pos(Sun) = 0 Day_Of_Week'Pos(Wed) = 3
Day_Of_Week'Val(0) = Sun Day_Of_Week'Val(3) = Wed
Day_Of_Week'Succ(Mon) = Tue Day_Of_Week'Pred(Fri) = Thu
Note that you will get a constraint error if you try to evaluate anything like Day_Of_Week'Val (7), Day_Of_Week'Succ
(Sat) or Day_Of_Week'Pred (Sun) since in all these cases you are going outside the limits of the range of values
available.
It is sometimes useful to use the same name for an enumeration value for two unrelated types. Here is a slightly
artificial example:
Note that the name Sun is used as a value for Weekday as well as Computer. This shows that enumeration literals, like
subprogram names, can be overloaded. The compiler will normally be able to distinguish between them from the type
of value it expects to see at a particular point in the program. In the rare cases when it cant it will report an error, and
you will then have to specify explicitly whether you mean Sun of type Weekday or of type Computer like this:
This looks similar to a type conversion but it isnt; the apostrophe between the type name and the parenthetical
expression shows that this is a qualified expression which just tells the compiler what type you expect the
parenthetical expression to have. No conversion is performed:
The integer and enumeration types are collectively known as discrete types since they define a set of discrete values
which can be listed in order. Real numbers cant be listed in this way since there are (in theory at least) an infinite
number of them between any two real values you care to choose. Discrete types play a special role in various
circumstances where discreteness is a useful property; Ill return to this point later. (A table showing the hierarchy of
the types available in Ada and the relationships between them is given at the end of Appendix A.) Like the packages for
input and output of the numeric types, there is a generic package called Ada.Text_IO.Enumeration_IO that you can
instantiate for input/output of enumeration types:
package Day_Of_Week_IO is
new Ada.Text_IO.Enumeration_IO (Day_Of_Week);
This will provide Get and Put procedures for Day_Of_Week values. Put will by default display the enumeration value
in upper case in the minimum possible width, but there are optional parameters Width and Set you can use to alter this.
Here are some examples:
Unfortunately there is no Capitalised value for the Set parameter which would display it as Sun; the only
possibilities are Upper_Case (giving SUN) and Lower_Case (giving sun).
We could use an enumerated type in a variant of the Greetings program from the previous chapter. Instead of asking the
user to type M or A, we could define an enumeration type like this:
If we instantiate Enumeration_IO for use with type Time_Of_Day the user could then type in either AM or PM in
either upper or lower case (or any mixture of the two) with or without leading spaces (which previous versions of the
program didnt allow). Heres the reworked program:
with Ada.Text_IO;
use Ada.Text_IO;
procedure Greetings is
type Time_Of_Day is (AM, PM);
package Time_IO is new Enumeration_IO (Time_Of_Day);
use Time_IO;
Answer : Time_Of_Day;
begin
Put ("Is it morning (AM) or afternoon (PM)? ");
Get (Answer);
if Answer = AM then
Put_Line ("Good morning!");
else
Put_Line ("Good afternoon!");
end if;
end Greetings;
An exception will be raised if anything other than AM or PM is typed in response to the prompt. Modify the
program to deal with this.
Now you can use Working_Day wherever Day_Of_Week can be used but the values allowed are limited to those
between Mon and Fri inclusive.
Boolean plays a special role in Ada; its used in the conditions of if and exit when statements as well as a few other
places. Note that if you try putting a declaration for Boolean (or any other standard type) in your program you will be
creating a brand new type; types in Ada which have different names are different even if their declarations are identical,
and the full name for the standard Boolean type is Standard.Boolean. You will end up with two completely separate
types called Boolean and Standard.Boolean, and since if statements and the like require conditions of type
Standard.Boolean you wont be able to use your own Boolean type in this sort of context.
Comparison operators like = produce a Boolean result. The comparison operators available are as follows:
A = B -- True if A is equal to B
A /= B -- True if A is not equal to B
A < B -- True if A is less than B
A <= B -- True if A is less than or equal to B
A > B -- True if A is greater than B
A >= B -- True if A is greater than or equal to B
These can be used to compare values of any of the types described in this chapter. There are some other operators
which combine Boolean values to produce Boolean results. Weve already seen how or can be used; here is the full list:
And, or and xor have the same precedence; if you want to mix them (e.g. using and and or together) in the same
expression you must use parentheses to make the meaning unambiguous:
There are also variants of and and or to cater for a few tricky situations. Consider this situation as an example:
The problem with this is that the expression on the right of the and operator is always evaluated, so that when B is zero
the expression A/B will still be evaluated with the result that the division by zero will cause a constraint error to be
raised. However, this is presumably what the check on Bs value is supposed to avoid! The solution is to use the
operator and then instead of and:
And then only evaluates its right hand side if it needs to; if B is zero, the overall result of the Boolean expression must
be false so the right hand side wont be evaluated. This means that the division by zero wont happen, so a constraint
error wont occur. The right hand side will only be evaluated if B is non-zero, in which case its safe to divide A by B.
The equivalent for or is or else, which only evaluates its right hand side if the expression on the left hand side is false:
This refers to the constants Start and Noon defined earlier. Morning will be True if the program is run before noon and
False otherwise. Boolean variables or constants can be used directly in if statements, while loops and any other context
that expects a Boolean value:
One common beginners mistake is to write if statements involving Boolean values like this:
This is of course redundant; if you do this you are asking if True is equal to True, and if it is the result is True!
Likewise, these are two different ways of saying the same thing:
The second version is considered better style; it is certainly more easily understood than the first. A similar situation
arises when assigning values to Boolean variables. Beginners sometimes write things like this:
but you can achieve the same effect in a much less long-winded way, like this:
If Start is less than Noon, the expression Start < Noon will evaluate to True, so Morning will be assigned the value
True; if not, it will be assigned the value False.
Some of the characters have no printable value; they are used as control characters. Examples include the carriage
return character which moves the cursor to the left of your screen when you display it and the form feed character
which is used to start a new page on a printer. To allow you to refer to them there is a package called ASCII (for
American Standard Code for Information Interchange, the predecessor to ISO-8859) defined as part of the package
Standard which provides names for these. Since ASCII is defined inside Standard you dont need to specify it in a with
clause before you can use it. As a result, you can always refer to the carriage return character as ASCII.CR and the
form feed character as ASCII.FF. However, this is a historical remnant from Ada 83; it only provides names for the
first 128 characters of ISO-8859 and it might not be provided at all in future versions of the language. For these reasons
it is better (if slightly less convenient) to use the package Ada.Characters.Latin_1 instead. This gives names for all the
256 available characters but it must be included using a with clause. Appendix B contains a listing of
Ada.Characters.Latin_1.
The 256 characters are sufficient for European languages but doesnt cater for languages like Japanese or Russian. Ada
provides another type Wide_Character which is similar to Character except that it provides 65536 different characters.
There is also a type Wide_String corresponding to String; type String is a sequence of Characters, and Wide_String is a
sequence of Wide_Characters.
As well as defining enumeration types using names like Sunday or Monday for the values you are allowed to use
character literals like '+' or '*'. The declaration of Character in the package Standard makes use of this to define the set
of printable characters. Heres the declaration of a data type which represents the operators used in the previous
chapters calculator program:
Note that this is a completely different data type to Character, and there is no way to do a straight type conversion from
Character to Operator or vice versa. Also, if you instantiate Enumeration_IO for Operator you might be in for a nasty
shock; the values will be displayed complete with the enclosing quotes, and you must also type the quotes on input.
This makes this facility somewhat less useful than it might otherwise be.
and then you can just write Latin_1.ESC instead of Ada.Characters.Latin_1.ESC. A renaming declaration like this
just provides you with an extra name for an existing object.
If the individual names within the package are awkward to use you can rename them in the same way:
After this declaration you can just use the name TM whenever you want to refer to
Ada.Characters.Latin_1.Registered_Trade_Mark_Sign in your program. TM acquires all the characteristics of the
object it renames; in this case, since the object being renamed is a constant, TM is also a constant.
The moral is that you should try to use meaningful names (and, as mentioned in chapter 2, avoid use clauses), and then
use renaming declarations where necessary to alleviate the burden if the resulting names get too long for comfort. This
is quite a common use for declare blocks; long names can be abbreviated for use within a particular section of the
program without making the abbreviation universally accessible.
You can also use renaming declarations to rename procedures and functions; you can also change the names and
default values of the parameters if you want. The only requirement is that the number and the types of the parameters
(and the type of result in the case of functions) are unchanged. So, if you always want floating point values to be
displayed in the minimum possible width to two decimal places with no exponent, you can do this:
This gives you a procedure called Show instead of Put; its first parameter is called Value instead of Item and the default
values for the other parameters are different to those for Put itself. Show can be used instead of Put like this:
The one situation where youre not allowed to use renaming declarations is with data types. This means that you cant
say type Time renames Ada.Calendar.Day_Duration, for example. However, you can achieve the same effect by
subtyping:
Now Time is a subtype of Day_Duration so it can be used wherever Day_Duration can be used, but we havent
restricted its range of values. The result is a type called Time which has the same range of values as Day_Duration and
which can be used interchangeably with Day_Duration; in other words they are identical, and Time is effectively just
another name for Day_Duration. Renaming a type like this may not be completely satisfactory; in the case of
enumerated types you will also need to rename the enumeration literals. Enumeration literals behave as if they were
parameterless functions, so if the type Day_Of_Week were defined in the package JE.Dates you could rename the type
and its enumeration literals like this:
This is quite awkward and long-winded, but fortunately its rarely necessary in practice.
Exercises
5.1 Write a program to play a simple guessing game. Define an integer type with a range of values from 1 to 1000 and
declare a secret value as a constant of this type, and then give the user ten chances to guess its value. A message
should be displayed at the beginning to tell the user what to do. For each unsuccessful guess, the user should be
told whether the guess was too low or too high. You will need to keep a count of the number of attempts. The
program ends after the user has successfully guessed the secret value or after the tenth unsuccessful attempt.
Display a message of congratulations or condolence at the end of the program. Modify the program so that the
value to be guessed is chosen at random each time the program is run. You can generate random values of a
discrete type X by instantiating the package Ada.Numerics.Discrete_Random for type X:
You will of course need a with clause for Ada.Numerics.Discrete_Random. The random-value generator Gen can
be initialised ready for use by calling the procedure Reset(Gen); you can then generate random values by calling
the function Random(Gen), which will produce a new random value of type X from the generator Gen each time
you call it.
5.2 Rewrite the date package from the previous chapter so that it includes a set of type declarations for days, months,
years and days of the week. Use an enumeration type for the months and for the days of the week. Modify the
functions in the package so that they use these types for their parameters and results instead of Integers and rewrite
the main program Weekday so that it reads in a day, month and year as values of the appropriate types and
displays the corresponding day of the week using input/output packages created from Ada.Text_IO.Integer_IO and
Ada.Text_IO.Enumeration_IO as described earlier.
5.3 Write a function which takes a character as its parameter and returns it converted to lower case if it is an upper
case letter, or returns it unchanged otherwise. Note that you can convert from upper case to lower case by adding
the difference between an 'a' and an 'A', using Character'Pos and Character'Val to convert characters to and from
integers.
5.4 Define data types to represent the suit and value of a playing card. Cards have four suits (Clubs, Diamonds, Hearts
and Spades) and 13 cards in each suit (Ace, 2 to 10, Jack, Queen and King). Use Ada.Numerics.Discrete_Random
as described in exercise 5.1 above to write a program to display three random cards, each of which is different.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files
are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 6:
Composite data types
Yea, from the table of my memory
Ill wipe away all trivial fond records.
William Shakespeare, Hamlet
All the types described in the last chapter are used to represent individual values; they are known collectively as scalar types
(see Appendix A for a complete list of the hierarchy of types in Ada). However, in most real-life situations the data you deal
with cant be represented by simple numbers or enumerations of possible values. Most data is composite in nature, consisting
of a collection of simpler data items. For example, a date consists of a day, a month and a year; a time consists of an hour and
a minute. The data types weve used so far are adequate for representing each of these individual components; days, years,
hours and minutes are all numerical and months are either enumerations or numbers. For example, here are some possible
declarations for types to represent days, months and years:
Although you could represent a date using three separate variables (day, month and year), this is not a very satisfactory
solution. Every procedure or function that dealt with a date would require three parameters instead of one. Functions would
not be able to return date results, since a function can only return a single result but a date consists of three separate values.
Worse, it wouldnt necessarily be obvious that the three variables were parts of a common whole. You could end up
supplying a procedure with the day from one date and the month from another by mistake as the result of a simple typing
error. All this can lead to some pretty horrible debugging and maintenance problems.
The solution in Ada is to gather the components of the data type together into a single type known as a record type. Here is
how we could define a record type to represent a date:
type Date_Type is
record
Day : Day_Type;
Month : Month_Type;
Year : Year_Type;
end record;
Given this type declaration, variables of type Date_Type can be declared just like variables of any other data type:
Notice that the individual components of the type defined between record and end record look just like variable
declarations. You can if you like think of Date_Type as a type which acts as a container for three separate objects called Day,
Month and Year, so that the variables Later and Now each contain three distinct subvariables, normally referred to as
components (or sometimes fields). The components of a record type can be selected by using the same dot notation youre
already familiar with for selecting procedures from within a package, so that the Day component of Later can be referred to
as Later.Day. In other words, you can treat Later.Day as a variable of type Day_Type, or Now.Month as a variable of type
Month_Type. To copy one date into another you could do something like this:
Later.Day := Now.Day;
Later.Month := Now.Month;
Later.Year := Now.Year;
However, a variable of type Date_Type can also be treated as a single item, so that a Date_Type value can be passed as a
parameter to a procedure or returned from a function. You can also assign one Date_Type variable to another in a single
operation, so that the three assignment statements above can be written like this:
Later := Now;
Although the effect is exactly the same, its much simpler and clearer than using three separate assignments as well as
reducing the risk of error. The more statements you have to write to do something, the more chance there is of making a
mistake. Also, if you have to change this when you maintain the program, you only have to change one statement instead of
three; the more changes you have to make, the greater the risk of errors creeping in. The only other standard operations on
record types are tests for equality and inequality:
You can also use an aggregate to build a record from a set of components:
Later := (25,Dec,1995);
if Now = (25,Dec,1995) then ...
if Now /= (25,Dec,1995) then ...
An aggregate is simply a list of values of the appropriate types enclosed in parentheses. The assignment above is equivalent
to the following longer-winded set of three assignment statements:
Later.Day := 25;
Later.Month := Dec;
Later.Year := 1995;
If you do this you can write the values for the components in any order since the compiler can use the component names to
arrange them into the correct order:
Aggregates are often used to provide an initial value for a variable as part of a declaration; they can also be used for declaring
constants:
Record types are types just like any other. You can use them in exactly the same way as any other type; they can be used for
parameters to procedures, function results, even components of other records. If we declare a record type to represent a time
of day like this:
we can define yet another record type which contains a date and a time, perhaps for recording the date and time of an
appointment:
type Appointment_Type is
record
Date : Date_Type;
Time : Time_Type;
end record;
Given a variable A of type Appointment_Type, its components can be referred to like this:
So, A is name of the date-and-time record as a whole. Its of type Appointment_Type, so we can select its Date component
by saying A.Date; this is of type Date_Type, and so we can select the Day component of the date by saying A.Date.Day.
6.2 Strings
Of course, the one thing thats missing from the Appointment_Type above is a description of the appointment. We could
represent this as a string using the standard type String. String is a predefined array type; an array is a collection of items of
the same type. This is in contrast to record types, where the components can be different types. In the case of String, the
individual components of the array are Characters. Ill describe how you can define your own array types in the next section,
but for now Ill use String as an example of an array type to show you how arrays can be used.
To declare a string variable you have to specify how many characters it can hold:
Another way of doing the same thing would be to declare subtypes to specify the length:
Both methods give you strings called Details and Name; Details can hold 100 characters numbered 1 to 100, while Name can
hold ten characters numbered 1 to 10. You can select individual characters from Details using an index between 1 and 100:
Details(1) := 'x';
Details(I+1) := 'x';
The range of possible index values is known as the index subtype of the array. If you try to access the array with an index
which isnt within the range of the index subtype, youll get a Constraint_Error. Both of the assignments above set a specified
character in Details to x. In the first case its the first character; in the second case the character selected depends on the value
of the expression I + 1. If the value of I + 1 isnt within the range of the index subtype (i.e. 1 to 100) youll get a
Constraint_Error. Note that the individual elements of the string are of type Character.
As well as dealing with individual array elements, you can deal with slices of a string:
Details(1..5) := "Hello";
Details(2..6) := Details(1..5);
The first assignment copies the five-character string "Hello" into the first five characters of Details. The second assignment
copies the first five characters into the five characters beginning at the second character of the string, so that after these two
statements the first six characters of Details will be "HHello". As you can see, it doesnt matter if the slices overlap; a copy of
the slice is taken and then the copy is stored in the destination slice. Note that slices are also arrays of characters, i.e. Strings.
The length of the string that you assign to a slice must be the same as the length of the slice; the same applies to Details as a
whole:
This will be all right provided that the length of the string being assigned to Details is in fact exactly 100 characters long.
Extremely long strings like this might not fit on one line, in which case you can use the concatenation operator & to join
two shorter strings end to end to create a longer one:
You could use slicing and the concatenation operator to interchange the two halves of Details (but Im not sure why you
would want to; this is only an example!). You would do it like this:
The two slices are the last 50 characters and the first 50 characters of Details. These are then concatenated together to give a
single 100-character string which can be assigned to Details.
If you want to fill an entire string with spaces, you can do it like this:
The string literal above is actually an abbreviation for the following array aggregate:
Name := (' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ');
-- an aggregate containing 10 spaces
This can be awkward with longer strings like Details. When all the characters are the same (and especially when there are a
lot of them) you can simplify matters by writing the aggregate like this:
Details := (1..100 => ' '); -- no need to write exactly 100 spaces!
This specifies that each character with an index in the range 1 to 100 is to be set to a space, and avoids having to write a
string literal which contains exactly the right number of characters and to modify it if the string size needs to be changed.
Better still, you can avoid mentioning the string bounds at all like this:
Details := (1..10 => 'x', 11..13 | 21..23 | 27 | 29 => 'y', others => ' ');
This sets each of the first ten characters of Details to x, characters 11 to 13, 21 to 23, 27 and 29 to y (a total of eight
characters) and the remaining 82 characters to spaces.
The easiest way to read in a string is to use the procedure Get_Line defined in Ada.Text_IO:
This will read a line of up to 100 characters (the size of Details) into Details from the keyboard. N is an out Natural
parameter which is set by Get_Line to the actual number of characters read, so that you can then access the characters that
you read in by using the slice Details (1..N).
Arrays can be compared using the comparison operators (=, /=, <, >, <=, >=). In the case of String, two strings are equal if
they are the same length and contain the same characters. For comparing using < and so on, the characters of the strings are
compared from left to right until either the strings are found to differ or the end of the shorter string is reached. This more or
less gives alphabetical ordering (but not perfectly; 'a' comes after 'Z' in the definition of Character, and if you use accented
characters as in French '' comes after 'z'). Here are some examples:
The comparison operations for other array types are defined in the same way. You can only compare arrays if the individual
components can be compared, so that you couldnt use < to compare an array of records since you cant compare records
using <. You could, however, compare two arrays of records for equality since you can compare individual records for
equality. The logical operations and, or, not and xor are also defined for arrays provided that the individual components
support these operations (e.g. an array of Booleans); the operation will be applied to corresponding components in the two
arrays.
Now we can use String to finish off the appointment type defined earlier:
type Appointment_Type is
record
Date : Date_Type;
Time : Time_Type;
Details : String (1..50);
end record;
Note that if A is an Appointment_Type value, A.Details is a 50-character string and A.Details(1) is the first character of the
string.
This defines Appt_Array as a type describing a collection of appointments which can be individually selected by an index
between 1 and 100. Appt_Array variables can be declared in the normal way:
The individual appointments can then be accessed as Appt_List(1), Appt_List(2) and so on up to Appt_List(100), just like
selecting an individual character from a string. You can slice any array in exactly the same way as you can slice a string; the
first five appointments of Appt_List can be sliced out as Appt_List(1..5). As I mentioned earlier, anything you can do with a
string (indexing it to select individual characters, slicing it to get a smaller string) can be done with any other array type;
similarly all the new features of arrays Ill be describing below (array attributes and so on) apply to strings as well.
The first appointment in the array would be referred to as Appt_List(1). Since this is an individual array element of type
Appointment_Type, we can refer to the details of the appointment as Appt_List(1).Details. Since this is a string you can then
select its first character by referring to Appt_List(1).Details(1).
If you only want one array of a particular type, you can use the array type specification directly in the declaration of a
variable:
You dont have a name for the type of Appt_List, so you cant declare any more arrays of the same type. This means you
wont be able to assign one array to another in a single operation (or a slice of one array to another); you wont be able to use
the name of the array type in a procedure parameter declaration so you wont be able to pass the array (or a slice of it) as a
parameter to a procedure. However, you can still use individual array elements since each element has a known type, namely
Appointment_Type.
One place where anonymous arrays are handy is in defining lookup tables; for example, the following declares an array
containing the number of days in each month:
With this declaration, Month_Length(1) is 31, which tells us that January (month 1) has 31 days. The length of any month N
is given by Month_Length (N), although it doesnt cater for February having 29 days in leap years.
The index subtype for an array type doesnt have to be a range of integers; it can be any discrete type (i.e. any integer or
enumeration type) or a range of values of a discrete type:
If you just give a range like 1..100 without specifying a particular type, the type Integer is assumed, so that the declaration of
Appointment_Array given earlier is equivalent to this:
We could redefine Month_Length to use the enumeration type Month_Type declared earlier:
Or equivalently:
The individual elements of Month_Length would then be accessed as Month_Length(Jan), Month_Length(Feb) and so on.
Heres another useful one using the type Day_Of_Week from chapter 5:
Tomorrow(Sun) is Mon, Tomorrow(Mon) is Tue, and so on up to Tomorrow(Sat) which is Sun. This can be used to get
around the problem that Day_Of_Week'Succ cant be used to get the day after Saturday since Sat is the last value of the type.
Here the index subtype for String is defined to be a subtype of Positive. The symbol <> is known as a box; it signifies that
the exact range of values allowed is unspecified. This is referred to as an unconstrained array type; the actual range of
values (the constraint) must be supplied whenever you declare a String variable so that the compiler knows how much
memory to reserve to hold the string. One place where you are allowed to use an unconstrained type is as the type of a
parameter in a procedure or function, which means that a procedure or function can be written to accept a string of any length
as a parameter. This is acceptable because the compiler doesnt have to allocate any memory in this case; the actual
parameter will refer to a constrained string whose memory space has already been allocated. If an array type declaration
specifies the size you lose this level of flexibility.
Arrays have a number of useful attributes which can be used to find out details about the index subtype:
These can be applied to the types themselves (if theyre constrained) or to individual array objects. The values of these
attributes for the arrays Appt_List and Month_Length defined above would be as follows:
The Range attribute can be used in situations where a range is expected, e.g. an array aggregate or a constraint in a
declaration:
We can declare Appt_Array as an unconstrained array in exactly the same way as String was declared:
To declare an Appt_Array, we have to provide the bounds of the range for the index subtype:
The range constraint 1..100 is effectively used to fill in the box <> which is used in the declaration of Appt_Array, so the
index subtype for Appt_List is Positive range 1..100. Alternatively, an initial value in a declaration can be used to set the
bounds of the index subtype:
In this case, the bounds are set to be 1..11 because the initial value has these bounds. String literals and array aggregates
whose bounds arent specified explicitly take their lower bounds from the lower bound of the index subtype; in the case of
String the index is a subtype of Positive so the lower bound of a string literal is always Positive'First, i.e. 1. The initial value
has to give the compiler enough information to figure out what the bounds are supposed to be, so an aggregate using others
or the 'Range attribute wont be acceptable.
The bounds of an array dont have to be static; youre allowed to calculate them at run time:
A declare block can be useful if you want to calculate the array size at run time; the array size can be read in and the declare
block can then use the value read in to create the array accordingly:
As youve seen with strings, you can supply the constraint as part of a subtype declaration and then use the subtype to declare
variables:
Here P takes on successive values in the range Appt_List'First to Appt_List'Last each time around the loop. The first time
around, P will be 1 (Appt_List'First), the second time it will be 2, and so on up to 100 (Appt_List'Last). The range
specification in a for loop is just like the range specification for an array index subtype: it can be any discrete type or a range
of values of a discrete type; as with an array index, a range like 1..100 which doesnt mention a specific type will be assumed
to be a range of type Integer.
There are a few significant points to note about for loops. The control variable P doesnt have to be declared in advance; P
is declared automatically just by its appearance as the control variable for the loop. In fact, if you do declare a variable called
P you wont be able to access it from inside the loop; using the name P inside the loop will refer to the control variable P
rather than the variable called P declared outside the loop:
Also, the control variable cant be altered from inside the loop. This ensures that the number of times a for loop is executed is
fixed in advance; you can still exit from the loop before its finished using an exit statement, but you cant get stuck inside it
forever. The control variable cant be accessed outside the loop (it only exists while the loop is being executed); if you do use
an exit statement and you need to find out what the value of the control variable was when you exited the loop, youll have to
copy the control variable into an ordinary variable before executing the exit statement:
The Range attribute provides a simple way of specifying the range of values in a for loop to process an array:
The range of values in a for loop, like the range of an array index subtype, can be any discrete subtype. Here are some
examples:
Note that the bounds of the range dont have to be constants; N in the last example might be a variable. The value of N at the
time the loop is started will determine how many times the loop will be executed. Note that changing the value of N inside
the loop wont change the number of times the loop is executed, which is determined by the value of N at the moment you
enter the loop.
You can also go through a range of values in reverse order by specifying the word reverse before the index subtype:
P will take on the values 100, 99, 98 and so on all the way down to 1. Note that the following wont do what you might
expect it to:
Since there is no range of values starting from 100 and ending at 1, the loop will not be executed (or rather, it will be
executed zero times, which is the number of values over 100 which are less than 1). This is quite sensible; the following loop
will be executed N times:
If N is zero, the range of values will be 1..0, and so the loop will be executed zero times (i.e. it wont be executed at all). If it
didnt behave this way and counted backwards from 1 to 0 youd have to put in extra checks for the case where N is zero,
Notice how the procedure makes no assumptions about the values of the upper and lower bounds of the array; it uses the
attributes First and Last to refer to them, which makes this procedure work with any array regardless of what its actual
bounds are. You should always do this; never assume anything about the bounds of an array.
The innermost loop which searches backwards is an interesting one. It compares each item in turn with the saved item,
starting with element I1 and working back to the start of the array (X'First). The loop will always be executed at least once
since I starts off as X'First+1; this means that I1 cannot be less than X'First. Since we already know that element I1 is
greater than the saved item, the loop will always be executed in full at least once, so the value of Position will always be set.
Position will end up holding the index of the last item which was greater than the saved item; if the loop terminates naturally
rather than because of the exit statement, Position will be set to X'First and the saved item (which must be smaller than every
other value before it if the exit statement was never triggered) will be slotted in at the very beginning of the array.
The value of Strings(4) will be the single String "OPERA"; as usual, this can be subscripted to extract a single character from
the string so that the letter P in "OPERA" could be referred to as Strings(4)(2).
An alternative to declaring arrays of arrays like this is to declare multidimensional arrays. A multidimensional array like the
one above can be declared like this:
This declares Strings to be a 55 array of characters. The array aggregate is constructed as an array of array aggregates;
hence the double parentheses. Individual elements are selected using a pair of subscripts, e.g. Strings(1,1) or Strings(1,5). The
major difference between a two-dimensional array and an array of arrays is that you can only select individual elements of a
two-dimensional array, but you can select an entire one-dimensional array from an array of arrays which you can then use in
its entirety, or you can then slice it or subscript it like any other one-dimensional array.
You can have as many array dimensions as you like in a multidimensional array (subject to the overall limits on memory
availability on your machine) and the index subtypes for each dimension can be different. For example, a chessboard consists
of an 88 grid whose ranks (rows) are numbered 1 to 8 and whose files (columns) are lettered A to H, so that individual
squares can be referred to as e5 or g6 or whatever. Heres a declaration of a chessboard in Ada:
You can now refer to Board(E,5) or Board(G,6) and so on. If you wanted to, you could create a three-dimensional array to
hold the positions after each of the first 40 moves like this:
You could then obtain the value of the e5 square on the 20th move by referring to Game(E,5,20).
When there is more than one dimension to the array, you have to specify which dimension youre referring to when you use
the attributes 'Range, 'First, 'Last and 'Length. You do this by appending the dimension number to the attribute name, e.g.
Board'First(1) or Board'Last(2). Here are the values of the attributes of the array Board above:
6.8 Discriminants
Sometimes you may want to use an array as a component of a record but you dont necessarily want to tie yourself down to a
specific array size. However, you can only use constrained arrays in a record type declaration so that the compiler knows how
much space in memory to allocate when you declare a variable of that type. For example, consider a type to represent a
variable-length string:
type Varying_String is
record
Length : Natural;
Value : String (1..100);
end record;
The idea here is that Value holds the string itself, which is restricted to a maximum of 100 characters, and that Length is used
to record how much of the string is actually in use at any one time. The problem is if we want a different maximum length we
have to redefine Varying_String. It would be much more convenient to be able to declare an unconstrained record. The way
to get around the problem is to use a discriminant in the record declaration. A discriminant must be a value of either a
discrete type or an access type (which will be discussed in chapter 11):
The discriminant effectively acts as a parameter for the record. Varying_String is now an unconstrained type (like an
unconstrained array, the compiler cant work out how much space to reserve in memory for a Varying_String unless the
value of Maximum is known), so when you declare a Varying_String you will have to specify a value for the discriminant
Maximum:
The discriminant can be accessed just like any other component of the record:
However, unlike other components, discriminants are constants; once theyve been set in the declaration of the variable they
cant be changed:
Aggregates used to initialise record types must provide values for all the components. Since the discriminant is a component,
you have to provide a value for the discriminant in the aggregate:
For convenience you can also provide a default value in the declaration of the original type:
You can also provide default values for other record components:
Whenever a Varying_String is declared, the Value component will be 80 characters long by default and its Length
component will be set to zero by default, so that newly created Varying_Strings will automatically be marked as containing
zero characters but able to hold up to 80 characters. You can still set Length to a different value if you supply an initial value
in the declaration:
The defaults for record components dont have to be constants, they can be any expression. Whenever a variable is declared
the expression will be evaluated. You could for example include a timestamp in every record to keep track of the time it was
created:
type Time_Stamp is
record
Creation_Time : Ada.Calendar.Time := Ada.Calendar.Clock;
end record;
In this case the function Clock from Ada.Calendar will be called when a Time_Stamp object is declared so that
Creation_Time will be set by default to the time at which the object was created. Heres another example: a record type to
represent a bank account which is automatically given a unique account number when it is created:
type Bank_Account_Type is
record
Account : Account_Number := Next_Account_Number;
Balance : Money_Type := 0.00;
-- and so on
end record;
Now whenever an object of type Bank_Account_Type is created, the function Next_Account_Number will be called to
initialise the Account component; this will use the value of Last_Account (so the first account created will be number
1000_0000) and it will also increment it (so the next account will be number 1000_0001, then 1000_0002, and so on). Note
that Last_Account has to be declared outside Next_Account_Number so that it wont lose its value between one call of
Next_Account_Number and another.
type Bank_Account_Type is
limited record
Account : Account_Number := Next_Account_Number;
Balance : Money_Type := 0.00;
-- and so on
end record;
Now you can declare variables of type Bank_Account_Type in the normal way:
The only operations available for ordinary record types are assignment (:=) and testing for equality or inequality (=, /=).
Limited types dont have these operations, so you cant assign one bank account to another or test if they have the same value
as each other. This also means that you cant assign an initial value to a bank account in its declaration. You can, however,
alter the individual components within the record; the prohibition on assignment and comparison only applies to the record as
a whole. There is a way around this, but Ill save it for later. Here are some examples involving the bank accounts John and
Fred declared above:
Any array or record type involving limited components is automatically limited as well; you wont be able to assign or
compare a record containing a limited component since this involves assigning or comparing each individual component.
package JE.Dates is
subtype Day_Type is Integer range 1..31;
type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov,
Dec);
subtype Year_Type is Integer range 1901..2099;
type Date_Type is
record
Day : Day_Type;
Month : Month_Type;
Year : Year_Type;
end record;
end JE.Dates;
Given this, any program which wants to use these declarations just has to specify JE.Dates in a with clause. Note that since
the package does not contain any subprograms, there is no need for a package body since the specification does not include
any incomplete declarations; in fact, you are forbidden to provide a package body unless the specification is incomplete in
some way, e.g. if it declares any subprograms which then need to be defined in the package body.
If you want to define subprograms which have parameters of user-defined types, the type declarations must be available at
the point where the subprogram is declared as well as at the point where it is called (so that the calling program can supply
values of the correct type). The simplest way to do this is to put the type declarations and related subprograms together in a
package so that the type declarations as well as the subprogram declarations are accessible to the calling program. Here is a
modification of the date package from chapter 4 which uses Date_Type for the subprogram parameters instead of three
individual integers, and which has Day_Of returning a result of the enumerated type Weekday_Type instead of an Integer:
package JE.Dates is
subtype Day_Type is Integer range 1..31;
type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov,
Dec);
subtype Year_Type is Integer range 1901..2099;
type Weekday_Type is (Sun, Mon, Tue, Wed, Thu, Fri, Sat);
type Date_Type is
record
Day : Day_Type;
Month : Month_Type;
Year : Year_Type;
end record;
function Day_Of (Date : Date_Type) return Weekday_Type;
function Valid (Date : Date_Type) return Boolean;
end JE.Dates;
The type declarations in the package specification are automatically visible in the package body so the package body will
only need to contain the bodies of Day_Of and Valid. Any program which uses this package has access to the functions
Day_Of and Valid as well as the type declarations it needs in order to use them.
Exercises
6.1 Write a program to count the number of occurrences of each letter of the alphabet typed as input at the keyboard. Using
a subtype of Character as the index subtype of an array is a sensible way to do this.
6.2 Write a program which counts the number of occurrences of each word typed at the keyboard. Consider a word to be a
sequence of up to 32 letters. You will need to use an array of records, where each record contains a word and the number
of occurrences. Allow for a maximum of 100 different words, and ignore case differences. Functions to convert a
character from upper case to lower case and to read the next word from the keyboard and return it as a string would be a
good idea.
6.3 Produce a package defining data types to represent individual playing cards and also packs of cards (see exercise 5.4).
Each card should be a record containing a suit and a value; the pack should contain an array of up to 52 cards (but try to
avoid using the magic number 52 if you can!) together with the number of cards in the pack. Provide subprograms to
initialise a pack (so that it contains a complete set of four suits of 13 cards each), to shuffle a pack (by interchanging
each card with another randomly selected card from the same pack; see exercise 5.1), to deal a card (by removing the
first card from the pack) and to replace a card (by adding it to the end of the pack). Write a simple test program to check
that the package works correctly.
6.4 Write a program to encode a message using a simple substitution cipher, where each letter of the message is replaced by
a different letter using a lookup table. The lookup table is generated using a keyword; the letters of the keyword are used
for the first few positions in the table (ignoring any repeated letters) and the remaining letters of the alphabet are used to
complete the table in alphabetical order. The output should be displayed in groups of five letters, all in upper case, with
the last group padded out with Xs. For example, if the keyword is JABBERWOCKY, the lookup table will be as
follows:
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
encoded as: J A B E R W O C K Y D F G H I L M N P Q S T U V X Z
Note that B occurs twice in the keyword so the keyword actually appears as JABERWOCKY in the table, and the
remaining letters of the alphabet (D, F, G, etc.) are then used to fill up the rest of the table. The message Cabbages and
kings would be encoded as BJAAJ ORPJH EDKHO PXXXX using this table.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are
distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 7:
Exceptions
To err is human, to forgive, divine.
Alexander Pope, An Essay on Criticism
This separation of concerns is particularly important when designing packages which could be used by several different
programs. Its always tempting to try and deal with errors as soon as you detect them, but one of the basic rules of
package design is that you should never try to handle any errors within the package itself. The package may be able to
detect errors but it will not usually know how to deal with them. Handling errors is something which is normally
dependent on the overall program, and a package never knows anything about the program that is using it. What may
be appropriate in one program may be totally inappropriate in another, and building in any assumptions about how an
error should be handled will prevent you from reusing the package in more than one program. Displaying an error
message on the screen and then halting may be appropriate in some situations, but in other situations there may not be a
screen (e.g. your package is used by a program which controls a washing machine) or it may be a bad idea to halt (e.g.
the package is used by a program in an aircrafts navigational system). Instead you should define your own exceptions
and raise them if an error is detected; this will allow the main program to decide how best to deal with the error.
Ada allows you to define your own exceptions in addition to the standard exceptions, like this:
Something_Wrong : exception;
This declares an exception called Something_Wrong. The standard exceptions are of course declared in the package
Standard, just as the standard types like Integer are. Other exceptions are defined in other packages such as
Ada.Text_IO; Data_Error is an example of this. You may have noticed that Data_Error is not in the list of standard
exceptions above. It is actually declared in a package called Ada.IO_Exceptions, and redeclared by renaming inside
Ada.Text_IO (and all the other input/output packages) like this:
Although an exception declaration looks like a variable declaration, it isnt; about the only thing you can do with an
exception (apart from handling it when it is raised) is to raise it using a raise statement:
raise Something_Wrong;
When you raise an exception, the system looks for a handler for that exception in the current block. If there isnt one, it
exits from the block (going to the line after end in the case of a begin ... end block, or returning to where it was called
from in the case of a procedure or function body) and looks for a handler in the block it now finds itself in. In the worst
case where there is no handler anywhere it will eventually exit from the main program, at which point the program will
halt and an error will be reported.
Note that if an exception is raised inside an exception handler, you exit from the block immediately and then look for
an exception handler in the block youve returned to. This prevents you getting stuck in an endless exception handling
loop. The same thing happens if an exception is raised while elaborating declarations in a declaration section; this
avoids the possibility of an exception handler referring to a variable that hasnt been created yet. Until youve got past
the begin at the start of the block youre not counted as being inside it and hence not subject to the blocks exception
handlers; once an exception occurs and youve entered the exception handler section, youre counted as having left the
block so once again youre not subject to that blocks exception handlers. In other words, the exception handler only
applies to the statements in the body of the block between begin and exception.
begin
-- create a temporary file
-- do something that might raise a Constraint_Error
-- delete the temporary file
exception
when Constraint_Error =>
-- delete the temporary file
raise Constraint_Error;
end;
The temporary file will be deleted whether an exception occurs or not, either in the course of normal processing or
from within the exception handler. A raise statement is used inside the exception handler to raise the same exception
again, so that you will immediately exit from the block and look for another handler to handle the exception properly.
Sometimes you dont know exactly which exception has occurred. If you have an others handler or a single handler for
several different exceptions, you wont know which exception to raise after youve done your tidying up. The solution
is to use a special form of the raise statement which is only allowed inside an exception handler:
begin
-- create a temporary file
-- do something that might raise an exception
-- delete the temporary file
exception
when others =>
-- delete the temporary file
raise; -- re-raise the same exception
end;
Raise on its own will re-raise the same exception, whatever it might be.
package called Ada.Exceptions which contains some functions to give you this sort of information. Ada.Exceptions
defines a data type called Exception_Occurrence and provides a function called Exception_Name which produces the
name of the exception as a string from an Exception_Occurrence. You can get a value of type Exception_Occurrence
by specifying a name for it as part of your exception handler:
begin
...
exception
when Error : Constraint_Error | Data_Error =>
Put ("The exception was ");
Put_Line ( Exception_Name(Error) );
end;
The name of the Exception_Occurrence is prefixed to the list of exceptions in the handler (the name chosen was Error
in this case). There are some other useful functions like Exception_Name; in particular, Exception_Message produces a
string containing a short message giving some details about the exception, and Exception_Information produces a
longer and more detailed message. Exception_Occurrence objects can also be useful for passing exception information
to subprograms called from within an exception handler.
The standard exceptions will have a standard message associated with them. If you want to supply a message for an
exception that youve defined yourself (or supply a different message for an existing exception) you can use the
procedure Raise_Exception:
This has the same effect as raise Constraint_Error except that the message Value out of range will be associated
with the exception occurrence. Since an exception is not a data object, you cant use an exception as a parameter to a
subprogram. You can get a data object representing an exception using the Identity attribute which produces a
valuewhich produces a of type Ada.Exceptions.Exception_Id, and it is this value which is passed as the first parameter
to Raise_Exception..
To illustrate this, Ill briefly describe how file input/output works in Ada. File input/output is fundamentally no
different to dealing with the keyboard and screen. The Text_IO package provides all the necessary facilities. The main
difference is that you need to open a file before you can use it, and you must close it when youve finished using it. To
open a file you first of all have to declare an object of type File_Type (defined in Ada.Text_IO):
File : Ada.Text_IO.File_Type;
Now you can open the file using the procedure Open:
This opens an input file whose name is diary. The Mode parameter is an enumeration with three possible values.
In_File means the file is to be opened for input, as in this case. Out_File means the file is to be opened for output. Any
existing contents of the file will be lost in this case; Out_File specifies that the existing contents of the file should be
scrapped. If you dont want to do this you can use Append_File, which means that whatever output you write to the file
will be appended to the end of the files existing contents (if any). If the file doesnt already exist, Open will generate a
Name_Error exception. If you want to create a brand new file for output, you can use the procedure Create:
This will create a new output file with the given name if it doesnt already exist, or destroys the existing contents of the
file if it does exist. You can optionally supply a Mode parameter as with Open; the default is Out_File, but you might
want to use Append_File instead so that you will append your output to the file if it already exists. If the name isnt
legal for some reason a Name_Error exception will be raised; for example, some systems cant handle filenames
containing asterisks or question marks. The other exceptions that can occur when you try to open or create a file are
Status_Error, which indicates that the file is already open, and Use_Error, which is raised if you cant open or create
the file for any other reason (e.g. if there is no more disk space).
When youve finished using a file you should close it by calling the procedure Close:
Close (File);
While the file is open you can use Get to read it if its an input file and Put or Put_Line to write to it if its an output
file. The only difference from using the keyboard and the screen is that you have to specify the file you want to read
from or write to as the first parameter:
If you try to read from a file when youve reached the end of it, an End_Error exception will be raised. To avoid this
you can test if youre at the end of the file using the function End_Of_File, which is defined in Ada.Text_IO like this:
This returns the value True if youre at the end of the file.
To illustrate how exception handling is used with file I/O, heres an example program which counts the number of
words in a file:
-- Process file
while not End_Of_File (File) loop
-- The end of a line is also the end of a word
if End_Of_Line (File) then
In_Word := False;
end if;
The program is divided into the three traditional parts: initialisation (open the file), main processing (process the file)
and finalisation (close the file and display the results). Opening the file involves getting the name of the input file from
the user and then attempting to open it. This is done in a loop, and the loop is exited as soon as the input file is
successfully opened. If attempting to open the file raises an exception, the exception handler for the block inside the
loop displays the error message; the loop will then be executed again to give the user a chance to type in the filename
correctly.
Once the file has been opened, the main processing loop begins. A variable called In_Word is used to keep track of
whether we are in the middle of processing a word; initially its set to False to indicate that were not processing a
word. A non-space character means that the start of a word has been seen, so In_Word is set True and the word count in
Count is incremented. Once inside a word, characters are skipped until the end of the current line is reached or a space
is read, using the function End_Of_Line to test if the current position is at the end of a line of input. Either of these
conditions signals the end of a word, so In_Word gets set back to False. When the end of the file is reached, the loop
terminates. The input file is then closed and the value of Count is displayed.
Exercises
7.1 Modify the guessing game program from exercise 5.1 to provide comprehensive exception handling to guard
against input errors of any kind.
7.2 Write a program which asks the user for the name of an input file and an output file, reads up to 10000 integers
from the input file, sorts them using the Shuffle_Sort procedure from the previous chapter, and then writes the
sorted data to the output file. Check it to make sure it copes with errors arising from non-existent input files, write-
protected destinations for output files, illegal filenames, and (one that often gets overlooked) using the same name
for both files.
7.3 Modify the packages JE.Dates from the end of chapter 4 to define an exception called Date_Error, and get the
Day_Of function to raise a Date_Error exception if it is called with an invalid date.
7.4 Modify the playing card package from exercise 6.3 to define an exception which will be raised if you try to deal a
card from an empty pack or replace a card in a full pack. Use the package to implement a simple card game called
Follow The Leader, in which a human player and the computer player are dealt ten cards each. The object of the
game is to get rid of all the cards in your hand. The first player lays down a card, and each player in turn has to
play a card which matches either the suit or the value of the previously played card (e.g. the Jack of Clubs could
be followed by any Jack or any Club). If a player has no card that can be used to follow on, an extra card must be
taken from the pack. If the pack becomes empty, the cards that were previously played (except the last one) must
be returned to the pack. The first player to play out a hand and have no cards left is the winner.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files
are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 8:
Program design and debugging
It is to be noted that when any part of this
paper appears dull, there is a design in it.
Sir Richard Steele, The Tatler
A good way to start is to try to split your problem up into a number of smaller subproblems and then deal with each of them
in turn. This is known as stepwise refinement or top-down design. It is a divide-and-conquer approach which lets you avoid
having to deal with a large and complex design as a single monolithic unit. The problem can be broken down into a set of
smaller steps, which can then be refined into more detail by applying the same process. The design of the calculator example
at the end of chapter 3 used this approach.
Top-down design lets you avoid getting bogged down in details until the last possible moment. Its not entirely foolproof; to
be able to do this effortlessly involves having an appreciation of where youre trying to get to and a vague idea of what kind
of low-level details youll end up having to deal with. In particular, it helps to know what packages are available that can
provide pieces of the jigsaw puzzle youre trying to put together; if you can steer your breakdown of the solution in the
general direction of being able to use some existing packages, you can save yourself some effort. This is part of the craft of
programming which you have to learn through experience; if there was a formula you could apply to generate a correct
solution someone would have written a program to do it and programmers would find themselves out of a job. In fact, as
youve already seen, there is no single correct solution to a particular problem; different people will tend to find different
solutions to the same problem.
Fortunately, there are some general principles that you can use as a guide to get you started. As I mentioned in chapter 3, a
good start in most cases is to divide the problem up into an initialisation part, a main processing part and a finalisation part.
The initialisation does any initial setting up that may be required (e.g. displaying a window on the screen or opening some
files) and the finalisation does any final tidying up (destroying windows, closing files, etc.) before the program ends. The
main processing in the middle is where all the hard work is done. This is usually a loop which repetitively processes events
such as user input, mouse movements, the passage of time or whatever. You may well end up designing the main processing
section first and then identifying what initialisation and finalisation it requires.
Beyond this point you have to look at what sort of thing youre trying to do. Does it involve repeating some action over and
over again? If so, you need a loop statement which encloses the required action. Alternatively, does it involve choosing
between different actions? If so, you need an if or case statement. Now youve got one or more smaller actions inside your
loop, if or case statement which you can now break down into smaller pieces using the same approach.
If you want to, you can just sweep some of the details under the carpet by inventing a procedure or function which will
(eventually) deal with some aspect of the problem. Your initial implementation of the procedure or function might be a stub
which does nothing or which cheats in some way (e.g. by getting the user to supply the value to be returned from a function);
you can then test the general outline of the program before coming back to your stub and doing the job properly.
To illustrate all this Im going to develop a larger example, an electronic diary which can be used to keep track of your
appointments. It will need to provide as a minimum the ability to add new appointments, delete existing appointments,
display the list of appointments and save the appointments to a file. Also, if there are any saved appointments the program
should read them in when it starts up.
The initialisation part of this program will involve reading the diary file if it exists. The main processing will consist of
displaying a menu of choices, getting the users response and carrying out the specified operation. At the end, there isnt
really anything more to do.
Loading the diary from a file can be delegated to a procedure which Ill call Load_Diary and define in JE.Diaries:
The main processing (dealing with commands from the user) is a repetitive task: repeatedly get a command and do it. This
means that the main processing will be a loop of some sort:
Processing a command involves displaying a menu, getting a command in response to the menu, and then doing it. Ill use
single characters for the command responses, so Ill need a Character variable to store it in which Ill call Command. The
loop will be terminated when the user selects the Quit command. We can expand the design a bit further now:
Getting a command is trivial; it involves getting a single character using Get from Ada.Text_IO. This means that
Ada.Text_IO must be added to the list of packages in the with clause:
-- display menu
Get (Command);
The menu just needs to list the command choices (A for add, D for delete, L for list, S for save, Q for quit; you can expand or
alter the list of commands later if you need to). How you perform the selected command depends on which command it is;
its a choice of alternative actions, so an if or case statement is needed. A case statement is appropriate here since there are
several possible choices which depend on the value of Command:
-- get a command
Get (Command);
Displaying the menu and performing the selected command could easily be implemented as separate procedures, but Ive left
them in the main program so that its easier to see the correspondence between the menu and the choices in the case
statement, which ensures that if any changes are made to the menu it will be obvious whether the case statement has been
changed to reflect this. However, others might prefer to use procedures to minimise code bulk in the main program. The
difference in approach isnt major.
The Quit command is easy to deal with; this just involves exiting from the main loop. The others choice can simply display
an error message, and the remaining commands can be handled by procedures in JE.Diaries:
case Command is
when 'A' | 'a' =>
Add_Appointment;
when 'D' | d' =>
Delete_Appointment;
when 'L' | 'l' =>
List_Appointments;
when 'S' | 's' =>
Save_Appointments;
when 'Q' | 'q' =>
exit;
when others =>
Put_Line ("Invalid choice -- " &
"please enter A, D, L, S or Q");
end case;
So far so good. Now lets construct the specification of JE.Diaries from what weve got so far; all it needs is a list of the
procedures referred to by the main program:
package JE.Diaries is
procedure Load_Diary;
procedure Add_Appointment;
procedure Delete_Appointment;
procedure List_Appointments;
procedure Save_Appointments;
end JE.Diaries;
type Date_Type is
record
Day : Day_Type;
Month : Month_Type;
Year : Year_Type;
end record;
type Time_Type is
record
Hour : Hour_Type;
Minute : Minute_Type;
end record;
type Appointment_Type is
record
Date : Date_Type;
Time : Time_Type;
Details : String (1..50); -- an arbitrarily chosen size
Length : Natural := 0;
end record;
The only difference is that Appointment_Type now contains a Length component to record the actual length of the Details
component; this will allow a maximum of 50 characters rather than exactly 50 characters. The maximum length is arbitrary;
you can change it if you want longer appointment details.
Now well need an Appointment_Array together with a variable to keep track of how many appointments there actually are
in the diary. This calls for another record type declaration:
Im only allowing ten entries at the moment so that itll be easy to test that the program behaves properly when the diary is
full. The size of the diary and the length of the details string can both be changed by changing a single line of the declarations
above, recompiling the package body and then relinking the main program. Well need to be careful not to assume that those
values will always be what they are now, and use the 'Last attribute for the length of the details string and the Maximum
discriminant for the number of entries.
The procedures can be implemented as stubs for now. These are temporary versions of the procedures that we can use to
complete the package body so that it can be compiled, and the bits that have been written so far can be tested. What Ill do is
provide versions of the procedures that just display a message to say theyve been called (which will also require a with
clause for Ada.Text_IO at the top of the package body):
procedure Load_Diary is
begin
procedure Add_Appointment is
begin
Put_Line ("Add_Appointment called");
end Add_Appointment;
procedure Delete_Appointment is
begin
Put_Line ("Delete_Appointment called");
end Delete_Appointment;
procedure List_Appointments is
begin
Put_Line ("List_Appointments called");
end List_Appointments;
procedure Save_Appointments is
begin
Put_Line ("Save_Appointments called");
end Save_Appointments;
The full version of the package body obtained by putting the above bits and pieces together looks like this:
with Ada.Text_IO;
use Ada.Text_IO;
package body JE.Diaries is
type Date_Type is
record
Day : Day_Type;
Month : Month_Type;
Year : Year_Type;
end record;
type Time_Type is
record
Hour : Hour_Type;
Minute : Minute_Type;
end record;
type Appointment_Type is
record
Date : Date_Type;
Time : Time_Type;
procedure Load_Diary is
begin
Put_Line ("Load_Diary called");
end Load_Diary;
procedure Add_Appointment is
begin
Put_Line ("Add_Appointment called");
end Add_Appointment;
procedure Delete_Appointment is
begin
Put_Line ("Delete_Appointment called");
end Delete_Appointment;
procedure List_Appointments is
begin
Put_Line ("List_Appointments called");
end List_Appointments;
procedure Save_Appointments is
begin
Put_Line ("Save_Appointments called");
end Save_Appointments;
end JE.Diaries;
Once youve checked that the program works so far, you can replace the stubs with working versions of the code for each
procedure. One of the advantages of top-down design is that it lets you postpone worrying about the finer details of your
programs until the last possible moment; it also lets you do incremental testing, where you test each part of the program
before you go any further.
At this point we want to be confident that the main program works. Once we know that it can cope with anything we throw at
it we can get down to implementing the rest of the program. There isnt any point in proceeding until we know that
everything so far works; apart from anything else, its still only a small program which can be tested, fixed and then
recompiled quite quickly.
The first thing to do is to test it with correct input. When the program starts up it should display the message Load_Diary
called followed by the menu. Try typing A, D, L and S to make sure that the correct procedure is called in each case. Now try
a, d, l and s. Is everything all right? This has tested the three main menu commands; the remaining ones to try are Q and q.
Type Q and the program should terminate. Now start it up again and try q instead.
So, the program so far works with correct input. Fortunately at this stage the possibilities can be tested exhaustively; in most
cases you have to choose a representative set of test data since there are too many possibilities to test them all (unless you
know somewhere where you can hire an infinite number of monkeys, that is!). Now what about incorrect input? First of all
try typing X. You should get a message saying Invalid choice -- please enter A, D, L, S or Q. So far so good. Now try
typing Add. You should immediately spot that theres a problem.
You can probably see what the problem is straight away in this case, but to show you how to track down problems of this sort
lets pretend weve been hit by an attack of stupidity. If you cant figure out whats happening, the first thing to do is to get
more information about whats going wrong. You may have access to a debugger which will let you step through the
program line by line, or set breakpoints so that the program halts whenever it gets to a particular line to let you inspect the
values of selected variables as the program is running. Then again, you may not.
Using a debugger is the simplest thing to do as it doesnt involve making any changes to the program to find out whats going
on. In the absence of a debugger, you have to modify the program to display a bit more information. In this case we know
that the problem manifests itself in the case statement, so the simplest thing to do is to use Put to display the value of
Command just before the case statement:
Tinkering with the program like this means you have to be careful to fix all your changes and test everything again when
youve finished debugging. The comment DEBUG at the end of the line is to make it easy to find and remove lines added
for debugging purposes once the bug has been fixed. It might also be a good idea to comment out the lines displaying the
menu (i.e. make them into comments so that they have no effect but can be reinstated later) in case debugging information
scrolls off the top of the screen:
Again, the comment DEBUG shows that the lines are commented out for debugging purposes so we can track down these
lines and uncomment them after the bug has been fixed. Removing lines is far more risky; you might introduce extra bugs by
doing so, or even remove the source of the bug youre trying to find! A better alternative would be to send debugging output
somewhere else (e.g. into a file, or to a separate screen or window). Anyway, if you make the changes above, run the
Load_Diary called
It should now be fairly obvious whats going on. Each of the three characters on the line is being read in and treated as a
command. A simple solution is to use Skip_Line to get rid of the rest of the line immediately after the call to Get:
-- get a command
Get (Command);
Skip_Line;
Now if you recompile and try again, this is what you should see:
Load_Diary called
Theres one more test to try. What happens if you type in the end-of-file character (which is usually control-Z or control-D)?
You should find that the program halts with an End_Error exception. The solution to this one is obviously to add an
exception handler for End_Error. However, after an end-of-file, the program might not be able to read any more input from
the keyboard, so there probably isnt any point in going round the loop again. The simplest thing to do is to exit the program.
The end-of-file test is always a good one to remember; its surprising how many programs written by novices will just fall
over gracelessly if you type the end-of-file character.
loop
-- display menu
... as before
-- get a command
Get (Command);
Skip_Line; -- Bug fix added here
-- perform selected command
case Command is
... as before
end case;
end loop;
procedure List_Appointments is
begin
for I in Diary.Appts'First .. Diary.Count loop
Put ( Diary.Appts(I) );
end loop;
end List_Appointments;
Im assuming the existence of a Put procedure to display an appointment in an appropriate form on the screen; well need to
provide this in the package body. The loop uses the value of the Count component of Diary to control the number of times the
loop will be executed, and thus the number of appointments that will be displayed.
The trouble with this is that its too simple; when Count is zero, the loop will be executed zero times, with the result that
absolutely nothing will be displayed. It would be better to detect this as a special case and deal with it separately:
procedure List_Appointments is
begin
if Diary.Count = 0 then
Put_Line ("No appointments found.");
else
for I in Diary.Appts'First .. Diary.Count loop
Put ( Diary.Appts(I) );
end loop;
end if;
end List_Appointments;
For the sake of simplicity, Ill assume that the appointments are stored in ascending order of date and time, so that listing
them in sequence produces an ordered list rather than a random jumble of appointments. This will be something to bear in
mind when we consider how to add new appointments. Ive also ignored the situation where there are more appointments
than will fit on the screen; if this happens the screen will scroll up so that youll only be able to see the last screenful of
appointments.
We need to implement Put before we can test this properly. Heres a possible implementation:
This in turn requires versions of Put for the individual components of the appointment. These are all subtypes of Integer apart
from Month_Type, so we can just instantiate Integer_IO for Integer and Enumeration_IO for Month_Type:
with Ada.Text_IO;
use Ada.Text_IO;
package body JE.Diaries is
subtype Day_Type is Integer range 1..31;
type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov,
Dec);
subtype Year_Type is Integer range 1901..2099;
subtype Hour_Type is Integer range 0..23;
subtype Minute_Type is Integer range 0..59;
... etc.
end JE.Diaries;
You can test this before proceeding any further; you should just get a message saying No appointments found. Youll need
to implement Add_Appointment before you can test any further.
At the moment, the version of Put defined above will display a time like 10 oclock as 10: 0 (i.e. the minute part of the
time will be padded with a space rather than a leading zero). Fix this so that times are displayed properly, e.g. 10:00.
the diary. You can start with a simple-minded version that doesnt keep the appointments in order; this will let you test that
the Add_Appointment procedure works so far before going any further. All we need to do at this stage is to ask the user to
enter the appointment details and then add the appointment to the end of the list. Well need an Appointment_Type variable
to hold the new appointment; we can write the code to get the appointment details like this:
procedure Add_Appointment is
New_Appt : Appointment_Type;
begin
Put_Line ("Enter appointment details...");
Put ("Date: ");
Get (New_Appt.Date);
Put ("Time: ");
Get (New_Appt.Time);
Put ("Details: ");
Skip_Line;
Get_Line (New_Appt.Details, New_Appt.Length);
This assumes that JE.Diaries will provide versions of Get to read in dates and times. Input is always an area where legitimate
errors can occur due to typing mistakes, so you should always think about exception handling whenever input is done. You
could just run the version of Add_Appointment above and find out which exceptions will occur by trial and error. Youll find
that a constraint error or a data error will be raised if the input is incorrect, so an exception handler will be required to deal
with these errors. Heres what it might look like:
exception
when Data_Error | Constraint_Error =>
Put_Line ("Invalid date or time");
Skip_Line;
Separate exception handlers for getting the date and for getting the time would improve things even more since you
would be able to tell the user more precisely whats happened. Change the program so it does this.
Simple versions of Get for dates and times can just use the versions of Get for Integers and Month_Types which are already
available in Int_IO and Month_IO to read in the components of the record, like this:
This does minimal checking; it might be a good idea to check that the date is valid in the version of Get for dates. You could
use the function Valid from chapter 4 or a variant of it, or use Ada.Calendar.Time_Of as described in the previous chapter to
do this. If the date is invalid, a sensible response would be to raise a Constraint_Error so that entering the 31st of February
would be reported as the same sort of error you would get if you entered the 32nd of January.
Think about what different formats you might want Get to be able to accept. See how close to the ideal you can make
your version of Get.
Now to add the appointment to the end of the diary. This involves incrementing the number of appointments (Diary.Count)
so that it refers to the next free appointment and then storing the new appointment at that point in the array:
Diary.Count := Diary.Count + 1;
Diary.Appt (Diary.Count) := New_Appt;
If you try this out, youll find that it will also raise a constraint error when the diary is full (i.e. when Diary.Count goes out of
range). The code for reading the appointment details also needs to handle constraint errors, so it needs to go in a separate
block with its own Constraint_Error handler to allow the code for adding the appointment to have a different
Constraint_Error handler. The final version of Add_Appointment will look like this when you put all these bits together:
procedure Add_Appointment is
New_Appt : Appointment_Type;
begin
begin
Put_Line ("Enter appointment details...");
Put ("Date: ");
Get (New_Appt.Date);
Put ("Time: ");
Get (New_Appt.Time);
Put ("Details: ");
Skip_Line;
Get_Line (New_Appt.Details, New_Appt.Length);
exception
when Data_Error | Constraint_Error =>
Put_Line ("Error in input -- appointment not added");
Skip_Line;
end;
Diary.Count := Diary.Count + 1;
Diary.Appts (Diary.Count) := New_Appt;
exception
when Constraint_Error =>
Put_Line ("Diary full -- appointment not added");
end Add_Appointment;
Now there are two separate Constraint_Error handlers: one in the inner block to cope with out-of-range input values and one
in the outer block to cope with a full diary. When you test this you should discover that when there is an error in the input to
Add_Appointment, an appointment is still added. This is because after the inner exception handler, execution continues with
the statements after the inner block which will add a non-existent appointment to the diary. This can be fixed by adding a
return statement to the inner exception handler:
exception
when Data_Error | Constraint_Error =>
Put_Line ("Invalid date or time");
Skip_Line;
return;
At this point you can test List_Appointments more thoroughly. If you start with an empty diary you can add appointments
one by one, listing the appointments before and after you do so, and see what happens. Your testing should reveal that there
is still a bug in Add_Appointment.
Well, have you spotted the bug? If not, try harder! Once youve found it you can discover the cause of it by printing
out the values of some of the main variables used in Add_Appointment and List_Appointments. Fix it before you go
any further.
procedure List_Appointments is
begin
if Diary.Count = 0 then
Put_Line ("No appointments found.");
else
for I in Diary.Appts'First .. Diary.Count loop
Put (I, Width=>3); Put (") ");
Put ( Diary.Appts(I) );
end loop;
end if;
end List_Appointments;
The steps involved in deleting an appointment will be to read in the appointment number, check that its valid and then delete
the corresponding appointment from the array. This means that well need to declare a variable to hold the appointment
number that the user types in:
Appt_No : Positive;
Reading in the appointment number involves displaying a prompt and then reading a number into Appt_No:
Of course, well need to check that the appointment number is valid. Well need to provide an exception handler to check for
input errors (constraint and data errors) as is nearly always the case when performing input:
exception
when Constraint_Error | Data_Error =>
Put_Line ("Invalid appointment number");
Skip_Line;
Diary.Count gives the number of appointments that the diary currently holds, so Appt_No must not be greater than
Diary.Count. An easy way to respond to this is to treat it as a constraint error:
Deleting the appointment involves moving all the appointments after the one identified by Appt_No up one place in the array
and decrementing Diary.Count. The appointments can be moved using a slice which selects all the entries from Appt_No+1
to Diary.Count:
procedure Delete_Appointment is
Appt_No : Positive;
begin
Put ("Enter appointment number: ");
Get (Appt_No);
if Appt_No not in Diary.Appts'First .. Diary.Count then
raise Constraint_Error;
end if;
Diary.Appts(Appt_No..Diary.Count-1) := Diary.Appts(Appt_No+1..Diary.Count);
Diary.Count := Diary.Count - 1;
exception
when Constraint_Error | Data_Error =>
Put_Line ("Invalid appointment number");
Skip_Line;
end Delete_Appointment;
Testing this will require typing in both valid and invalid appointment numbers and checking that the correct appointment
disappears when a valid appointment number is given. Testing for boundary cases is always important, that is to say those
values at the upper and lower limits of the range. If youve got appointments numbered 1 to 5, does the procedure work
properly for 1 and 5 and does it correctly report an error for 0 and 6? Similarly, test the boundary cases for the number of
appointments in the diary. Does it work correctly when the diary is full, or when its empty, or when it just has a single
appointment in it?
Theres a bug in the code above; try typing 1 X when youre asked for the appointment number (assuming there is
an appointment 1, of course). Track down the bug and fix it.
Saving the appointments can be done just like List_Appointments except that the appointments dont need to be numbered
and theyre written to a file instead of being displayed on the screen. It would also be a good idea to write the number of
appointments at the start of the file. A File_Type variable (Diary_File) will be needed to do the file accesses. Heres an
outline:
procedure Save_Appointments is
Diary_File : File_Type;
begin
-- open the file
-- write Diary.Count to the file
for I in Diary.Appts'First .. Diary.Count loop
-- write the I-th appointment to the file
end loop;
-- close the file
end Save_Appointments;
Opening the file can be done using Create as described in the previous chapter. Closing the file just involves using Close.
Various exceptions can be raised by Create, so an exception handler will be needed. Use_Error indicates that the file couldnt
be created; perhaps the disk is full or you dont have write access to it, so this should be reported to the user. Name_Error
will be raised if the filename is invalid and Status_Error will be raised if the file is already open. Neither of these should
occur unless a total disaster occurs (the constant string "Diary", which is presumably a valid filename, has been corrupted
somehow, or youve opened the file and forgotten to close it elsewhere in the program), so they shouldnt be handled. That
way if they ever do occur the exception will be reported as a genuine error which terminates the program:
procedure Save_Appointments is
Diary_File : File_Type;
begin
Create (Diary_File, Name => Diary_File_Name);
-- write Diary.Count to the file
for I in Diary.Appts'First .. Diary.Count loop
-- write the I-th appointment to the file
end loop;
Close (Diary_File);
exception
when Use_Error =>
Put_Line ("Couldn't create diary file!");
end Save_Appointments;
Writing the appointments can be done using appropriate versions of Put for each of the appointments components. A space
should be output between each component to separate them in the file so that they can be read in by Load_Diary, and each
procedure Save_Appointments is
Diary_File : File_Type;
begin
Create (Diary_File, Name => Diary_File_Name);
Put (Diary_File, Diary.Count);
New_Line (Diary_File);
Notice how Ive used a local block inside the loop so that I can rename the current appointment to avoid having to use long-
winded names like Diary.Appts(I).Date.Day.
instead of:
The effects of this sort of mistake can be easy to overlook if youre not being sufficiently careful. The program will compile
and it will even appear to work. The spaces will be displayed invisibly on the screen instead of in the file, and if it werent for
the Width=>1 parameter in the calls to Put you might not notice it due to the number of spaces that will be output in front of
each integer. The only way youd notice it in this case is if youd worked out exactly what the diary file should look like and
discovered that the actual file didnt meet your expectations. This illustrates just how methodical you have to be if you dont
want to end up with a buggy program.
Load_Diary will need to do the same as Save_Appointments but in reverse; itll need to open the file for input, read the
number of appointments, and then read in the appointment details one by one and then close the file:
procedure Load_Diary is
Diary_File : File_Type;
begin
-- open the file
-- read Diary.Count
for I in Diary.Appts'First .. Diary.Count loop
-- read the I-th appointment from the file
end loop;
-- close the file
end Load_Diary;
The file can be opened using Open. The exception handling for Open will need to be slightly different to that for Create;
Name_Error indicates that the file doesnt exist, and one way to handle this is to do nothing, so that the diary will just start
off empty. Heres the next version, with a few more details filled in:
procedure Load_Diary is
Diary_File : File_Type;
begin
Open (Diary_File, Mode => In_File, Name => Diary_File_Name);
-- read Diary.Count
Close (Diary_File);
exception
when Name_Error =>
null;
when Use_Error =>
Put_Line ("Couldn't open diary file!");
end Load_Diary;
Reading the appointments from the file can be done using a series of calls to Get:
procedure Load_Diary is
Diary_File : File_Type;
begin
Open (Diary_File, Mode => In_File, Name => Diary_File_Name);
Get (Diary_File, Diary.Count);
Skip_Line (Diary_File);
declare
Appt : Appointment_Type renames Diary.Appts(I);
begin
Get (Diary_File, Appt.Date.Day);
Get (Diary_File, Appt.Date.Month);
Get (Diary_File, Appt.Date.Year);
Get (Diary_File, Appt.Time.Hour);
Get (Diary_File, Appt.Time.Minute);
Get_Line (Diary_File, Appt.Details, Appt.Length);
end;
end loop;
Close (Diary_File);
exception
when Name_Error =>
null;
when Use_Error =>
Put_Line ("Couldn't open diary file!");
end Load_Diary;
You can test this by starting the program up, adding some appointments, saving them and then quitting. You should then
have a file called Diary which you can look at with a text editor to verify that its correct. Make a copy of the file, and then
start the program again. If you list the appointments you should see exactly what you had before. If you save the
appointments again and quit the program, the Diary file and the copy of it that you saved earlier should be identical.
Unfortunately youll find that they arent identical; you should see two spaces between the time and the details in the latest
version of the file but only one space in the saved copy. Of course, whats happened is that Load_Diary has read the
separating space as the first character of Details, so it will need modifying to read and ignore the space:
procedure Load_Diary is
Diary_File : File_Type;
begin
Open (Diary_File, Mode => In_File, Name => Diary_File_Name);
Get (Diary_File, Diary.Count);
Skip_Line (Diary_File);
Close (Diary_File);
exception
when Name_Error =>
null;
when Use_Error =>
Put_Line ("Couldn't open diary file!");
end Load_Diary;
One of the advantages of top-down design is that it allows programs to be developed on a piecemeal basis, a bit at a time.
That way you dont end up biting off more than you can chew. You can use stubs or incomplete versions to get yourself to a
point where you can start testing; after your tests have been passed satisfactorily, you can incrementally implement a bit
more, do a bit more testing, and so on until youre finished (phew!). Testing a program thoroughly is an essential part of
development; youve got to try and think of everything that can go wrong and what to do about it. You can help track down
bugs by adding extra code to display debugging information or commenting out code as necessary, or by using a debugger. If
all else fails, I find that just explaining the problem to anyone whos prepared to listen usually helps. If they understand Ada,
they might spot something youve overlooked; if they dont, you might find that you spot the problem as youre trying to
explain whats happening (or at least come up with a theory as to what the problem is). Well, it works for me anyway!
Exhaustive testing is usually out of the question, so you have to come up with a representative set of test data and a plan of
how youre going to carry out your testing. Plan your development steps in advance and do them in an order which will help
you to test things. Make sure that valid data is handled correctly; look particularly closely at boundary conditions since these
are usually where bugs can creep in. Make sure invalid data is detected and dealt with properly. Make sure you know what
you expect the program to do, and check that it does exactly what you expect. Get used to looking at your code with a critical
eye, and try to think of things that can go wrong as youre writing it (such as the user typing an end-of-file character
unexpectedly). And when you think youve finished and theres no more to be done, spend a bit more time playing around
with the program doing a mixture of sensible and silly things, because there might still be some combination of
circumstances youve overlooked.
Exercises
8.1 It would be a good idea to ask the user whether or not to save any changes made to the diary before quitting. This is only
necessary if any appointments have been added or deleted since the last time the diary was saved. Make the necessary
modifications to the program.
8.2 Change the way that appointments are listed so that they are displayed a screenful at a time (or just under a screenful to
allow room for prompts etc.) on the system that youre using. Provide the ability to go either forwards or backwards a
screen or half a screen at a time.
8.3 Add a Find command to display all appointments containing a specified string in the Details component. Ignore
distinctions between upper and lower case when searching for the string.
8.4 Write a program which will read the diary file and display all appointments for the next three working days (where
Saturday and Sunday do not count as working days), so that if you ran this program on a Thursday it would show you all
appointments from that day until the following Monday inclusive, since the three working days involved are Thursday,
Friday and Monday.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are
distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Part Two:
Abstract data types
9. Private types
10. Designing with abstract data types
11. Dynamic memory allocation
12. Generics
13. Building a calculator
This part moves the focus from the mechanics of processing data in small examples to the
representation of the data being processed. The previous part showed how Ada allows you
to define new data types, but it often appears easier to use built-in types provided by the
language. In larger-scale applications where maintenance is the dominant cost factor this
turns out to be a false economy due to the increased testing and debugging costs incurred
after every change. The data types are not faithful representations of the types of object
being modelled in the real world, and so it is possible to perform unrealistic actions on
them. The more a program is changed, the more likelihood there is of this happening.
Design approaches like stepwise refinement only attack the problem of processing data.
Choosing a sensible organisation for the data to make it easier to process is also essential.
Top-down design of programs can only work if you have a firm idea of the architecture of
the bottom layer that you are trying to reach. It also takes no account of the seismic
changes that will result if the nature of this bottom layer changes. This part introduces
abstract data types as a way of providing a stable bottom layer towards which a program
design can aim, and it shows how a design using abstract data types can be used to contain
the impact that maintenance changes in one part of the design will have on other parts.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the
constituent files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 9:
Private types
What is essential is invisible to the eye.
Antoine de Saint-Exupry, The Little Prince
So why do I say this? Whats the matter with it? After applying all these design techniques, shouldnt we expect the result to
be a shining example of program design? The unfortunate truth is that top-down design is a technique with some severe
limitations. It doesnt necessarily take into account the need for reusability, portability or maintainability. It assists in
producing maintainable programs in so far as you can replace low-level modules with improved versions, but not when the
maintenance needs affect the outer levels of the design. It concentrates on the processing of the data and doesnt pay enough
attention to the data itself.
You want to be able to look at several independent diaries at the same time. In order to schedule a meeting, you might
want to be able to open the diaries of those involved and find a common free slot when you can hold your meeting and
then book that slot in your own diary, if not in all of them. You might also want to be able to copy appointments from
one diary into another.
You want to move from a text-based environment to a graphical user interface. Commands to add or delete
appointments might be accessed via a pull-down menu. Appointment details might be entered by filling in a form, and
dates might be selected by pointing at a picture of a calendar using a mouse.
You want to integrate your diary into another application: for example, an electronic mail system that will
automatically send out reminders of important meetings to those involved or will add appointments to your diary
which are embedded in incoming mail messages.
You want to change the way dates are represented to make it easier to do arithmetic on dates. Rather than storing a
date as a day, a month and a year you want to represent it by the number of days since January 1st 1901 (a Julian
date).
In all of these cases youll end up more or less rewriting the entire program. You need to do more than just fiddle about with
low-level details in these situations. In the first case you suffer from the fact that there is a single diary buried in the package.
Youll need to move the declaration into the package specification where it is visible to the rest of the program; youll need to
rejig the main program to provide extra commands for opening and closing and selecting different diaries; and youll have to
pass the selected diary as a parameter to each of the procedures in the package. There wont be any part of the program which
escapes the effects of these changes. In the second case the input/output in the main program (displaying the menu and
getting the users response) will need changing, but so will the input/output in the package procedures (getting the details of a
new appointment, selecting the appointment to be deleted, and so on). In the third case the package wont be reusable in the
context of an electronic mail program since it does input and output to the screen and cant accept an appointment from
another source. Youd have to add the ability to pass appointments around as parameters to the procedures in the package,
which implies a complete rewrite. In the final case all the places where the components of dates are accessed will need
rewriting since those components will no longer exist.
In other words, the program is inflexible in its present state. What top-down design as practised in the previous chapter gives
you is a workable but brittle program. It will do exactly what you originally wanted, but what it wont necessarily do is adapt
easily to changing requirements. None of these maintenance scenarios could have been anticipated at the time of the original
development, but maintenance requirements are always going to be unpredictable unless you have an extremely high-class
crystal ball in your desk drawer.
However, all is not lost. If you bear in mind the need for maintenance and reuse as you design, you can come up with designs
that are a lot less nave than the one in the previous chapter. The maintenance problems arise largely from the fact that the
design concentrated on defining the processing required, and the design of the data structures involved was treated as a
subsidiary problem of lesser importance. What is needed is a more data-centred approach. The representation of dates,
appointments and so on used record types as containers for a collection of simpler types. Record types have a very limited
repertoire of standard operations (assignment and equality testing only) unlike scalar types like Integer which have a rich set
of predefined operations. The program just used standard operations to manipulate the record components rather than
defining a set of procedures and functions to provide a range of operations for the record types themselves. What is really
needed is a set of operations on appointments, dates and so on so that these types can be dealt with as entities in their own
right rather than as containers for simpler types. We want to have a set of operations available for dates rather than having to
deal with the day, month and year components of dates.
How will this help? Consider the final maintenance scenario described above. The program in its present form relies heavily
on the internal representation of dates. If Date_Type were defined in a package of its own which provided a function Day
which returned the value of the day component, it would be a relatively simple matter to change the way dates are
represented as long as the program using the package always used the function to access the day component rather than
accessing it directly. Functions like Day would need rewriting to accommodate the new representation, but this would just be
a change to the package body. Programs using the package would not need to be changed. And, as long the package provided
a sufficiently rich set of operations on dates, it would be usable in any program which needed to operate on dates. What we
would end up with is an abstract data type (ADT) whose operations would allow us to manipulate dates without worrying
about the internal representation of the type. An abstract data type should be like a black box whose internal workings are
invisible to the user. If you think about it, this is what happens with types like Integer. We dont care how Integers are
represented inside the computer; all we care about is that we can do addition and subtraction and so on. What we want to do
is to elevate Dates to the point that they are indistinguishable from any of the built-in data types like Integer. We should just
be able to use them as if they too were built in to the language.
Changes to the package body will not involve any changes to the clients, since the package body is not part of the packages
contractual requirements (i.e. it isnt visible to the packages clients). Changes to the specification will involve changes to the
clients as well, and since a package may be used by many different programs this can be expensive. Careful design is
therefore required to reduce the risk that the package specification might need changing. Here are some important principles
that should guide you when you design packages (which I deliberately ignored in the previous chapter!):
Avoid revealing implementation details. The more information you can hide about implementation details, the more
freedom you will have to make changes to them later. Anything visible in the package specification will be one of the
contractual obligations for the package, so information should only be provided in the specification if you're willing to
be tied down to the obligation that results. Programs will be written which rely on the things you reveal in the package
specification, so any changes to the visible specification might make it necessary to rewrite a lot of client code with
consequential costs in testing and debugging.
Avoid trying to handle errors in the package. Undoubtedly there will be situations where error checks should be made,
but dont confuse error detection and error handling. Although your package may be able to detect errors, this doesnt
mean it should try to handle them. Thats what exceptions are for; errors can be detected in one place and handled in
another. In most cases, if you detect an error in a package you should just raise an exception and let the client decide
how best to handle it. If you dont do this your package loses a lot of generality. Displaying an error message may
seem like a good idea, but what happens if your package might be used in the control system for a washing machine
with nowhere to display the message?
Avoid doing input/output in the package. If your package does its own input and output it is tying itself down to a
particular way of interacting with the user, and so its making assumptions about the clients that it will be used with. It
may work fine when its being used in a program with a text-based user interface, but if the package displays prompts
and reads input from the user this will prevent it from being used with a graphical interface where data is entered via
dialog boxes which might involve choosing an option from a list rather than typing it in. Let the client program deal
with user interface issues; the program should read the data in an appropriate fashion and pass it as parameters to
procedures in the package. Likewise the program should be responsible for displaying any results in the appropriate
form.
Hiding the data structure in the diary avoided revealing its implementation details, so this is apparently in accordance with
the first of these three principles. However, its a bad idea as you end up with a single data structure which is referenced from
many different places and it becomes more and more difficult to untangle the program from the single instance of the data
structure as the program grows. If at some point you want more than one instance of the data structure (as in the first
maintenance scenario above) youre in real trouble. Heres one more design principle to round things off:
Avoid using global data structures. By global I mean something which is widely accessible, something which can be
referred to directly from many places within a program. Direct access by subprograms produces an undesirable
coupling between the subprograms and the data structure. The subprograms end up being dependent on the data
structure so that even a simple change of name causes serious repercussions throughout the program. What you should
almost always have is a data type rather than a single global instance of a type which can be accessed directly by
subprograms. By declaring a data type, you can create as many instances of the type as you need. The data type (only
its name, not its internal details!) should be widely visible, but individual instances of it should not. Instances should
be passed as parameters to the subprograms that manipulate them This breaks the coupling between the subprogram
and any particular object; the subprogram will be able to operate on any instance of the type that it is given.
Rather than saying to yourself I am writing a program to manage an electronic diary you should say I am developing an
electronic diary data type; the program is then almost an afterthought, a particular client of the package that contains the data
type and the operations it supports. Writing the program may help to identify the operations that a diary should support, but
the diary should be the focus of attention rather than the program that uses it. That way there wont be any problems if the
program then needs to be changed to handle multiple diaries.
In other words, a package should in most cases be a repository for one or more data types and a set of client-neutral
operations on those data types. By client-neutral I mean that the operations shouldnt assume anything about the clients
which might use them, so they shouldnt try to handle errors or do input/output, both of which are very client-specific
activities. Packages should just provide services to clients that need them without assuming any knowledge about who those
clients might be. Occams razor should be applied to package design: a package should provide those operations that are
absolutely necessary but nothing more than is absolutely necessary.
package JE.Dates is
subtype Day_Type is Integer range 1..31;
type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov,
Dec);
subtype Year_Type is Integer range 1901..2099;
type Weekday_Type is (Sun, Mon, Tue, Wed, Thu, Fri, Sat);
type Date_Type is
record
Day : Day_Type;
Month : Month_Type;
Year : Year_Type;
end record;
As discussed above, there is a fatal flaw in the package design as it stands. Clients which use the package just access the
internal representation of Date_Type in order to extract the components of a date. Clients which directly access the
components will need rewriting if the details of the data structure need to be changed at a later date. What is needed is a way
of preventing direct access to the components of Date_Type. To solve this problem, Ada allows us to define private types.
A private type is one whose name is given in a package specification but whose internal workings are kept private. Only the
package body is allowed to know about the internal representation, so that the body can use this information to implement the
operations declared in the package specification. The only standard operations you can perform on values of a private type
are assignment (:=) and testing for equality (= and /=). Heres how you declare a private type in a package specification:
This lets you declare as many variables of type Date_Type as you like, but (except in the package body) the only things you
can do with those variables is to assign them to each other using :=, compare them for equality and inequality using = and
/=, and use the operations declared in the package specification to manipulate them.
At some point, of course, you do have to reveal the internal workings of the type. The package body has to know how the
type is implemented and the compiler has to know how much space to allocate in memory when a variable of that type is
declared. This is done by placing a section headed private at the end of the package specification which divides it into two
parts: a visible part which is visible to users of the package and a private part which is only accessible from within the
package body.
package JE.Dates is
-- Visible part of package
subtype Day_Type is Integer range 1..31;
type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov,
Dec);
subtype Year_Type is Integer range 1901..2099;
type Weekday_Type is (Sun, Mon, Tue, Wed, Thu, Fri, Sat);
type Date_Type is private;
It may seem strange to put the private information in the package specification rather than in the body, but the reason for this
is that the compiler needs to know how much space to allocate when compiling programs that use the package (since the
compiler only looks at the package specification in these situations). You may be able to look at the private information in the
file containing the package specification but you arent allowed to make any use of it in your programs. The compiler will
refuse to let programs which use this package refer to anything defined in the packages private part. The only place where
you can make use of the information in the private part of a package is in the package body, or in the bodies of any child
packages (which are effectively treated as extensions to the parent package).
The great advantage of using private types is that it prevents anyone accessing the data structures directly in order to bypass
the operations that your package provides. It also ensures that if you change the information in the private part during
maintenance you can guarantee that only the package body (and the bodies of any of its child packages which use the private
information) will need changing to match, since the package body is the only place where the private part is accessible.
Package clients will need to be recompiled, but as long as there havent been any changes to the visible part of the package
specification (or the behaviour it implies) you can guarantee that no changes to the client code will be needed.
Of course, it is now impossible for clients of the package to do anything with dates except assign them to each other, test
them for equality, test that they are valid and find out what day of the week they fall on. Since theres no access to the
components of a date its impossible to construct a date with a particular value or find out what day, month or year it is. What
we need to do is to provide some extra functions to give clients these capabilities without revealing anything more about the
internal structure of Date_Type. First we need a set of accessor functions to access the components of a date:
The constructor function is the only way to create dates, so we dont need to worry about possibilities like the month of a date
being changed independently of the rest of it. This makes using dates much safer. In fact, since we can check dates for
validity when they are first created by the constructor, the only place that a validity check is needed is inside the function
Date itself, which means that Valid doesnt need to be made visible to clients. Date can just raise an exception if its
parameters dont represent a valid date, so we need an exception declaration in the visible part of the package:
This is much more like a built-in type like Integer which doesnt rely on users calling a Valid function to check that Integer
values are correct; instead, automatic error checking is done behind the scenes and an exception is raised whenever an error is
detected. By adopting the same approach for Date_Type we start to make it look much more like a built-in type.
After these changes, the package specification so far looks like this:
package JE.Dates is
-- Accessor functions
function Day (Date : Date_Type) return Day_Type;
function Month (Date : Date_Type) return Month_Type;
function Year (Date : Date_Type) return Year_Type;
function Weekday (Date : Date_Type) return Weekday_Type;
-- Constructor function
function Date (Day : Day_Type;
Month : Month_Type;
Year : Year_Type) return Date_Type;
Notice that Weekday is in fact just another accessor function, although it has to do a bit more work than the other ones.
You can also define a child package or child subprogram to be private, in which case the entire package is private:
The only place a private child can be accessed is from the body of its parent package or from the body of another child of the
parent package. You cant access it from the specification of a non-private package at all. You can think of a private child as
an extension of the private part of the parent package; its completely inaccessible to external clients. Private children can be
useful for providing operations which will be shared by the implementation of different children of a parent package.
The basic rule in Ada is that the partial view should never provide more capabilities than the full view allows. Limited types
(see chapter 6) provide an excellent illustration of this. A type which is declared as limited in the private part of the package
(the full view) must be declared limited private in the visible part (the partial view):
package Some_Package is
type Some_Type is limited private;
...
private
type Some_Type is limited record ... end record;
end Some_Package;
If you were allowed to declare Some_Type as private in the visible part of this package, this would mean that assignment
would not be allowed if you had access to the full view but would be allowed if you only had access to the partial view, since
non-limited private types allow assignment. However, you can declare a limited private type in the partial view which is non-
limited in the full view, since the declaration in the visible part only restricts access in the partial view:
package Some_Package is
type Some_Type is limited private; -- assignment not allowed
...
private
type Some_Type is record ... end record; -- assignment allowed
end Some_Package;
Package clients which only have access to the partial view cannot perform assignment or equality testing since they see
Some_Type as a limited type, but anywhere which has access to the full view is allowed to do these things.
The idea of different parts of a program having different views of the same type is an important principle which makes the
type restrictions associated with private types much easier to understand; you will meet it again in connection with generic
type parameters and tagged types.
package JE.Dates is
type Date_Type is private;
Invalid_Date : constant Date_Type; -- deferred constant
...
private
type Date_Type is
record
Day : Day_Type;
Month : Month_Type;
Year : Year_Type;
end record;
The deferred declaration of Invalid_Date tells clients that there is a constant called Invalid_Date and that its a Date_Type,
but it does this without revealing anything about the internal structure of Date_Type. The full declaration of Invalid_Date is
given in the private part of the package after the full declaration of Date_Type has been given. This is the only situation in
Ada where you are allowed to omit the value of a constant in a constant declaration, and the omission can only be a
temporary one; the full declaration must be given in the private part of the package.
The function Valid will still be needed in the package body even if it isnt required in the specification since Date will need
to use it to check if a date is valid. Valid can be made into a procedure which Ill call Validate; all it needs to do is to raise a
Date_Error if its parameters dont form a valid date. It can be made much simpler than the Valid function from chapter 4
thanks to the fact that Day_Type, Month_Type and Year_Type ensure that the parameters to Date are at least in range, so the
only thing it needs to check is that the value of Day isnt higher than the maximum allowed by Month and Year. It can also
be amended to take a Date_Type parameter instead of three separate parameters. Here is the definition of Validate, which
should be inserted at the start of the package body above:
This just extracts the components of Date into a set of Integers, evaluates Zellers Congruence and then uses
Weekday_Type'Val to convert the result (0 to 6) into the corresponding Weekday_Type value. Thanks to the order in which
the values of Weekday_Type were declared this will automatically give the correct result (0 = Sun, 1 = Mon and so on). The
Month component is converted using Month_Type'Pos; this gives a value between 0 and 11 so the result is adjusted to the
range 1 to 12 by adding 1 to it.
As mentioned earlier, Occams razor should be used whenever you start to think of clever new things to put into a package.
The operations mentioned above all require access to the private part of the package; they could be implemented outside the
package using the accessor functions Day and Month and Year, but if the representation of Date_Type were ever changed to
a Julian date it would be far easier to add a number of days to a Julian date directly instead of laboriously converting a Julian
date into a day, month and year and then performing a much more complex process of addition. Similar arguments apply to
comparison functions; Julian dates are easier to compare than dates composed of a day, a month and a year. This means that
it is well worth providing these as part of the package itself. However, what about an operation to find when the next Monday
(or any other day) after a given date will be? This isnt worth putting in a package like this: firstly, its a very specialised
operation that few programs will need, and secondly it can be implemented by adding 1 to a date up to a maximum of seven
times (or by finding out what day of the week it is, calculating how many days it is from that day to the following Monday
and then adding that number of days to the date). Putting this rarely used and relatively simple function into the package
simply increases the bulk of the package to little effect. In general, operations like this are not worth building in as the costs
outweigh the benefits. If they do turn out to be useful you can always define them in a child package to avoid bulking out the
parent package.
Addition and subtraction could be done with functions declared like this:
Thus to find out how many days there are from Now until Christmas you could use Sub (Christmas,Now) and to find out
what the date is a week from Now you could use Add (Now,7).
Write the bodies for these functions. Note that the first version of Sub can be defined in terms of Add since
Sub(Now,N) is equivalent to Add(Now,N).
Note that the name Sub is overloaded in the two declarations above. You can use the same name for different subprograms
provided that the compiler can work out which one youre referring to. Procedures and functions can always be distinguished
since procedure calls are statements whereas function calls occur in expressions. You can also tell subprograms apart if they
have different numbers of parameters or different parameter types (as with the two versions of Sub above). Functions can be
distinguished by their return types, so you can have several functions with the same name and identical parameter types
provided that they return different types of result.
Function calls are a very awkward notation compared to the arithmetic operations for standard types such as integers; if you
were dealing with Integers instead of Date_Types you would write the above function calls as Now+7 and ChristmasNow
respectively. Using function calls is far less readable than using the equivalent operators and adds to the number of things
you have to remember about the data types youre dealing with. Overloading function names (as is done with Sub above) can
reduce the burden a little, but it is still awkward and makes user-defined types visibly different from other types.
In fact, operators in Ada are just functions with special names. Writing 2 + 2 is actually equivalent to writing "+"(2,2). The
name of the function corresponding to an operator is just the name of the operator enclosed in double quotes. Ada allows you
to write functions which overload existing operators; all you have to do is to write a function whose name is the name of the
operator enclosed in double quotes. The function must also have the correct number of parameters, which in the case of "<" is
two; in the case of the "abs" or "not" operators you would only have one parameter, and in the case of "+" or "" you can
define functions with one parameter (the unary version of the operator) or two parameters (the binary version of the operator)
or both. Also, you arent allowed to provide default values for the parameters. Operator overloading means that instead of
declaring functions called Add and Sub like the ones above, you can declare operators like this:
Overloading "+" and "" like this means that it is now possible to write expressions like Now+7 or ChristmasNow.
All the operators in Ada can be overloaded except for in, not in, and then and or else which are not normal operators. There
are also restrictions on overloading the inequality operator "/=" which Ill explain in a moment. The names of the operators
that you can overload are therefore as follows:
You cannot invent your own operators (e.g. you cannot invent a percentage operator called "%"); you are also unable to alter
the precedence of operators, so that "*" will always be applied before "+". In the case of "=", if it produces a Boolean result
then "/=" is automatically defined to produce the opposite result and you are not allowed to redefine "/=" yourself. You can
only redefine "/=" if it produces a result of some other type than Boolean; for example, if you have a fuzzy logic type like
this:
then you can define "=" to compare two values and return a Fuzzy result, but you wont automatically get a corresponding
version of "/=". Youll have to define "/=" yourself in this case.
There is nothing special or magical about overloading operators; you can define a function named after an operator whenever
you like. It doesnt have to be in a package specification, but this is normally where you want to do it, in connection with a
type which is declared in the same package. The only time you cant use a name like "+" for a function is if you want to
compile it as a free-standing library unit; overloaded operators must be enclosed within library units (packages or
subprograms) which have normal alphanumeric names.
We can also overload "<", ">", "<=" and ">=" to compare pairs of dates:
Notice that it isnt necessary to overload "=" and "/="; private types already have these operators predefined and they will
give the expected result. Given the definition of "<" above, we can now write things like this:
The version of "<" above is defined in terms of the versions of "<" predefined for Day_Type, Month_Type and Year_Type.
There is no problem with this; the compiler always knows which version of "<" you are referring to by the types of the
operands you use it with. This is exactly the same as having a number of procedures called Put with different types of
parameters; the operands on either side of an operator are its parameters, and the compiler uses these to figure out whats
happening in just the same way as it uses the type of parameter supplied for Put to figure out which version of Put youre
talking about.
begin
return not (Left < Right);
end ">=";
Since A>B means the same as B<A, we can define the ">" operator using the "<" operator defined above; similarly A<=B is
True if A>B is False and A>=B is True if A<B is False. This makes the definitions nice and easy to write, and if the
representation of Date_Type ever changes youll only ever need to change the definition of "<" in order to change all of
them.
This is seriously ugly, and in the absence of any other solution it would more or less force you to write use clauses for any
packages declaring data types with overloaded operators. One solution would be to use renaming declarations for the
operators you needed to use, like this:
In situations where there are lots of different operators, this is still extremely awkward. Fortunately theres a variant of the
use clause designed to help in exactly this situation:
This gives you access to the operators of the specified type from the specified package (Date_Type from JE.Dates in this
case) but not anything else, so with a use type clause youd be able to write this:
Note that although operator names like "+" are directly accessible thanks to the use type clause, other names like Weekday
are still not accessible and must still be fully qualified with the package name.
not doing something addition-like. It is possible to be too cute for your own good; here is an example which shows how ""
can be redefined as a way of constructing dates:
type Date_Type is
record
Day : Day_Type;
Month : Month_Type;
Year : Year_Type;
end record;
type Day_And_Month is
record
Day : Day_Type;
Month : Month_Type;
end record;
function "-" (Left : Day_Type; Right : Month_Type) return Day_And_Month is
Result : Day_And_Month := (Day => Left, Month => Right);
begin
return Result;
end "-";
This means that you can write 25Dec1995 to mean Christmas Day, 1995: 25Dec gives a Day_And_Month value
consisting of a Day component set to 25 and a Month component set to Dec; this is then combined with 1995 using the
second version of "" above to give a complete date. I do not recommend doing this! It may look nice but it is using "" to do
an operation which has no relationship at all to subtraction and this is a possible source of confusion; in another context
where Dec is an Integer, the meaning of this expression will be completely different.
package Ada.Calendar is
type Time is private;
Time_Error : exception;
private
... -- not specified by the language
end Ada.Calendar;
As you can see, this defines a private type Time which represents a date and time together with a number of supporting scalar
types. It defines a set of accessor functions (Year, Month, Day, Seconds) and a constructor function (Time_Of). There is a
deconstructor procedure called Split which splits a Time into its component parts. There are also arithmetic operators and
comparison operators and an exception Time_Error for reporting any errors that are detected by the operations in the
package. The only problem with the package is that the time of day is expressed as a number of seconds rather than as an
hour, minute and seconds, and the month is represented by an integer instead of an enumerated type. These could be provided
in a separate package, but the disadvantage of this approach is that users will end up having to use both Ada.Calendar and the
new package together. This means that the users would have to keep track of which operations belonged to which packages,
or provide use clauses for both. A simple solution is to write a wrapper package which provides the extra operations as well
as providing the existing operations by renaming. Although the result is not very pretty, its a much easier solution from the
users point of view since all the operations and data types are then provided in a single package, and its attractive (if ugly!)
from an implementation point of view since it minimises the amount of new code that needs to be written. Heres the result:
with Ada.Calendar;
package JE.Times is
subtype Time_Type is Ada.Calendar.Time;
type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct,
Nov, Dec);
subtype Day_Type is Ada.Calendar.Day_Number;
subtype Hour_Type is Integer range 0..23;
subtype Minute_Type is Integer range 0..59;
subtype Second_Type is Integer range 0..59;
end JE.Times;
Interval is a new addition to the package which constructs a Duration from a number of days, hours, minutes and seconds.
The parameters all have defaults of zero, so that you can define intervals like this:
Apart from the functions Interval, Month, Hour, Minute, Second and Time, everything in this package is just a renaming of
the corresponding parts of Ada.Calendar (with subtyping being used for type renaming). However, the renamed subprograms
have to use the original type names since the types of the parameters in a renaming declaration must be identical to those in
the original subprogram. The extra functions are easy enough to implement:
end JE.Times;
So by reusing whats already available instead of reinventing the wheel, we end up with a fully functional package in a
fraction of the time it would have otherwise taken. Theres a moral in this somewhere, Im sure!
Theres one nasty little trap in the package body which Ive carefully avoided. Since real values are converted to integers by
rounding, Day_Duration is rounded to an integer in the range 0 to 86400. We actually want a value in the range 0 to 86399,
so the result of the rounding needs to be taken modulo 86400. If Id forgotten to do this the program would crash in the last
half-second before midnight, but would work perfectly the rest of the time. Bugs like this can be quite hard to detect, since
very few people do their debugging at exactly half a second before midnight!
Ive defined the function Convert_Time to do the conversion from Day_Duration to Integer_Time. Ive had to define an
internal type called Extended_Integer_Time with a range of 0..86400 rather than 0..86399 to avoid constraint errors when
rounding from Day_Duration. The mod operator is then used to produce a result in the range 0..86399 which is then
converted to an Integer_Time result. Using a modular type for Integer_Time wouldnt help since values arent forced into
range when converting to a modular type; youd still get a constraint error in the last half-second before midnight. If Duration
were a floating point type, you could get around this problem by using the 'Truncation attribute (see Appendix C) to do the
conversion by truncation instead of rounding. Unfortunately there is no 'Truncation attribute for fixed point types, which
appears to be an oversight on the part of the language standards committee; there is no easy way to do fixed point truncation
without using an integer type with a wider range than you actually require.
Exercises
9.1 Write a package which defines an abstract data type to represent fractions like 1/2 or 2/3. A fraction consists of a
numerator (an Integer) which is divided by a denominator (a Positive). Fractions should always be stored in their lowest
possible form, so that 2/4 is always reduced to 1/2; you can do this by always dividing the numerator and denominator
by their greatest common divisor, using Euclids algorithm (see exercise 4.3) to find the greatest common divisor.
Provide a complete set of arithmetic operations between pairs of Fractions (e.g. 1/2 + 1/3) as well as between Fractions
and Integers (e.g. 1/2 + 2) and between Integers and Fractions (e.g. 2 + 1/2) as well as a function to convert a rational
number to a Float. Note that the "/" operator can be overloaded for use as a constructor, dividing an Integer by an Integer
to return a Fraction as its result.
9.2 Modify the playing card package from exercise 7.4 to use private types for cards and packs of cards. A card should have
a Boolean component called Valid with a default value of False to indicate whether a card variable contains a valid card
or not, and a pack should have a Boolean component called Initialised with a default value of False to show whether it
has been initialised. All operations on a pack of cards should check the Initialised member and initialise the pack if
necessary so that it contains a full set of 52 cards. Note that cards and packs should be limited private types so that it
isnt possible to duplicate them by assignment. Provide a Move operation which moves a valid card into an invalid
variable, leaving the destination valid and the original card invalid, so that it is impossible to create duplicate cards. It
should be an error to move a card into another card which is marked as valid, since this would allow existing cards to be
destroyed. You will also need functions to access the suit and value of a card.
9.3 One of the remaining shortcomings in the Ada type system is that if a numeric type is declared to represent physical
values such as lengths in metres, the inherited multiplication operator will multiply two lengths to produce another
length. In reality, multiplying metres by metres gives square metres. Produce a package to represent dimensioned
physical quantities by recording the dimensions of mass (in kilograms), length (in metres) and time (in seconds)
involved together with the magnitude of the quantity. For example, acceleration is measured in metres per second
squared (m s2), so the dimensions are 0 for mass, 1 for length and 2 for time. Provide a set of arithmetic operators for
dimensioned quantities. They can only be added or subtracted if the dimensions match (so you cant add density and
acceleration); they can be multiplied or divided by multiplying or dividing the magnitudes and adding or subtracting the
corresponding dimensions.
9.4 Modify the diary program from chapter 8 so that it uses the package JE.Times defined in this chapter, and modify the
Load procedure so that appointments with dates prior to todays date (without taking the time into account) are ignored.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are
distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 10:
Designing with abstract data types
We explain the behaviour of a component at any given level
in terms of interactions between subcomponents whose own
internal organization, for the moment, is taken for granted.
Richard Dawkins, The Blind Watchmaker
The next step is to identify the operations that each ADT supports. Again, a useful rule of thumb is to look at the verbs in the
specification which are used in connection with the corresponding nouns: add an appointment to the diary, list the
appointments in the diary, and so on. These describe the actions to be performed by the objects. From this you can build up a
list of the operations that each ADT must provide and write subprogram specifications to match. By now you should have an
outline of the visible part of the package specification for each of the ADTs and you can concentrate on writing the program
in terms of these ADTs and the operations they provide.
This is no longer a top-down approach; the top-down approach only works if you know where the bottom is that your
design is heading towards. By sketching out the design of the ADTs in advance you are providing yourself with a known
bottom layer to aim for, and you can then steer your top-down design towards it. In other words, you combine a top-down
approach with a bottom-up approach with the aim of getting the two to meet in the middle. The nitty-gritty implementation
details of each ADT operation can then be the subject of another iteration of the same design process, so that each ADT is
implemented in terms of further ADTs. The result will be an object-oriented design; each type of object (i.e. each ADT)
provides a set of services that other objects can use without revealing how those services are implemented, and in turn uses
the services provided by other ADTs without needing to know how they are implemented. At the bottom level the ADTs are
those provided for in the language specification: Integer, Float, Boolean, String, Ada.Exceptions.Exception_Id,
Ada.Calendar.Time and so on. As long as the set of services that an ADT provides is sufficient for the needs of its clients and
independent of any particular implementation you should be able to reimplement any of the ADTs without affecting the
overall design of the system. Defining the interaction of the ADTs is the most important part of the design process;
implementing them is a secondary concern.
With this approach, the program is responsible for tying together the model and the view. The model itself should be
completely independent of the program; this can be achieved by defining the model as an abstract data type in a package. The
view will of course be heavily dependent on the model and will be tailored to the needs of the particular program, but the
program shouldnt have any special knowledge of the internal details of the views implementation so that these can be
changed if necessary. One way to manage this would be to define a set of user interface procedures which could be compiled
separately from the main program, but a better way is to define the view in yet another package. This will give us the
freedom to hide additional implementation details in the package body (data types, procedures, etc.) which are specific to the
view and which the main program should not need to see.
Since views are program specific, the package defining the view might actually be defined inside the main program. Heres
the sort of thing you might do in the diary program:
with JE.Diaries;
procedure Diary is
package Diary_View is
type Command_Type is (Add, List, Delete, Save, Quit);
function Next_Command return Command_Type;
... -- etc.
begin
... -- body of program
end Diary;
The subprograms declared in Diary_View will all be concerned with the user interface, interacting with the user to get the
details of an appointment and then using the facilities of JE.Diaries to add the appointment to the diary.
Notice that a package can be defined inside a procedure rather than being made into a separate library unit. Both the
specification and the body must be declared at the same level, so for library packages they are both library units. Inside a
procedure they must be declared in the same declaration section.
The package specification tells us absolutely nothing about the appearance of the user interface; it just provides a list of
possible commands, a function for getting the next command, and a set of procedures to interact with the user in order to
implement those commands. The commands themselves are completely abstract; they may be characters typed at a keyboard
or items selected from a pull-down menu. All the program ever sees is values of type Command_Type.
The package body is defined as separate, so somewhere else we need to define it like this:
separate (Diary)
package body Diary_View is
...
end Diary_View;
The great advantage of this approach is that the package body can provide any internal data structures or subprograms that it
needs as well as its own initialisation code (e.g. creating a window on the screen) without the main program having to know
anything about it at all. I described in chapter 4 how a package initialisation section could be used to display copyright
notices when a program starts up; the same facility can be used to perform any other package-specific initialisation (although
you have to be careful to handle every exception that might occur, since any unhandled exceptions will abort the program
before the main procedure gets started!). The separate package body can also have its own with clauses to allow it to
reference any external packages it needs. If the user interface changes, only the procedures in the package body need to be
changed; if multiple views of the diary are needed, List_Appointments can display whatever views the user selects and
Next_Command can respond to the users interactions with any of those views without the program having to be involved in
view management. All the program ends up doing is providing the model and using the package Diary_View to get and
respond to commands from the user:
with JE.Diaries;
procedure Diary is
package Diary_View is
type Command_Type is (Add, List, Delete, Save, Quit);
function Next_Command return Command_Type;
procedure Load_Diary (Diary : in out JE.Diaries.Diary_Type);
Diary_Model : JE.Diaries.Diary_Type;
begin
begin
Diary_View.Load_Diary (Diary_Model);
exception
when JE.Diaries.Diary_Error =>
null; -- ignore errors when trying to load the diary
end;
loop
case Diary_View.Next_Command is
when Diary_View.Add =>
Diary_View.Add_Appointment (Diary_Model);
when Diary_View.List =>
Diary_View.List_Appointments (Diary_Model);
when Diary_View.Delete =>
Diary_View.Delete_Appointment (Diary_Model);
when Diary_View.Save =>
Diary_View.Save_Diary (Diary_Model);
when Diary_View.Quit =>
exit;
end case;
end loop;
end Diary;
The program tries to load the diary, and then processes commands one after another. Ive assumed an exception called
Diary_Error will be reported if anything goes wrong during loading so that errors encountered when loading the diary can be
ignored by the exception handler. Each command is directed to the appropriate operation in Diary_View except for Quit,
which just breaks out of the main loop to end the program.
should be private, but we probably do want to be able to copy appointments so it wont need to be a limited type.
Next we need to identify the operations that a diary should provide. We can do this by looking at the specification for the
original problem:
It will need to provide as a minimum the ability to add new appointments, delete existing appointments, display the list of
appointments and save the appointments to a file. Also, if there are any saved appointments we should read them in when the
program starts up.
(Most specifications that youll be given will hopefully be more detailed than this!) The operations are similar to those from
chapter 8; we need to be able to add appointments, delete specified appointments, extract individual appointments in order to
list them, save the diary to a file and load it from a file.
with JE.Appointments;
use JE.Appointments;
package JE.Diaries is
Diary_Error : exception;
private
... -- it's a secret!
end JE.Diaries;
Load and Save both take a diary and a string as their parameters; the diary will be loaded from or saved to the file named by
the Name parameter. Add takes a diary and an appointment as its parameters and adds the appointment to the diary. Delete
takes a diary and an appointment number as its parameters and deletes the specified appointment; Choose also takes a diary
and an appointment number as its parameters and returns a copy of the selected appointment. Earlier I assumed the existence
of an exception called Diary_Error which would be reported if anything went wrong when loading the diary, which is
declared here. It can also be used for reporting errors arising from other operations. In the case of Add, the diary might be
full, and in the case of Delete and Choose the appointment number might be out of range, so Diary_Error can be used to
report these errors. An extra function to return the number of appointments in the diary would also be useful, since this will
allow clients to test if an appointment number is valid before calling Delete or Choose:
The chances are that youll overlook a few minor details like this in the early stages of any design and then discover the need
for them as you get more involved in the implementation. This is normal; dont worry about it. As you get more and more
experienced youll start spotting these things earlier, but in the meantime theres nothing wrong with having to go back and
make a few minor changes occasionally.
Appointment_Type needs to be dealt with next. An appointment consists of a date (day, month, year, hour and minute) and a
description; we will need to be able to extract these via accessor functions and construct an appointment from its components
with a constructor function so that values can be transferred to and from the user interface. The package JE.Times from the
previous chapter provides Time_Type which can be used for the date, and the description will just be a String.
Heres an outline for JE.Appointments which shows the specifications for the accessor and constructor functions that
Appointment_Type requires:
with JE.Times;
use JE.Times;
package JE.Appointments is
-- Accessor functions
function Date (Appt : Appointment_Type) return Time_Type;
function Details (Appt : Appointment_Type) return String;
-- Constructor
function Appointment (Date : Time_Type;
Details : String) return Appointment_Type;
private
... -- it's a secret!
end JE.Appointments;
Lets consider each of these in turn. Next_Command will be responsible for displaying a menu and getting the users
response. Most of this code can be taken from the program in chapter 8:
-- get a key
Get (Command);
Skip_Line;
end loop;
exception
when End_Error => -- quit if end-of-file character entered
return Quit;
end Next_Command;
The function displays the menu, gets the users choice and then returns the appropriate Command_Type value for the choice.
If the user enters an incorrect character an error message is displayed before looping back to display the menu again. End-of-
file errors are handled by treating them as Quit commands.
Listing the appointments is also done in much the same way as before, except that the procedure Choose must be used to get
the appointment; since Diary_Type is private, we cant just access it as an array:
A version of Put for Appointment_Type values will be needed within Diary_View. This will be a private operation hidden
inside the package body; the implementation of it could be based on the version which was given in chapter 8.
Add_Appointment just needs to get the date, time and details of an appointment from the user and then use the Add
procedure from JE.Diaries to add the new appointment to the diary:
Day : JE.Times.Day_Type;
Month : JE.Times.Month_Type;
Year : JE.Times.Year_Type;
Hour : JE.Times.Hour_Type;
Minute : JE.Times.Minute_Type;
Details : String (1..50);
Length : Natural;
Separator : Character;
begin
Put ("Enter date: ");
Get (Day);
Get (Separator);
Get (Month);
Get (Separator);
Get (Year);
Skip_Line;
Put ("Enter time: ");
Get (Hour);
Get (Separator);
Get (Minute);
Skip_Line;
Put ("Description: ");
Get_Line (Details, Length);
JE.Diaries.Add
( Diary,
JE.Appointments.Appointment
( JE.Times.Time (Day, Month, Year, Hour, Minute),
Details(1..Length) )
);
exception
when Data_Error | Constraint_Error | JE.Times.Time_Error =>
Put_Line ("Invalid input.");
end Add_Appointment;
A single separator character is read between each component of the date and time; this allows the user to enter any separator
rather than requiring the components to be separated by spaces (e.g. 25-Dec-1995 for a date or 10.15 for a time).
Try using a colon as the separator (e.g. 10:15; try 2:30 as well). This wont work since the colon is a synonym for #
(see sections 2.4.2 and J.2 of the Language Reference Manual), and Get will think its dealing with a based number
like 10#15# as described in chapter 5. Try and think of a way of solving this problem.
The appointment details will be read into a string which is defined arbitrarily as being 50 characters long; this is done without
reference to the diary package which just uses String as the type for the appointment details without revealing what the
maximum length it allows is. The length of the string that the user is allowed to type in is then a property of the user interface
rather than the diary and the maximum length that an appointment can hold is a property of the diary package; the two are
kept independent of each other.
Deleting an appointment involves asking the user to enter an appointment number, checking that its valid and then calling
JE.Diaries.Delete to handle the actual deletion:
Finally, here are Load_Diary and Save_Diary. These just call JE.Diaries.Load to load the diary and JE.Diaries.Save to save
the diary:
The diary name can be defined as a constant inside the package body:
An alternative implementation might search a predefined list of places to find the file or might ask the user for the filename;
it might also allow multiple files to be loaded and merged.
type Diary_Type is
limited record
Appts : Appointment_Array (1..10); -- an arbitrary size
Count : Natural := 0;
end record;
end JE.Diaries;
These declarations are the same as the ones given in chapter 8, except that the date and time are represented using
JE.Times.Time_Type and that Diary_Type doesnt use a discriminant for the number of appointments any more. The
package body for JE.Appointments will look like this in outline:
The package body for JE.Diaries needs to provide bodies for the subprograms declared in the package specification, so in
outline it will look like this:
These outlines are taken directly from the package specifications. All that remains is to implement the bodies of the
subprograms in each package. Ill deal with the appointment package first. The appointment accessors are very
straightforward; they can be implemented like this:
The constructor for appointments is marginally more complex since it needs to take into account the fact that the Details
parameter might be longer than the appointment can hold, in which case only the first part of the string should be copied into
the appointment:
end Size;
Choose is likewise an accessor for a specific appointment within the array of appointments in a diary. It needs to check that
the appointment number is valid and raise a Diary_Error exception if it isnt:
Deleting an appointment involves checking that the appointment number is valid and then moving appointments up the array
to overwrite the appointment being deleted:
Adding an appointment involves locating the correct place in the array for the new appointment, moving appointments down
the array to make room for it and then inserting the new appointment into the vacated array element. Diary_Error will need to
be raised if the diary is full:
begin
if Diary.Count = Diary.Appts'Length then
raise Diary_Error;
else
Pos := 1;
for I in 1 .. Diary.Count loop
exit when Date(Diary.Appts(I)) > Date(Appt);
Pos := Pos + 1;
end loop;
Diary.Appts(Pos+1..Diary.Count+1) := Diary.Appts(Pos..Diary.Count);
Diary.Appts(Pos) := Appt;
Diary.Count := Diary.Count + 1;
end if;
end Add;
A use type clause is needed to allow the operator ">" to be accessed directly. The package body will of course need a with
clause for JE.Times.
Rather than saving the diary as a text file (which would involve unpicking each appointment into its component parts) the
appointments can be saved in their internal form using the package Ada.Sequential_IO. This is a generic package that needs
to be instantiated for the type of data to be stored in the file; the full specification is given in Appendix B. It provides
essentially the same facilities as Ada.Text_IO except that the input and output procedures are called Read and Write instead
of Get and Put. The package body will need a with clause for Ada.Sequential_IO and an instantiation for Appointment_Type:
Save needs to try to create the output file and then to write each appointment in turn into the file. Diary.Count cant be
written to the file any more since only Appointment_Type values can be written:
Load essentially reverses the process; there is no appointment count in the file now, so it needs to check for End_Of_File to
discover when its finished reading the file. Heres how Load can be implemented:
raise Diary_Error;
end Load;
The diary size (Diary.Count) is set to zero at the very beginning of the procedure so that its guaranteed to be valid if an
exception occurs. The exception handler will handle Name_Errors by raising a Diary_Error exception so that the main
program can decide how to deal with it, in accordance with the principle that package operations shouldnt do their own error
handling.
What will happen if a file called Diary exists that wasnt created by this program? Add the necessary exception
handling code to Load to deal with this possibility. Dont forget to close the file if its been opened!
The way that the model has been separated from the view will make it fairly easy to revise for use with a graphical user
interface. In a graphical environment the commands might be selected from pull-down menus; Next_Command will just need
to return command codes whenever one of the diary handling commands is selected. Extra commands might be needed to
manage aspects of the user interface, e.g. commands to control the placement and size of windows. These commands
wouldnt affect the model, so they could be handled internally within the Diary_View package. The List command might be
redundant since in a graphical environment the appointments would probably be visible in a window at all times. This is no
problem; if this were the case, the interfaces menu wouldnt provide a List command and Next_Command would never
return List as its result. Add_Appointment would be quite easy to rewrite since it does all the necessary interaction with the
user to get the appointment details; youd just need to replace the procedure with a version which used a graphical dialog to
get the details instead. When deleting appointments, the appointment to be deleted might be selected by pointing at one of the
appointments displayed on the screen. It would still be possible for the user interface code to work out what the
corresponding appointment number was by keeping track of which appointments were visible, so the fact that the
Diary_Type abstraction identifies appointments by number shouldnt be a problem. Also, the user might be able to select
multiple appointments for deletion; Delete_Appointment would then need to incorporate a loop to get the numbers
corresponding to the selected appointments and delete them one by one.
The program now exhibits the object-oriented structure that I described at the beginning of the chapter. The program defines
the user interface and uses the services provided by the other ADTs in the design (the diary and the appointments).
Appointments rely on an ADT which provides date and time services (JE.Times), and so on. Individual ADTs can be
changed independently as long as the set of services needed by their clients is still available. However, we havent eliminated
maintenance problems completely. Maintenance requirements like the ones described will still involve a fair amount of work,
but the way that the different aspects of the program have been compartmentalised will make maintenance much easier than
it was before. Also, as well see in later chapters, there are other maintenance issues which the current design will still have
difficulties coping with.
Exercises
10.1 Modify the diary program in this chapter to allow the user to specify the name of the diary file to be used.
10.2 Modify the diary program to allow the user to open multiple diaries at the same time and switch from using one diary
to another.
10.3 Once the ability to open multiple diaries has been provided, add the ability to copy or move appointments from one
diary to another.
10.4 Once the ability to open multiple diaries has been provided, add a command which allows the user to display a merged
list of all the appointments in all the diaries that are currently open. The appointments should still be listed in order of
date and time.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are
distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 11:
Dynamic memory allocation
The memory strengthens as you lay burdens on it.
Thomas De Quincey, Confessions of an English Opium-Eater
To solve this problem we need some way of allocating extra memory as and when it is needed. Since this depends on the
dynamic behaviour of the program as it is run, this sort of memory allocation scheme is known as dynamic allocation. Ada
allows memory to be allocated dynamically like this:
New takes a free block of memory from a storage pool of available memory (often referred to as a heap) and reserves it for
use as an Appointment_Type variable. A reference to its location is then assigned to the variable X so that we then have some
way of accessing it. An initial value can be specified for the new appointment like this, assuming that the full declaration of
Appointment_Type from the previous chapter is visible:
As this example shows, an initial value is specified in parentheses and appended to the type name by an apostrophe ('); this is
actually a qualified expression as described in chapter 5.
So how do we declare X in this case? First of all, we need to define an access type (which Ill call Appointment_Access) and
then declare X to be an Appointment_Access variable:
Variables of type Appointment_Access can only be assigned references to Appointment_Type variables generated by new. In
many programming languages variables like this are referred to as pointers since they point to a dynamically allocated block
of memory, and although this is not official Ada terminology its in such widespread use that Im going to risk offending some
language purists by using the terms access value and pointer interchangeably.
Having set X to point to a dynamically allocated Appointment_Type variable, you can then use X.all to access the
appointment itself. You can then select components of the appointment in the usual way:
X.all.Time := Time(1995,Dec,25,21,00);
if Month(X.all.Time) = Jan then ...
As a convenience, when you access components of a dynamically allocated record you can just say X.Time and so on instead
of X.all.Time:
Be careful not to confuse X and X.all; X on its own is the name of the access variable, but X.all is the value that X
points to:
Assuming that X and Y point to different appointments, the first assignment will copy the contents of one appointment into the
other so that you end up with two identical appointments. In the second case X and Y will both end up pointing to the same
appointment, and the appointment that X pointed to before is now inaccessible unless theres another access variable which
points to it. After the first assignment, you can alter X.Date and it wont affect Y.Date since X and Y point to different
appointments, but after the second assignment X.Date and Y.Date both refer to the same thing, so any change to X.Date will
also be a change to Y.Date.
Apart from assigning a value generated by new to X, you can assign the special value null to X to indicate that it doesnt point
to anything (a null pointer). Access variables are automatically set to null when they are declared unless new is used to
initialise them:
Attempting to access the value that a null pointer points to will generate a constraint error. Its a good idea to check for null
first:
The solution is to build a linked list of appointments. We need to extend Appointment_Type to include an
Appointment_Access value which will be used to point to the next appointment. Then all we need is a single variable to point
to the first appointment; the first appointment then points us to the second appointment, which then points us to the third
appointment, and so on. It might also be convenient to have a variable which points to the last appointment in the list, but Ill
ignore that possibility for the moment. Here are declarations for types Appt_Type and Appt_Access to handle this:
type Appt_Type; -- 1
type Appt_Type is
record
Time : JE.Times.Time_Type;
Details : String (1..50);
Length : Natural := 0;
Next : Appt_Access; -- 3
end record;
The reason for line 1 in the code above is to resolve the circularity in the declarations of Appt_Type and Appt_Access. The
declaration of Appt_Access (line 2) refers to Appt_Type and the declaration of Appt_Type refers to Appt_Access (line 3).
Line 1 is an incomplete declaration which simply tells the compiler that Appt_Type is the name of a type of some sort so that
the name can be used on line 2 where Appt_Access is declared. If Appt_Type had any discriminants, wed have to use the
following incomplete declaration:
type Appt_Type (<>); -- (<>) means that the type has discriminants
We dont need to know anything about Appt_Type other than its name in order to declare an access type for it; typically, all
access values will occupy the same amount of memory no matter what type of data they point to so that no major burdens are
imposed on the compiler. Then, once Appt_Access is declared, we can give the full declaration of Appt_Type which includes
an Appt_Access component.
Diary_Type will now contain a pointer to the first appointment in the diary instead of an array of appointments:
type Diary_Type is
record
First : Appt_Access;
Count : Natural := 0;
end record;
Since access values reflect the precise memory location where an appointment has been created its no use saving them in a
file and expecting them to make any sense when the program is run again, since the actual locations of the appointments will
probably be quite different each time the program is run. For this reason its unwise to use Ada.Sequential_IO to store
Appt_Type values directly; a better way would be to embed the original Appointment_Type from chapter 10 (containing Time,
Details and Length) as a component in a larger record which includes the pointer to the next appointment:
type Appointment_Record;
type Appointment_Record is
record
Appt : Appointment_Type; -- see chapter 10
Next : Appointment_Access;
end record;
The Appt component can then be saved to a file and restored from it, rather than saving and restoring the whole record.
The following diagram illustrates what a list like this will look like:
The variable First points to the first appointment in the list, which consists of an appointment component Appt and a pointer
Next. Next points in turn to the second appointment, and the second appointments Next field points to the last appointment.
The last appointment in the list does not have an appointment after it, so its Next pointer is set to null to indicate this
(symbolised by a diagonal bar in the diagram). We can work through a list of this sort processing each appointment in some
way like this:
Current := Diary.First;
while Current /= null loop
Process (Current.Appt);
Current := Current.Next;
end loop;
This assumes that Current is an Appointment_Access variable used to keep track of the current position in the list. Remember,
Current.Next and Current.Appt are abbreviations for Current.all.Next and Current.all.Appt.
Inserting a new item into the list is quite easy. Given that Current points to an appointment somewhere in the list, heres how
you can insert a new item immediately after that appointment:
Line 1 creates a new appointment and stores a pointer to it in New_Item, which Ill assume was declared to be a variable of
type Appointment_Access. Line 2 sets its Next field so that the appointment after it is the same appointment that follows
Current. Currents Next pointer is then changed so that the new item is the one after the current item.
Deleting an item is just as easy; the drawback is that to delete a particular item you have to have a pointer to the item before it
(since the Next pointer of the item before needs to be altered so that it no longer points to the item being deleted). Assuming
that Current points to the item before the one you want to delete, heres what you have to do:
Current.Next := Current.Next.Next;
In other words, make the item which follows Current be the one after the one that follows it now. The one that follows it now
is thus left with nothing pointing to it, and so the memory allocated for it can now be recycled for use elsewhere. Ill explain
how this is done in a moment.
There are a couple of problems here that Ive glossed over: one of them is inserting an item at the beginning of the list (or into
an empty list) rather than after an existing item, and another is deleting the first item in the list. One way to get around these
problems is to provide extra procedures to deal with these specific situations, so that you would need two insertion procedures
and two deletion procedures. This is not particularly elegant. Another solution would be to use a value of null for the current
position to indicate that an item should be inserted before the first item or that the first item should be deleted. Heres how
insertion into a diary called Diary would be done:
Diary.First := New_Item;
end if;
type Appointment_Record is
record
Appt : Appointment_Type;
Next : Appointment_Access; -- pointer to next appointment
Prev : Appointment_Access; -- pointer to previous appointment
end record;
What you now have is a doubly linked list. There will need to be a pointer to both the first and last items in the list so that you
can start at either end and traverse the list in either direction. This arrangement makes it much easier to delete appointments. In
the case of a singly linked list you have to have a pointer to the item before the one you want to delete; in a doubly linked list
the item you want to delete points to its neighbours on each side, so this is what you have to do to delete the item that Current
points to:
Current.Prev.Next := Current.Next; -- 1
Current.Next.Prev := Current.Prev; -- 2
In other words, the item before Current is changed to point to the one after Current, and the item after Current is changed to
point to the one before Current. The items on either side of Current will therefore bypass it completely. leaving it isolated from
the list.
The next diagram illustrates what the state of the list is after the two steps above:
Inserting an appointment into the list involves setting the Prev pointers as well as the Next pointers. This is how you would
insert a new appointment in front of the one that Current points to:
This sequence of steps creates a new record (line 1) and links it to the current item and the previous one so that it will appear
between the current item and the previous one (lines 2 and 3). The Next pointer of the previous item is then set to point to the
new item (line 4), as is the Prev pointer of the current item (line 5).
Heres a diagram which illustrates what the situation is after the new item has been created by line 1:
Lines 2 and 3 link the new item to the existing list, and lines 4 and 5 then modify the existing list to include the new item.
Heres a diagram which shows you how it happens:
As with singly linked lists, you can detect the end of the list by the fact that Next contains a null pointer; you can also detect
the beginning of the list by the fact that Prev contains a null pointer.
There are several other variations on this theme; for example, its sometimes useful to have circular lists where the ends of the
list are joined together (i.e. the last item points to the first item and vice versa).
11.4 Iterators
The variable Current that Ive been using so far could be made into another component of Diary_Type so that each diary
would keep track of its own current position for insertion, deletion, etc. A better solution is to define a separate type to
represent positions within the list (often called an iterator) which points to the list itself as well as the current position. The
advantage of this approach is that you can have more than one current position in a single list, and you can save a copy of the
iterator for the current position so you can go back to it later. The only problem is when one iterator is used to delete an item
that another one is pointing to. Using the second iterator can be catastrophic since the item it points to might not exist any
more. There is no easy solution to this; one approach is to keep a count of the number of iterators that are referring to each
item, but this is quite complicated to administer and is still not foolproof. The simplest way out is to ignore the problem and
just try to avoid getting into such a situation. This is the solution Im going to adopt here.
Ive ducked the problem; see if you can devise a scheme (e.g. using an iterator count in each list item) which will be
safe when items are deleted.
type Appointment_Record;
type Appointment_Access is access Appointment_Record;
type Appointment_Record is
record
Appt : Appointment_Type;
Next : Appointment_Access;
Prev : Appointment_Access;
end record;
type List_Header is
record
First : Appointment_Access;
Last : Appointment_Access;
Count : Natural := 0;
end record;
type Diary_Type is
record
List : List_Access := new List_Header;
end record;
type Diary_Iterator is
record
List : List_Access;
Current : Appointment_Access;
end record;
The declarations above involve a delicate balancing act. A List_Header contains pointers to the first and last items in the list as
well as a count of the number of items in the list. Since access variables can only point to objects created using new, the List
pointer in Diary_Iterator cant point to a List_Header unless the List_Header is created using new. A diary is then just a
pointer to a List_Header; this is what the declarations of List_Access and Diary_Type do. When a Diary_Type object is
declared, its List component is automatically initialised to point to a newly created List_Header, and an iterator can then point
to the same List_Header object. The iterator references the list its associated with as well as the current position in the list; Ill
adopt the convention that if the current position is null it means that the iterator is past the end of the list, so that inserting an
item when the current position is null will add the item being inserted to the end of the list. When an iterator is declared, its
List component will be null which indicates that it isnt associated with a list; this will need to be checked by the subprograms
which operate on iterators.
Now well need some operations to get iterators from a diary and to manipulate them. We can define functions which return
iterators pointing to the start of the list and the end of the list (the position after the last item):
The names have been chosen to echo the names of the attributes of a discrete type to make them easy to remember. Well also
need functions to move an iterator forwards or backwards through the list; we can call them Succ and Pred to match the
attribute names. These need to check that the iterator is valid (i.e. it points to a list) and that we arent trying to go past the end
of the list in either direction. Well need an Iterator_Error exception which can be raised if anything is wrong:
Iterator_Error : exception;
Diary_Iterator is
begin
if Iterator.List = null or else Iterator.Current = null then
raise Iterator_Error;
else
return (List => Iterator.List,
Current => Iterator.Current.Next);
end if;
end Succ;
Both these functions check that Iterator.List isnt null; Succ also checks that the current position isnt null (i.e. that were not
already at the end of the list) and Pred checks that were not at the start of the list. Pred also needs to check if the current
position is null (i.e. just past the last appointment in the list) and if so set the current position to point to the last appointment.
Well also need a function to get the appointment that an iterator points to, and a function to return the length of a list:
These functions can be used to process each appointment in a diary called Diary like this:
declare
I : Diary_Iterator;
begin
I := First(Diary);
while I /= Last(Diary) loop
Process ( Value(I) );
I := Succ(I);
end loop;
end;
One way to deal with this problem is to keep a free list; that is, a list of items which have been deleted and are free to be used
again. All you need to do when you want to delete an item from a list is to detach it from the list and then attach it to the free
list instead. When you want to allocate a new item, you just take one from the free list. If the free list is empty, you just use
new in the normal way.
The problem with this approach is that your memory usage will only ever increase. If you ever need to allocate an object of a
different type using new, there may not be enough free memory available even though you have plenty of free memory on
your free list. What is needed is a way of telling the system to deallocate the memory so that it can be reused by anything that
needs it. The way to do this is to use the standard procedure Ada.Unchecked_Deallocation. As the name implies, there is no
check made that the memory is actually free and that you dont still have an access variable pointing to it; its entirely your
responsibility to ensure that once youve used Unchecked_Deallocation to get rid of something, you never try to refer to it
again. If you do the result will be unpredictable and might well crash your program.
Ada.Unchecked_Deallocation is a generic procedure, in just the same way as Ada.Text_IO.Integer_IO is a generic package.
Like Ada.Text_IO.Integer_IO, you have to instantiate it before you can use it by specifying what sort of object youre going
to delete with it. Heres how you do it:
You need to mention Ada.Unchecked_Deallocation in a with clause before you can do this. What you get out of this is a
procedure called Delete_Appt which will deallocate Appointment_Type objects which are pointed to by Appointment_Access
values. Delete_Appt takes an Appointment_Access parameter which points to the object you want to delete:
We can use this in a procedure to delete an appointment that an iterator is pointing to:
else
if Iterator.Current.Next = null then
Iterator.List.Last := Iterator.Current.Prev;
else
Iterator.Current.Next.Prev := Iterator.Current.Prev;
end if;
Appt := Iterator.Current;
Delete_Appt (Appt);
Iterator.List.Count := Iterator.List.Count - 1;
end if;
end Delete;
This procedure needs to check if the appointment being deleted is the first or last one in the list; if not, the pointers in the
adjoining appointment records are adjusted, but if it is, the lists First or Last component must be adjusted instead. Note that
the parameter to Delete_Appt is an in out parameter, but Iterator is an unmodifiable in parameter, so we have to copy
Iterator.Current into a variable Appt and use Appt as the parameter for Delete_Appt.
To round off the set of operations, we need an Insert procedure to insert a new item in front of the position that an iterator
points to:
This creates a new appointment record which points to the current position as the next appointment after it. There are two
special cases to consider. The first is when inserting at the end of the list (i.e. when the current position is null); in this case the
appointment needs to be linked to what was the last item in the list and the lists Last pointer needs adjusting to point to the
new appointment. The other is when inserting at the start of the list, in which case the lists First pointer needs updating.
with JE.Appointments;
use JE.Appointments;
package JE.Diaries is
type Diary_Type is limited private;
... -- etc.
private
type Appointment_Record;
type Appointment_Access is access Appointment_Record;
type Appointment_Record is
record
Appt : Appointment_Type;
Next : Appointment_Access;
Prev : Appointment_Access;
end record;
type List_Header is
record
First : Appointment_Access;
Last : Appointment_Access;
Count : Natural := 0;
end record;
type Diary_Iterator is
record
List : List_Access;
Current : Appointment_Access;
end record;
end JE.Diaries;
The package body will need to provide an instantiation of Ada.Unchecked_Deallocation, so well need a with clause for
Ada.Unchecked_Deallocation; the operations on iterators will also need to go into the package body:
These operations were all defined earlier. It would also be a good idea to put declarations of these operations in the private
section of the package specification rather than in the package body; this will prevent client packages from using them but it
will allow child packages to use them to navigate through the diary if necessary.
package JE.Diaries is
... -- as before
private
... -- as before
function First
(Diary : Diary_Type) return Diary_Iterator;
function Last
(Diary : Diary_Type) return Diary_Iterator;
function Succ
(Iterator : Diary_Iterator) return Diary_Iterator;
function Pred
(Iterator : Diary_Iterator) return Diary_Iterator;
function Value
(Iterator : Diary_Iterator) return Appointment_Type;
procedure Insert
(Iterator : in Diary_Iterator;
Appt : in Appointment_Type);
procedure Delete (Iterator : in Diary_Iterator);
end JE.Diaries;
end Size;
Choose is a bit more difficult. It needs to step through the list from the beginning to the requested position:
Deleting an appointment also involves stepping through the list to the correct position:
Adding an appointment involves locating the correct position for the appointment just like it did with the array
implementation. If were out of memory a Storage_Error exception will be raised, so this will need to be reported as a
Diary_Error:
exception
when Storage_Error =>
raise Diary_Error;
end Add;
Load is very similar to the way it was before, except that it has to make sure the diary is empty by deleting all the appointments
in the list rather than just by setting the Count component to zero:
exception
when Name_Error =>
raise Diary_Error;
end Load;
The diary can be assumed to be saved in date order, so each appointment can be added to the end of the list by using
Last(Diary) as the insertion position. Save is also very similar to the previous version, except that it now processes a list of
appointments rather than an array of appointments:
The diary program from the previous chapter will not need changing; the visible interface in the package hasnt been touched,
so all the facilities that the program used are still usable in exactly the same way. Only the internal implementation has been
affected.
I : aliased Integer;
You can also declare aliased array elements and record components:
Access variables which can be used with aliased objects as well as those allocated using new are declared like this:
The use of access all in the declaration of Integer_Access means that a variable of type Integer_Access is allowed to point to
aliased integers like I as well as integers created using new. Integer_Access is known as a general access type as opposed to
the pool-specific access types that youve seen so far.
You can get a pointer to an aliased variable by using the 'Access attribute:
There are some limitations placed on this for the sake of safety. The scope of any aliased Integer which IA is going to point to
must be at least as large as that of the scope of the type declaration for Integer_Access. This means that the following is illegal
in Ada:
The reason for this is that IA is assigned a pointer to I inside the inner block. At the end of the block, I ceases to exists so that
at the point where the assignment statement is executed, IA points to a non-existent variable. This is what is known as a
dangling pointer. The restriction may seem a bit severe but it guarantees that any objects that an Integer_Access variable can
point to must exist for at least as long as any Integer_Access variable. In particular, if Integer_Access is declared in a library
package, the scope of Integer_Access is the entire program so that only variables declared at library level (i.e. declared inside
a package which is compiled as a library unit) can be used with Integer_Access. You can get around this to some extent by
using generic packages as described in the next chapter, but if the restriction is still too severe you can subvert it by using the
attribute 'Unchecked_Access instead of 'Access. As the name implies, no checks on accessibility are performed and its up to
you to make sure that you dont do anything stupid:
procedure Legal_But_Stupid is
type Integer_Access is access all Integer;
IA : Integer_Access;
begin
...
declare
I : aliased Integer;
begin
IA := I'Unchecked_Access; -- dangerous!
end;
IA.all := IA.all + 1; -- it's your own fault when
end Legal_But_Stupid; -- this crashes!
Using 'Unchecked_Access is not recommended unless you are completely sure you know what youre doing!
General access variables must be set to point to variables since they can be used to assign a new value to the object they point
to; if you want to point to constants as well as variables you must use access constant instead of access all in the type
declaration:
A Constant_Integer_Access variable cant be used to alter the object it points to, whether that object is a constant or a variable.
One use for this is to create arrays of strings of different lengths. If you want an array of strings to hold the names of the days
of the week the individual strings must all be the same size:
However, you can have an array of pointers to strings instead, which allows the individual strings to have different lengths:
You are also allowed to use the 'Access attribute to create pointers to subprograms:
procedure Add;
procedure List;
procedure Delete;
return Float;
function Sin (F : Float) return Float;
function Cos (F : Float) return Float;
function Tan (F : Float) return Float;
Ops : constant array (1..3) of Math_Function :=
(Sin'Access, Cos'Access, Tan'Access);
The number and types of the parameters and the result type of functions must match those given in the access type declaration
(but the parameter names dont need to). Thus a Menu_Operation can point to any parameterless procedure and a
Math_Function can point to any function with a single Float parameter and a Float result. You can call these subprograms like
this:
Note that you have to use .all to call a parameterless subprogram, but you dont if there are any parameters; Ops(I)(F) is an
abbreviation for Ops(I).all(F).
The same scope rules apply for pointers to subprograms as for pointers to aliased objects, so that an access procedure type
declared in a library package can only point to library-level procedures (i.e. procedures compiled as library units in their own
right or procedures inside packages compiled as library units). You cant use the 'Unchecked_Access attribute with
subprograms; to get around this, you have to use generic packages as I mentioned earlier. The way this is done is described in
the next chapter.
The keyword access is used in place of in, out or in out. The actual parameter you supply when you call a subprogram with an
access parameter is any access value which points to the correct type of object:
X : Integer := F(IA);
Y : Integer := F(AI'Access);
Z : Integer := F(new Integer);
The parameters to F in the example above are all pointers to an Integer of one sort or another. Within the subprogram the
access parameter may be used to inspect or alter the object it points to. The parameter cant be a null pointer; if it is youll get a
Constraint_Error when you attempt to call the subprogram. Inside the subprogram the parameter acts like an access value
which is a constant (i.e. you cant alter the pointer itself, although you can alter what it points to) and which belongs to an
anonymous access type. Since you havent got a name for the access type you cant declare any more objects of the same type,
and any attempts to convert the value to a named access type will be checked to make sure you arent breaking the scope rules
described earlier for general access types, and a Program_Error exception will be raised if you are.
Any type with an access discriminant must be a limited type, so you cant use assignment as a way of breaking the scope rules.
When you declare an object of type T you must supply an appropriate access value of some sort for the discriminant:
X : T (Int_Access);
Y : T (Aliased_Int'Access);
Z : T (new Integer);
Again, the discriminant value cant be null and its type is anonymous so you cant declare any other objects of the same type.
Exercises
11.1 Produce a package which implements strings with no maximum size limit. This can be done by allocating space in
linked blocks of (say) 100 characters at a time, and linking extra blocks to the end of the existing allocation when more
space is needed. Define operations to convert to and from normal strings as well as the standard operations of copying,
slicing, concatenating and indexing individual characters.
11.2 Write a program which asks the user to pick an animal and then tries to identify it by asking a series of yes/no questions
(see exercise 3.3). Use a record containing a string and two pointers. If the pointers are null, the string is the name of an
animal; if not, the string is a question to ask and the pointers point to further records of the same type, one for a yes
answer and one for a no. The program should ask questions and follow the appropriate pointers until an animals name
(e.g. aardvark) is reached, at which point the question Is it an aardvark? should be asked. If the user responds no,
the program should ask for the name of the animal and a question to distinguish it from an aardvark (or whatever). The
question can be used to replace the animals name in the last node visited and two extra nodes can be created containing
the original animals name and the new name entered by the user.
11.3 Write a procedure to sort a linked list of integers into ascending order. There are lots of ways this could be done!
11.4 Write a program which counts the number of occurrences of each word typed at the keyboard, as in exercise 6.3, but use
a linked list to avoid imposing any limit on the maximum number of words which can be handled. As in exercise 6.3,
consider a word to be a sequence of up to 32 letters, and ignore case differences.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are
distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 12:
Generics
I am made all things to all men.
Corinthians I, 9:22
package JE.Lists is
type Appointment_Type is private;
type List_Type is limited private;
type List_Iterator is private;
Iterator_Error : exception;
private
... -- as in chapter 11
end JE.Lists;
Unfortunately it wouldnt be terribly useful since the lists it would define would be lists of diary appointments. We wouldnt
need lists of appointments that often, but lists of strings or integers or personnel details or playing cards could conceivably be
useful. The actual list handling would be the same no matter what type of data the list actually held, so why not define a
generic list management package which could be used to manage linked lists of any data type we happen to require?
At present, the linked list package uses Appointment_Type throughout as the type of the items to be stored in the lists. One
way to generalise the package would be to include the following subtype declaration:
This means that Item_Type is effectively a renaming of Appointment_Type. If the package is amended so that it uses
Item_Type as the type of the items to store in the list, its easy to change the package to deal with items of a different type; all
you have to do is make a copy of the package (and give it a new name), then change the declaration of Item_Type and
recompile. However, Ada provides a mechanism for defining generic packages that can give you the same effect without any
copying, editing or recompiling.
Youve already met some generic packages; the package Ada.Text_IO contains several generic packages like Integer_IO
which can be instantiated for use with any integer type and Enumeration_IO which can be instantiated for use with any
enumerated type. If you look at the declaration of Ada.Text_IO in Appendix B, youll see that Integer_IO is declared inside it
like this:
generic
type Num is range <>;
package Integer_IO is
-- subprograms with parameters of type Num
end Integer_IO;
Before you can use this package, you have to instantiate it by supplying the name of the actual type you want to use as a
parameter. The result of this is a brand new package:
What effectively happens is that the compiler creates a new package called My_Integer_IO which is identical to
Ada.Text_IO.Integer_IO except that all occurrences of the type Num have been replaced by My_Integer_Type. So where
Ada.Text_IO.Integer_IO provides a procedure Put which takes a parameter of type Num, My_Integer_IO provides a procedure
Put which takes a parameter of type My_Integer_Type instead. You can use a named parameter association when instantiating
a generic package, just as you can for the parameters in a procedure call:
This shows explicitly that the type Num in Integer_IO is to be replaced by My_Integer_Type when the package is instantiated.
The specification of the parameter Num as range <> shows that Num can be any integer type, since the reserved word range
in a type declaration indicates that the type being declared is an integer type. All the normal integer operations can be used
with type Num inside the package; the compiler will ensure that when the package is instantiated the actual type supplied as a
parameter really is an integer type so that those operations are guaranteed to be legitimate. However, the actual range of values
for Num is unspecified (as shown by the box symbol <>) so the package should be careful to avoid unwarranted assumptions.
For example, putting anything like this in the package body would be a bad idea:
If an integer type that doesnt include 0 in its range (e.g. Positive) is used to instantiate the package, the declaration above will
raise a constraint error. Attributes like Num'First and Num'Last should always be used instead of specific values for safety.
Generics are not restricted to use with packages; generic procedures and functions can also be defined. For example,
Ada.Unchecked_Deallocation (which was described in the previous chapter) is a generic procedure. In the case of a generic
package, the generic parameter list goes before the package specification but not in front of the package body; the compiler
knows about the generic parameters when its compiling the body because its already dealt with the specification. A similar
thing is done with generic procedures and functions; a specification of the procedure or function must be given which gives the
generic parameter list, and the procedure or function is then defined without repeating the generic parameter list.
generic
type Item_Type is private;
package JE.Lists is
...
end JE.Lists;
The only operations that will be allowed in the package body are those appropriate to private types, namely assignment (:=)
and testing for equality and inequality (= and /=). When the package is instantiated, any type that meets these requirements can
be supplied as the actual parameter. This includes records, arrays, integers and so on; the only types excluded are limited types.
Also, you must give a constrained type (so String would not be allowed, but a subtype of String which is constrained to a
particular length would be); if you wanted to allow unconstrained types as well as constrained types to be used to instantiate
the package, you would need to declare the generic parameter like this:
generic
type Item_Type(<>) is private;
package JE.Lists is
...
end JE.Lists;
The (<>) after the type name means that unconstrained types are allowed as well as constrained types. One effect of this is
that you would only be able to use Item_Type in ways which are allowed for unconstrained types; using Item_Type as a
procedure parameter would be allowed but declaring an uninitialised Item_Type variable wouldnt.
As you can see, the way you declare your generic type parameters puts restrictions on what operations you can perform on the
type inside the package as well as what types you can supply as parameters. Specifying the parameter as range <> allows the
package to use all the standard operations on integer types but restricts you to supplying an integer type when you instantiate
the package. Specifying the parameter as private gives you greater freedom when you instantiate the package but reduces the
range of operations that you can use inside the package itself. There are numerous ways of specifying generic parameters; the
table below gives the complete list, the last half-a-dozen of which are related to tagged types (which will be described in
chapter 14).
For example, you can use mod <> as a generic parameter, in which case you can use any modular type in your instantiation;
inside the package you can use any of the standard operations on modular types (e.g. the 'Modulus attribute).
Something that the above table doesnt show is that the generic parameter can also be specified as having discriminants, in
which case the actual type you supply for the parameter must have matching discriminants:
Also, as I mentioned above, you can specify for any generic parameter that its actual type may or may not have discriminants
by putting (<>) after the type name:
Note that in the cases of access types and derived types you must specify X in terms of another specific type (called Y in the
examples in the table above). This is so that the compiler knows what to do with the object that an X points to in the case of an
access type, and so that it knows what the parent type is (and hence what operations are available) in the case of a derived type.
Similarly, in array types you must specify the type of the individual items as well as the index subtype so that the compiler
knows what operations are allowed on the index type and the individual array elements; also you cannot use a constrained
array type for an unconstrained generic array parameter or an unconstrained array type for a constrained generic array
parameter. Typically the specific types used in access, array and derived type parameters will be other generic parameters; for
example, the standard procedure Ada.Unchecked_Deallocation (which was described in the previous chapter) is declared like
this:
generic
type Object(<>) is limited private;
Here Name is an access type defined in terms of Object, which is also a generic parameter. Thus when you instantiate
Unchecked_Deallocation you have to specify the access type that you want to deallocate as well as the type of object it points
to. The declaration of Object allows this to be any type at all, either constrained or unconstrained.
Generic packages, like any other packages, can have child packages. Child packages of generic packages must also be generic:
generic
type Other_Type is private;
package JE.Lists.Child is
...
end JE.Lists.Child;
To instantiate JE.Lists.Child you have to instantiate the parent package first. The child package is then effectively a generic
child of the instantiated parent package so that it can be instantiated like this:
If you dont need any extra generic parameters for the child package you can just leave them out, although the child package
must still be specified as being generic:
generic
-- it's generic but there are no generic parameters
package JE.Lists.Child is
...
end JE.Lists.Child;
In this case, you would first need to instantiate JE.Lists as before, and then instantiate JE.Lists.Child without supplying any
generic parameters:
Heres what the linked list package from the beginning of the chapter (including the full version of the private part, which is
taken from the previous chapter) looks like once its been modified to be a generic package:
generic
type Item_Type is private;
package JE.Lists is
type List_Type is limited private;
type List_Iterator is private;
Iterator_Error : exception;
private
type Item_Record;
type Item_Access is access Item_Record;
type Item_Record is
record
Item : Item_Type;
Next : Item_Access;
Pred : Item_Access;
end record;
type List_Header is
record
First : Item_Access;
Last : Item_Access;
Count : Natural := 0;
end record;
type List_Type is
record
List : List_Access := new List_Header;
end record;
type List_Iterator is
record
List : List_Access;
Current : Item_Access;
end record;
end JE.Lists;
The package body is exactly the same as it was before with the type names changed appropriately to match the new names
used in the specification (Item_Type instead of Appointment_Type, List_Type instead of Diary_Type, List_Iterator instead of
Diary_Iterator, Item_Access instead of Appointment_Access and so on).
Once the generic package has been compiled, using it is simply a matter of instantiating it with the item type you want to use:
The generic package doesnt need changing or recompiling at all when you do this, but all the type safety checks are still
enforced.
private
package Lists is new JE.Lists (Item_Type => Appointment_Type);
type Diary_Type is
limited record
List : Lists.List_Type;
end record;
end JE.Diaries;
The subprograms in the package body will need minor changes to use operations from the list package rather than doing things
by hand. For example, the definition of Size will need changing to use the Size operation from Lists (assuming that the
package body includes a use clause for the package Lists declared in the package specification):
The implementation of Choose involves the same sort of minor changes. Compare this version with the previous version:
Iterator : List_Iterator;
begin
if Appt not in 1 .. Size(Diary.List) then
raise Diary_Error;
else
Iterator := First(Diary.List);
for I in 2 .. Appt loop
Iterator := Succ(Iterator);
end loop;
Lists.Delete (Iterator);
end if;
end Delete;
begin
Appt_IO.Create (File, In_File, To);
while I /= Last(Diary.List) loop
Appt_IO.Write (File, Lists.Value(I));
I := Succ(I);
end loop;
Appt_IO.Close (File);
end Save;
Appointment_Type wasnt affected by any of the changes to Diary_Type, so none of the operations on Appointment_Type
need any modifications.
generic
type Item_Type is (<>);
type Index_Type is (<>);
type Array_Type is array (Index_Type range <>) of Item_Type;
procedure Generic_Sort (X : in out Array_Type);
Since Generic_Sort defines Item_Type to be a discrete type, we can use the comparison operator "<" to compare the relative
ordering of items in the array. Unfortunately this rules out using this procedure to sort an array of appointments since
Appointment_Type is a record type, not a discrete type, and there is no "<" operator defined for record types. The only way to
define Item_Type which would allow it to be used with record types is as a private type, but this prevents us from using "<" for
our comparisons since "<" is not a standard operation on private types (only assignment and tests for equality and inequality
are allowed). However, you can also specify procedures, functions or packages as generic parameters. What you need to do is
to supply a comparison function as a generic parameter:
generic
type Item_Type is private;
type Index_Type is (<>);
type Array_Type is array (Index_Type range <>) of Item_Type;
with function Compare (Left, Right : Item_Type) return Boolean;
procedure Generic_Sort (X : in out Array_Type);
Note that procedure, function and package parameters are preceded by with; if you left out with in the example above the
compiler would think that you were declaring a generic function called Compare.
Now when you instantiate it you just have to supply the name of a suitable function which has the right number and types of
parameters and the right result type:
type Character_Count is
record
Char : Character;
Count : Integer := 0;
end record;
Here Sort will sort an array of records using the function Less to decide on the order of the array items. Supplying the
comparison function as a generic parameter also has the advantage that you can supply any comparison function you like; for
example, here are two instantiations which sort an array of integers into ascending and descending order:
The first one uses "<" to compare the items so that theyll be sorted into ascending order, the second uses ">" instead so that
the ordering will be reversed.
In many cases, "<" will be the function we will want to use to do the comparisons. To avoid having to specify it as a parameter
in every instantiation, its possible to provide a default value. Heres how its done:
generic
type Item_Type is private;
type Index_Type is (<>);
type Array_Type is array (Index_Type range <>) of Item_Type;
with function "<" (Left, Right : Item_Type) return Boolean is <>;
procedure Generic_Sort (X : in out Array_Type);
The is <> at the end of the function declaration means that you dont need to provide a function for the parameter if a suitable
function already exists with the same name as the parameter (in this case "<"). This means that we can now define
Ascending_Sort like this:
So here, finally, is the sort procedure from chapter 6 generalised into a generic procedure. Remember that a separate
specification is required for the procedure which is prefixed by the generic parameter list; the procedure definition is given
separately without repeating the generic parameter list. A common mistake is to try and put the generic parameter list in front
of the procedure definition and not bother with a specification, but the compiler wont like it if you do this.
-- Procedure definition
procedure Generic_Sort (X : in out Array_Type) is
Position : Index_Type;
Value : Item_Type;
begin
for I in Index_Type'Succ(X'First)..X'Last loop
if X(I) < X(Index_Type'Pred(I)) then
Value := X(I);
for J in reverse X'First .. Index_Type'Pred(I) loop
exit when X(J) < Value;
Position := J;
end loop;
X(Index_Type'Succ(Position)..I) := X(Position..Index_Type'Pred(I));
X(Position) := Value;
end if;
end loop;
end Generic_Sort;
See what your compiler says if you leave out the specification and try to put the generic parameter list in front of the
procedure definition.
Notice how the attributes 'Succ, 'Pred, 'First and 'Last have been used throughout the procedure body to avoid making any
assumptions about the index subtype of Array_Type. You have to be very careful about this sort of thing when writing generic
code. Dont take anything for granted, and test everything with unusual types (e.g. arrays whose bounds are 100 .. 200).
Note that as well as using types and subprograms as generic parameters, you can also use packages, constants or variables; the
complete list of possibilities is shown in the following table:
In the first two cases, you can also supply a default value like this:
generic
Size : Integer := 100;
procedure Something_Or_Other;
When you instantiate this, you can omit specifying a value for Size:
You can use this to design a general purpose menu package. The idea is to create a linked list containing a menu item, a
character used to select it, and a pointer to a procedure to be executed when the menu item is selected. This reduces the amount
of work involved in displaying menus, getting responses and validating them, and selecting the action to be performed. Heres
an outline of a possible specification for the package:
with JE.Lists;
generic
package JE.Menus is
type Action_Type is access procedure;
type Menu_Type is limited private;
... -- operations on Menu_Type go here
private
type Menu_Item_Type is
record
Title : String (1..40);
Length : Natural;
Choice : Character;
Action : Action_Type;
end record;
package Menu_Lists is new JE.Lists (Menu_Item_Type);
type Menu_Type is
limited record
Menu_List : Menu_Lists.List_Type;
end record;
end JE.Menus;
This uses the private part of the package to define the types needed to support Menu_Type. Menu_Type is a limited record
because it contains a list of menu items, and lists are limited types. Menu_Item_Type declares the structure of an individual
menu item: a title to be displayed together with its length, a character used to select it and an action procedure to be called. The
package as a whole is generic even though there are no generic parameters needed; this is so that we can smuggle it into inner
scopes as described above by instantiating it at the same scope as the action procedures we want to use:
Well need operations to add new menu items to the menu and to allow the user to select menu choices:
The idea is that the Execute function will display the menu, get and validate the users choice and then call the selected
procedure. It will provide a Q (Quit) option automatically and will return True if the user doesnt select the Quit option, so that
it can be used in a loop like this:
begin
if Title'Length > Item.Title'Length then
Item.Title := Title (Title'First .. Item.Title'Length-Title'First+1);
Item.Length := Item.Title'Length;
else
Item.Title (Item.Title'First .. Title'Length-Item.Title'First+1) :=
Title;
Item.Length :=
Title'Length;
end if;
Item.Choice := Ada.Characters.Handling.To_Upper(Key);
Item.Action := Action;
Insert( Last(Menu.Menu_List), Item );
end Add;
Notice how this procedure carefully avoids assuming anything about the length or index range of the Title parameter and the
Title component of Item. It constructs the menu item from the parameters and then uses the linked list operation Insert to add
the new item to the end of the list. Since case differences should be ignored, it uses a function called To_Upper from the
package Ada.Characters.Handling (see Appendix B) which converts its parameter to upper case if its a lower case letter. The
body of JE.Menus will need a with clause for Ada.Characters.Handling so that it can be referenced from Add.
begin
loop
New_Line (3);
-- Display the menu
I := First(Menu.Menu_List);
while I /= Last(Menu.Menu_List) loop
Item := Value(I);
Put (" [");
Put (Item.Choice);
Put ("] ");
Put_Line (Item.Title(1..Item.Length));
I := Succ(I);
end loop;
I := Succ(I);
end loop;
end if;
This uses procedures from Ada.Text_IO, so the package body for JE.Menus will need with and use clauses for Ada.Text_IO.
Heres how the menu package could be used to display the menu for the electronic diary program:
Add, Delete and List would just be procedures to call the corresponding user interface procedures in the internal Diary_View
package with the appropriate parameters.
Exercises
12.1 Convert the diary package and main program from the previous chapter to use JE.Menus and JE.Lists.
12.2 Produce a generic version of the calculator program from chapter 3 as a procedure which can be instantiated to work
with any integer type and test it with some different integer types.
12.3 Write a generic procedure which will apply a function given as a generic parameter to each element of an array, so that
for example it could be used to square every value in an array of integers or convert all lower case letters in a string to
upper case.
12.4 Modify the dimensioned units package from exercise 9.3 so that the dimensions are specified by a discrete type supplied
as a generic parameter. The dimensions can be represented as an array of integers whose index subtype is the supplied
discrete type. For example, the original package used dimensions of mass, length and time; this could be handled by
instantiating the new package with an enumeration type consisting of the three values (Mass, Length, Time).
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are
distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 13:
Building a calculator
No reckoning made, but sent to my account
With all my imperfections on my head.
Shakespeare, Hamlet
Modifying the calculator to deal with this will require delaying the addition operation until the multiplication has been
performed. This is a well-known problem; algorithms to deal with this were first developed in the 1950s and refined in the
1960s. One approach is to use a data structure known as a stack. A stack is a collection with specific restrictions on how it can
be accessed; the traditional comparison is with a pile of plates. New items can be added to the top of the stack (i.e. you can put
more plates on top of the pile) and items can be removed from the top of the stack (i.e. you can remove plates from the top of
the pile). The conventional names for these operations are pushing an item onto the stack and popping an item off the top.
You can generally tell if someone is a programmer by asking what the opposite of push is; programmers say pop, everyone
else says pull! There may be a few extra operations; for example, you may be able to find out how many items the stack
contains or inspect the top item without removing it. What you cant do is add or remove items anywhere except at the top of
the stack; if you were to try with a stack of plates the result might be a stack crash! A stack is said to have a last-in first-out
(LIFO) organisation: the last item pushed onto the stack is the first one to be popped off.
Stacks are one of the most generally useful data structures around. They crop up in all sorts of situations; for example, the
compiler relies on using a stack to keep track of procedure calls. When you call a procedure, your return address (the point in
the calling procedure that you want to return to) is pushed onto a stack; returning from a procedure is simply a matter of
popping the return address off the top of the stack and going back to the place it specifies. Stacks can also be used for
The method for doing this requires two stacks, one for operands and one for operators. Whenever you see an operand, you put
it on the operand stack; when you see an operator, you compare it with the operator on top of the operator stack. Each
operation is given a priority (or precedence); multiplication has a higher priority than addition. If the operator youve just read
has a higher priority than the one on top of the stack, you just push it onto the stack. This defers dealing with high-priority
operators until youve had a chance to see what comes next. Otherwise, you remove the operator from the top of the stack,
remove the top two values from the operand stack, apply the operator to the two operands and push the result onto the operand
stack. You then repeat the process until the operator youre considering does have a higher priority than the one on top of the
operator stack. In other words, when you see a low-priority operator you first of all deal with any deferred operators on the
stack which have the same priority or higher. Finally, you push the operator youre considering onto the operator stack until
you see what comes next.
To make this work, you need to prime the operator stack with an operator which has a lower priority than any other. At the end
of the expression, operators must be removed one by one from the operator stack together with the top two operands from the
operand stack; each operator is applied to its two operands and the result is pushed onto the operand stack. When you reach the
low-priority operator on the bottom of the stack, the operand stack will contain a single value which is the result of the
expression. Heres what happens if you evaluate 2+3*4+5 using this algorithm:
The result is 19. Ive used # to represent the low-priority operator used to prime the operator stack. Operands and operators
are pushed onto their respective stacks until step 7 is reached. Here weve got + and the operator stack has * on top. So the
multiplication operator is removed from the operator stack, the top two items are removed from the operand stack (3 and 4),
the multiplication operator is applied to the two operands, and the result (12) is pushed back onto the operand stack. Now we
have a + on top of the operator stack, so step 8 repeats the process; 2+12 gives 14 which is pushed onto the operand stack.
Now the top of the operator stack is #, which has a lower priority than +, so the + finally gets pushed onto the operator
stack in step 9. At step 11 weve reached the full stop which signifies the end of the expression, so the + on top of the
operator stack is removed, the two operands 14 and 5 are removed from the operand stack, and the result (19) is pushed onto
the operand stack. The top operator on the stack is now #, so the value on top of the operand stack (19) is the final result.
used. The package only needs to use assignment to store and retrieve the items, so this is no problem.
The two basic operations to be provided are Push and Pop. These will need the stack to be operated on as a parameter; it will
need to be an in out parameter since it needs to be copied into the procedure and then modified. For convenience and
completeness we can provide a few extra operations: a function Top which returns a copy of the top item on the stack, a
function Size which returns the number of items on the stack, and a function Empty which returns a Boolean result to indicate
whether or not the stack is empty.
There are two things that can go wrong: the stack can overflow as the result of attempting to push too many items onto the
stack, or the stack can underflow as the result of attempting to access the top item of an empty stack. This means that we
need two exceptions, Stack_Overflow and Stack_Underflow. Heres the resulting package specification, with the private part
omitted for the moment:
generic
type Item_Type is private;
package JE.Stacks is
type Stack_Type is limited private;
private
-- to be dealt with later
end JE.Stacks;
Heres a modified version of chapter 3s calculator program which uses this stack package. Ive tried to keep the structural
changes to a minimum so that you can compare this with the original version more easily:
Operand_Stack : Operand_Stacks.Stack_Type;
Operator_Stack : Operator_Stacks.Stack_Type;
Operator : Character;
Operand : Integer;
procedure Apply is
Left, Right : Integer;
Operator : Character;
begin
Pop (Operand_Stack, Right);
Pop (Operand_Stack, Left);
Pop (Operator_Stack, Operator);
case Operator is
when '+' => Push (Operand_Stack, Left + Right);
when '-' => Push (Operand_Stack, Left - Right);
when '*' => Push (Operand_Stack, Left * Right);
when '/' => Push (Operand_Stack, Left / Right);
when others => raise Program_Error; -- should never happen (!)
end case;
end Apply;
loop
loop
Get (Operator);
exit when Operator /= ' ';
end loop;
if Operator = '.' then
while Top(Operator_Stack) /= '#' loop
Apply;
end loop;
Put (Top(Operand_Stack), Width => 1);
exit;
end if;
case Operator is
when '+' | '-' | '*' | '/' =>
while Prio(Operator) <= Prio(Top(Operator_Stack)) loop
Apply;
end loop;
Push (Operator_Stack, Operator);
when others =>
Put ("Invalid operator '");
Put (Operator);
Put ("'");
exit;
end case;
Get (Operand);
Push (Operand_Stack, Operand);
end loop;
New_Line;
end Calculator;
This program uses an internal procedure called Apply which pops two operands and an operator and then pushes the result of
applying the operator to the two operands; there is also an internal function called Prio which returns the priority of a selected
operator. These procedures should never be called with anything except a valid operator, but Ive written them defensively;
Murphys Law states that what can go wrong, will so rather than taking anything for granted Ive made sure that
Program_Error will be raised by Apply and Prio if they come across an illegal operator character.
loop
Look_Ahead (Ch, EOL);
if EOL then
Skip_Line; -- end of line, so go to next line
elsif Ch = ' ' then
Get (Ch); -- space, so read it and ignore it
else
exit; -- non-space, so exit loop
end if;
end loop;
if Ch in '0'..'9' then
Get (I); -- digit, so get an integer in I
else
Get (Ch); -- non-digit, so read it into Ch
end if;
Parentheses need to be dealt with in a special way. An opening parenthesis ( is given a priority of 0, just like the # which is
used to prime the stack. Any operators which follow this will have a higher priority and so will be stacked up above it. When
we get to the closing parenthesis we clear operators by applying them to their operands until the opening parenthesis is the top
item on the stack. It is then discarded, as is the closing parenthesis. Well also need to check that there are no opening
parentheses that havent been left unmatched at the end of the expression. This is what happens when 2*(3+4) is evaluated:
Heres a modified version of the program which allows for parentheses. Since Look_Ahead tells us when weve reached the
end of the input line, we can use this as the expression terminator instead of requiring a full stop. Also, an exception
Syntax_Error is used to respond to syntax errors in the input:
Operand_Stack : Operand_Stacks.Stack_Type;
Operator_Stack : Operator_Stacks.Stack_Type;
Operator : Character;
Operand : Integer;
Line_End : Boolean;
Got_Operand : Boolean := False;
procedure Apply is
Left, Right : Integer;
Operator : Character;
begin
Pop (Operand_Stack, Right);
Pop (Operand_Stack, Left);
Pop (Operator_Stack, Operator);
case Operator is
when '+' => Push (Operand_Stack, Left + Right);
when '-' => Push (Operand_Stack, Left - Right);
when '*' => Push (Operand_Stack, Left * Right);
when '/' => Push (Operand_Stack, Left / Right);
when others => raise Program_Error;
end case;
end Apply;
Syntax_Error : exception;
The main loop is executed until Look_Ahead reports the end of the line has been reached. If the character in Operator is a
digit, an integer is read into Operand and pushed onto the stack and a Boolean variable called Got_Operand will be set to
indicate that an operand has just been read. This is used to guard against two operands being entered one after the other; if this
happens an error message is displayed and a Syntax_Error is raised. The processing of the operators is as it was earlier except
that two extra cases are provided to deal with the left and right parentheses. A left parenthesis is just pushed onto the stack; a
right parenthesis applies operators from the operator stack until an operator with a priority of zero is reached. If this isnt a left
parenthesis it means that there are too many right parentheses in the expression, so an error message is displayed and a
Syntax_Error is raised.
The main loop ends when Look_Ahead reports that the end of the line has been reached. Operators are then applied to their
operands until an operator with a priority of zero is reached; if this isnt the # used to prime the stack it must be an
unmatched left parenthesis, which means that there is a right parenthesis missing somewhere.
One problem is that the program wont report operators with missing operands, so that *2 will be accepted and will
produce a Stack_Underflow. See if you can figure out how to fix this.
package JE.Stacks is
-- as before
private
Max_Items : constant := 100; -- an arbitrary maximum size for stacks
The package has a built-in arbitrary limit of 100 items per stack. The stack pointer (Top) is set to zero by default so that all
stacks will automatically start off empty. Pushing an item involves incrementing Top and storing the item at the array position
it indicates; popping an item off the stack is the reverse process, i.e. the item at position Top is retrieved and then Top is
decremented. The other functions are equally straightforward. Heres the package body:
raise Stack_Underflow;
end Top;
end JE.Stacks;
Note how Pop uses Top to get the value of the topmost stack item, including the necessary checks for stack underflow, so as to
avoid code duplication.
A linked list approach is just as simple. Heres what the private part of the specification looks like if we decide to take this
approach:
with JE.Lists;
package JE.Stacks is
-- as before
private
package Item_Lists is new JE.Lists (Item_Type);
type Stack_Type is new Item_Lists.List_Type;
end JE.Stacks;
Notice that Stack_Type is effectively just a renaming of Item_Lists.List_Type. However, since Stack_Type was declared as a
private type, you cant declare it as a subtype in the private part of the specification in order to effect a renaming; it must be
declared as a type, so Ive declared it as a derived type. The only problem with this is that Stack_Type values will need to be
explicitly converted to Item_Lists.List_Type values before operations on List_Type values can be applied:
This is awkward; another approach which leads to more readable code is to declare Stack_Type as a record containing a
List_Type component:
with JE.Lists;
package JE.Stacks is
-- as before
private
package Item_Lists is new JE.Lists (Item_Type);
type Stack_Type is
record
L : Item_Lists.List_Type;
end record;
end JE.Stacks;
This is much less complicated; using a record type like this is a trick worth remembering for future reference. Now we can use
the operations on lists defined in Lists to implement the package body:
with JE.Lists;
package body JE.Stacks is
end Empty;
end JE.Stacks;
How can we achieve this state of ultimate privacy? The answer is to use one of the features of access types that was mentioned
when I first started talking about them; if you tell the compiler the name of a type in an incomplete declaration you can define
an access type for it without the compiler needing to know anything more about the type. All access values are the same size,
no matter what type of thing they point to, so the compiler can allocate memory for access variables without needing to know
any details about what they point to. What we end up with is an opaque type whose name is mentioned but whose definition
can be hidden inside the package body:
package JE.Stacks is
-- as before
private
type Stack_Item; -- defined in package body
type Stack_Type is access Stack_Item;
end JE.Stacks;
The package body gets a little bit more complicated since a Stack_Type variable will start off set to null, so well need to
create a Stack_Item variable with new the first time an item is pushed onto it. Well also need to check for null in all the
packages procedures and functions. Heres how its done using an array:
begin
if Stack = null then
Stack := new Stack_Item;
end if;
Stack.Top := Stack.Top + 1;
Stack.Value(Stack.Top) := Item;
exception
when Storage_Error | Constraint_Error =>
raise Stack_Overflow;
end Push;
end JE.Stacks;
Push will allocate a new stack if its null; if we run out of memory to allocate the stack, a Storage_Error exception will be
raised. Like Constraint_Error, this is simply reported back to the caller as a stack overflow. Pop and Top dont need changing;
any attempt to access the top of a stack which hasnt been allocated (i.e. which is still null) will raise Constraint_Error, which
will then be turned into a Stack_Underflow exception by Top. Size and Empty are modified so that if you use either of them
with a stack that hasnt been allocated theyll behave as if the stack exists but is empty.
with JE.Lists;
package body JE.Stacks is
type Stack_Item is
record
L : Lists.List_Type;
end record;
end JE.Stacks;
This reveals another advantage of burying the list in a record rather than using a derived type; if a derived type were used, you
would need to write things like this in order to use Lists operations on a Stack_Type value:
return Lists.Value(Lists.First(Lists.List_Type(Stack.all)));
-- instead of "return Lists.Value(Lists.First(Stack.L));"
Since Stack_Type is an access type, the value that a Stack_Type object called Stack points to would have to be be accessed
using Stack.all; this would then need to be converted to List_Type before any List_Type operations could be performed on it.
Here the curly brackets { ... } indicate that what they enclose can be repeated any number of times (including zero times) and
the vertical bar | means or. The first rule is equivalent to an infinitely long rule that looks like this:
Expression = Term
| Term AddOp Term
| Term AddOp Term AddOp Term
| Term AddOp Term AddOp Term AddOp Term
... and so on
Primary is defined as being a number or an expression enclosed in parentheses. This seems slightly odd when you think about
it; Expression is defined in terms of Term which is defined in terms of Primary which is defined in terms of Expression which
is defined in terms of ... It seems completely circular, but actually it makes perfect sense. Look at how 2*(3+4) is analysed
using these rules:
An apparently circular definition like this is known as a recursive definition. It may seem like a bit of a curiosity, but recursion
turns up in all sorts of problems. Want to sort an array? Split it in two and sort each half, and then merge the sorted halves.
This may sound silly, but its the basis of a well-known technique called merge sorting which outperforms all of the obvious
ways of sorting that generally spring to peoples minds (e.g. the shuffle sort described in chapter 6). Want to generate all
possible permutations of a sequence? Take each item in turn as the start of a permutation and append all possible permutations
of the items that are left, so that the possible permutations of [1,2,3] are generated by 1 followed by all permutations of [2,3], 2
followed by all permutations of [1,3], and 3 followed by all permutations of [1,2]; this gives you [1,2,3], [1,3,2], [2,1,3],
[2,3,1], [3,1,2] and [3,2,1].
All recursion has to stop somewhere; the merge sort stops recursing when the array to be sorted contains a single item and the
permutation algorithm stops when you have to generate all possible permutations of a single item. In the case of parsing an
expression recursion only occurs when you see a left parenthesis, so its impossible to get stuck in an infinite loop. In
programming terms, recursion is where a function or procedure calls itself either directly or indirectly. Heres an example of a
recursive function to calculate factorials: factorial N is the product of 1 * 2 * 3 * ... * N (and as a special case, factorial 0 is 1),
which means that factorial N is N * factorial (N 1) for all N > 0, and 1 when N = 0:
The recursion always stops because N gets smaller every time and eventually gets down to 0 which is where the recursion
ends.
Ch : Character;
EOL : Boolean;
begin
loop
Look_Ahead (Ch, EOL);
if EOL then
return '.';
elsif Ch /= ' '
return Ch;
else
Get (Ch);
end if;
end loop;
end Next_Character;
This produces a full stop as its result when it reaches the end of the current line. The next step is to implement a function called
Expression:
This gets a Term (using another function well have to implement in a moment) and then sits in a loop accumulating more
Terms as long as it sees an adding operator. Term is very similar to Expression:
end if;
end loop;
return Value;
end Term;
A primary must be either a number or a parenthesised expression, so the case statement in this function checks for a digit or a
left parenthesis as the valid characters which can start an operand. If the character is anything other than a digit or left
parenthesis, a Syntax_Error exception gets raised. This deals with errors arising from missing operands, as in * 2 or 1 + * 2
where Primary will see an operator rather than a digit or left parenthesis.
If the character is a digit, an integer is read from the input and returned as the value of the function; if its a left parenthesis, an
expression is read and its value used as the functions return value. It is, however, necessary to check that a parenthesised
expression ends with a right parenthesis, so Next_Character is used after reading the expression to check for this. If the next
character is a right parenthesis, it is simply read in and ignored; if not, a Syntax_Error exception is raised. This deals with
errors arising from missing right parentheses.
A main program using these functions simply needs to call Expression and then check that the next character is a full stop. If it
isnt, theres a syntax error arising from a missing left parenthesis, a missing operator or an illegal character. If the character is
a right parenthesis, this indicates a missing left parenthesis, e.g. 1 + 2); if its a digit theres a missing operator, e.g. 1 2, and
if its anything else its an illegal character, e.g. 1 & 2. The functions above can be embedded in a program together with the
exception Syntax_Error; heres what the main program might look like:
Value : Integer;
begin
Put ("Enter an expression: ");
Value := Expression;
if Next_Character /= '.' then
raise Syntax_Error;
end if;
Put (Value);
New_Line;
exception
when Syntax_Error =>
Put_Line ("Syntax error!");
end Evaluate;
The functions themselves will need modifying to include separate clauses at the beginning, like this:
separate (Evaluate)
function Expression return Integer is
...
end Expression;
Compile this program and test that it works, especially the error handling. How could you improve the error reporting?
Exercises
13.1 Using a doubly linked list for a stack is overkill since items are only ever added to or removed from the front of the list.
Modify the stack package to use a simple singly linked list which is manipulated directly by the operations in the
package body, and test it using the calculator program.
13.2 Write a program to generate all possible anagrams of a word using the recursive method described in this chapter. If you
want to get a list in alphabetical order, sort the characters making up the word into ascending order before generating the
anagrams. To avoid generating duplicates, dont use the same letter twice. For example, to generate all unique anagrams
of apple in alphabetical order, sort the letters to give aelpp, then display a followed by all anagrams of elpp, e followed
by all anagrams of alpp, l followed by all anagrams of aepp, p followed by all anagrams of aelp, but dont do it again for
the second p.
13.3 The double-ended queue (or deque) is yet another data structure that is useful in many situations. A deque is a sequence
of items which allows you to add new items to either end or remove items from either end. Produce a deque package
which uses an opaque type to represent a deque and write a test program to allow you to exercise the package. Model
your package on the stack package shown in this chapter.
13.4 The data structure described in exercise 11.2 is known as a binary tree. It is a fundamentally recursive structure; the
node at the root of the tree points to two other nodes which can be considered to be the roots of two smaller subtrees
with the same structure. Modify your solution to exercise 11.2 using a recursive algorithm for searching the tree.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are
distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Part Three:
Designing extensible software
14. Tagged types
15. Polymorphism and dispatching
16. Controlled types
17. An object-oriented calculator
18. Designing a spreadsheet
19. Multitasking
20. Loose ends
This part deals with problems arising from the need to extend the existing capabilities of a
program as part of the maintenance process. The object-oriented design approach using
abstract data types which was described in the previous part simplifies many maintenance
problems where the services provided by individual objects comprising a program need to
be reimplemented to meet changing requirements such as a change in the user interface.
However, it assumes that the set of services provided by each type of object will remain
unchanged. What it doesnt address are issues arising from maintenance problems which
require an extension to the services provided by a particular type of object.
The ability to extend the capabilities of existing data types by inheritance is an essential
feature of object-oriented programming languages, and the main changes between Ada 83
and Ada 95 were in the area of support for object-oriented programming. This part shows
how the object-oriented facilities that Ada 95 provides can be used to improve the designs
of the examples from the previous part to make them easier to extend. The appointments
diary is extended so that it can handle a variety of different appointment types, including
any we may care to add in the unknown future, and the calculator is redesigned so that new
types of operators or operands can be added or the syntax of expressions can be changed.
The new improved calculator is then extended for use as a building block for a
spreadsheet, and finally the spreadsheet itself is extended to accommodate multitasking
capabilities.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the
constituent files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 14:
Tagged types
Profit comes from what is there,
usefulness from what is not there.
Lao Tsu, Tao Te Ching
This is quite a different sort of problem to the ones addressed in the preceding part; its about extensibility. Can you
extend an existing system to cope with new requirements and still maintain compatibility with whats already there
or do you have to rewrite everything? Can you produce a new version of the diary to cope with different types of
appointments so that existing appointments are left unchanged (so that you dont have to specify spurious agendas
for parties or lunch dates) while allowing new types of appointments to be integrated smoothly into whats already
there?
To do this you would seem to need some sort of foresight which will allow you to predict the things you might need
to change at some point in the future when you first design a system. This is where object-oriented programming
languages like Ada 95 come into their own. They provide mechanisms for writing programs which will still work
when confronted with undreamt-of variations on what they already do. It is yet another shift from a processor-
centric view of the world as exemplified in the first part of this book towards a more data-centric view as described
in the preceding part. Object-oriented programming languages take the data-centric view a step further: not only is
the data concealed so that its structure can be altered if necessary, but the data directs its own processing. In the
object-oriented view of the world, you do not perform operations on data items; instead, you ask the data items to
perform operations on themselves, and different data items might perform these operations in quite distinct ways. If
you want to extend the diary program to add a type of appointment, you arrange for the program to ask each
appointment to perform its own operations so that any new appointment types operations get used automatically by
the existing program.
This might not sound terribly revolutionary; after all, you saw in chapter 13 how you can build different versions of
a stack. You can always push an item of data onto a stack, but in one case the stack in question might be
implemented as an array and in another case it might be a linked list. You neither know nor care; all you know is
that when you say Push! it pushes and when you say Pop! it pops. Different stacks might be performing the same
operation in different ways; who knows, who cares, as long as it works? However, abstract data types as described
thus far only really help with changes in the implementation of existing types; they dont cater for extending a
program to cope with new data types, whereas the object-oriented techniques described in this part do.
Lets begin by considering what to do in order to extend the appointment type as it currently exists. Heres a
package specification which is nearly identical to the appointment package in chapter 10:
end JE.Appointments;
The only change is that the constructor function Appointment is now a procedure instead of a function, for reasons
which Ill explain later, and Ive added a Put procedure to display an appointment on the screen. Now lets consider
whats needed in order to produce a new appointment type for meetings which includes a room number. Other
details apart from room numbers can be added in the same way, so Ill stick to just adding a room number in order to
simplify matters. One way to add a room number would be to define a type called Meeting_Type which contains an
appointment and a room number:
type Meeting_Type is
record
Appt : Appointment_Type;
Room : Room_Type;
end record;
We can now declare a set of operations for Meeting_Type objects similar to those defined for Appointment_Type
objects:
These are the same as the operations for Appointment_Type except that Meeting (which constructs a Meeting_Type
object from its components) has an extra parameter for the room number, and there is an extra function Room to
extract the Room component of a meeting. Most of these operations are very similar to the equivalent operations for
Appointment_Type; in fact, Date and Details will be identical. As an example, heres how Meeting and Date could
be implemented:
Most of the work in these subprograms involves calling Appointment_Type operations to do the standard
appointment-related work; any extra work involving the room number is then done as an afterthought.
case Kind is
when Appointment =>
null;
when Meeting =>
Room : Room_Type;
end case;
end record;
The record declaration consists of a fixed part which applies to all Appointment_Type objects, so that all
Appointment_Type objects will have Date, Time, Details and Length components, followed by a variant part
which looks very much like a case statement. What the variant part says is that when the Kind discriminant is set to
Appointment, there will be no more components (as signified by null) but when Kind is set to Meeting you will also
have a Room component. You can then declare objects of either variety like this:
You can then process generalised Appointment_Types by using a case statement which inspects the discriminant
and decides what to do in each case:
case Appt.Kind is
when Appointment =>
null; -- do nothing for plain appointments
when Meeting =>
Put (Appt.Room); -- display Room component for meetings
end case;
New_Line;
end Put;
In this case the parameter to Put is unconstrained, which means that either variant of Appointment_Type can be
passed as a parameter to Put. Inside Put, a case statement is used to select alternative courses of action depending on
the value of the discriminant, which is what determines whether there is a Room component or not.
Using a variant record is simpler in some respects; you only have one copy of each subprogram. The disadvantage is
that you have to know in advance what type variants you are going to support. Adding a new one involves
modifying the original type declarations, and at every point where you discriminate between the variants you will
have to add extra code to deal with the new variant. This will involve modifying, recompiling and retesting
everything youve already written.
Fortunately, there is a much simpler way of defining extensible data types. A record type can be declared to be a
tagged record which allows it to be extended later. Heres what the declaration of Appointment_Type would look
like if wed defined it as a tagged record:
type Appointment_Type is
tagged record
Time : Time_Type;
Details : String (1..50);
Length : Natural := 0;
end record;
The only difference is that the declaration says tagged record instead of just plain record. However, we can now
define Meeting_Type by extending Appointment_Type like this:
This is another variation on derived types as described in chapter 5. Meeting_Type is derived from
Appointment_Type; we say that Appointment_Type is Meeting_Types parent type. What the declaration of
Meeting_Type says is that Meeting_Type is just like Appointment_Type except that it has an extra component
called Room. A Meeting_Type object called M will therefore have five components called M.Date, M.Time,
M.Details, M.Length and M.Room.
Bearing in mind that you can perform type conversions between derived types, you can convert M from a
Meeting_Type to an Appointment_Type like this:
All this does is to discard the extra components of M that were added to Appointment_Type when Meeting_Type
was defined (which is just Room in this case). To convert the other way you have to use an extension aggregate to
supply the missing components (namely, the value of Room):
M := (A with Room=>101);
This takes the value of A and adds a value of 101 for the room number to produce a Meeting_Type value.
Note that Appointment_Type must be declared in a package specification if it is to have any primitive operations;
declaring a tagged type in a procedure and then declaring some operations on it doesnt mean that those operations
are primitive operations of the type. Also, once a type like Meeting_Type is derived from Appointment_Type, you
cant declare any more primitive operations for Appointment_Type (since otherwise this would mean allowing
Meeting_Type to inherit operations which havent been declared yet):
package JE.Appointments is
type Appointment_Type is
tagged record ... end record;
The declaration of Appointment_Type is said to be frozen by the declaration of Meeting_Type; it would also be
frozen if any Appointment_Type objects were declared. Once a type is frozen, you cannot declare any more
primitive operations for it. The best way to avoid falling foul of the type-freezing rules is to declare all the primitive
operations of a type immediately after the type declaration so that its obvious what the primitive operations of each
type are.
The way inheritance works for primitive operations like X in the example above is that operations are implicitly
declared immediately after the derived type declaration which are identical to the parent types primitive operations
except that all uses of the parent types name in their specifications are effectively replaced by the name of the
derived type. In the example above, Meeting_Type inherits a primitive operation called X; it is as if X were declared
immediately after the declaration of Meeting_Type like this:
with JE.Times;
use JE.Times;
package JE.Appointments is
type Appointment_Type is tagged private;
Details : in String;
Result : out Appointment_Type);
private
type Appointment_Type is
tagged record
Time : Time_Type;
Details : String (1..50);
Length : Natural := 0;
end record;
end JE.Appointments;
Note that Appointment_Type is declared as tagged private in the visible part of the specification. Using tagged
private reveals to clients of the package that Appointment_Type is a tagged type. Of course, the full declaration in
the private part must be a tagged type as advertised so that the full view of the type has at least the same capabilities
as the partial view given in the visible part; alternatively you can just declare the type as private in the visible part
of the package if you dont want to let clients know whether the actual type is tagged or not.
We can now define Meeting_Type in a child package; this avoids having to modify the existing diary package and
so avoids having to recompile all the diary packages clients. Remember that a child package is treated as an
extension of its parent package and that the private part of the child (as well as the child package body) can use the
information in the private part of its parent. Heres a specification for the child package:
package JE.Appointments.Meetings is
subtype Room_Type is Integer range 100 .. 999;
type Meeting_Type is new Appointment_Type with private;
private
type Meeting_Type is new Appointment_Type with
record
Room : Room_Type;
end record;
end JE.Appointments.Meetings;
If Appointment_Type had simply been declared private rather than tagged private, the declaration of
Meeting_Type in the visible part would be illegal. The visible part of the child package only has access to the visible
part of its parent to ensure that the child cant reveal any private information from its parent in its visible part. As a
result, Appointment_Type has to be declared to be a tagged type in the visible part of JE.Appointments so that, in
the visible part of the package above, Meeting_Type can be declared to be derived from Appointment_Type. If
Appointment_Type wasnt visibly declared to be tagged, the visible declaration of Meeting_Type couldnt extend it.
Notice that Meeting_Type extends Appointment_Type using with private in the visible part of the package. This
lets clients of the package know that Meeting_Type is derived from Appointment_Type without providing any
information about the extra components it provides. Because of this the compiler will allow clients of the package to
use operations inherited from Appointment_Type on Meeting_Type objects. On the other hand, Meeting_Type
could simply have been declared to be private:
The disadvantage of doing this would be that clients of the package wouldnt be able to see that Meeting_Type is
related to Appointment_Type so that operations inherited from Appointment_Type wouldnt be accessible to its
clients (although they would be accessible in the package body, where the full declaration of Meeting_Type is
visible).
The functions Date, Details, etc., are inherited from Appointment_Type and can be used unchanged on
Meeting_Type objects; there are also two new primitive operations for Meeting_Type called Meeting and Room
which allow a meeting to be constructed from its components and the room number to be extracted from a
Meeting_Type object. If a further type were to be derived from Meeting_Type, it would inherit all the primitive
operations of Meeting_Type; this would mean that it would inherit the primitive operations that Meeting_Type
inherited from Appointment_Type (Date, Details, etc.) as well as the new primitive operations Meeting and Room.
Room is just an accessor function for the Room component, and Meeting just needs to create an appointment and
convert it to a Meeting_Type result with an extension aggregate:
Meeting_Type also inherits the procedure Put from Appointment_Type. What Put will do is to output a
Meeting_Type in exactly the same way as an Appointment_Type; in other words, the extra room number component
will be ignored. This is not what we want in this particular case, so we need to override the inherited version of Put
with one which can deal with the room numbers as well. This is done by declaring a procedure with exactly the same
specification as the inherited procedure (and it must be exact!):
package JE.Appointments.Meetings is
subtype Room_Type is Integer range 100 .. 999;
type Meeting_Type is new Appointment_Type with private;
private
type Meeting_Type is new Appointment_Type with
record
Room : Room_Type;
end record;
end JE.Appointments.Meetings;
Note that the rules for child packages mean that if you access JE.Appointments.Meetings in a with clause, you also
get access to the parent package JE.Appointments automatically (as well as the ultimate parent package JE, although
since this is empty it doesnt give you any extra benefits). This means that given a with clause for the package
JE.Appointments.Meetings, you dont need a separate with clause for the package JE.Appointments. However, a
use clause for JE.Appointments.Meetings allows you to refer to Meeting_Type directly but it does not let you refer
to Appointment_Type directly; youd need a separate use clause for JE.Appointments if you wanted to do this:
with JE.Appointments.Meetings;
use JE.Appointments.Meetings, JE.Appointments;
procedure X is
A : Appointment_Type; -- i.e. JE.Appointments.Appointment_Type
M : Meeting_Type; -- i.e. JE.Appointments.Meetings.Meeting_Type
begin
...
end X;
Alternatively, you could just have a use clause for JE.Appointments and then refer to Meetings.Meeting_Type since
the use clause for JE.Appointments lets you refer to JE.Appointments.Meetings simply as Meetings.
to deal with Meeting_Type objects; all you have to do is to use a type conversion to convert your Meeting_Type
object to an Appointment_Type when you call the procedure.
There is, however, a downside to all this. The derived type does not explicitly list the operations it inherits, so
without looking at the parent package (and the grandparent package, and so on) its difficult to know what the
complete set of operations for the derived type actually is. You may be very lucky and have access to automated
tools which can generate this information automatically from the relevant package specifications (using a class
browser), but in general the only reliable solution to this is to provide information about the inherited operations in
the documentation, including the use of comments inside the package specification. This requires self-discipline; the
compiler wont check what youve written so its up to you to get it right. If you dont provide this sort of
information (or if you get it wrong or miss something out) the users of the package will find it much harder to figure
out whats going on.
As an example of this, I didnt mention the procedure Appointment which was inherited from Appointment_Type.
Without reading the original package specification you wouldnt realise that there was any such procedure available.
However, the reason I didnt mention it was that it illustrates yet another danger arising from inheritance in Ada. In
many other object-oriented languages certain operations are not inheritable (particularly constructors), but in Ada,
all primitive operations are inherited. The specification of the procedure that Meeting_Type inherits will look like
this:
The result of calling this procedure will be that the Meeting_Type result will be created in exactly the same way as it
was when the result was an Appointment_Type; the room number wont have been set up. The procedure Meeting
doesnt override this because the name is different, and even if Meeting was renamed Appointment it wouldnt
override it since the parameter lists are different (Meeting has an extra parameter for the room number) so that there
would just be two procedures called Appointment. This is a serious problem because it provides a way to construct
Meeting_Type objects incorrectly. One solution is to override Appointment so that it provides a default value for the
room number; another possibility is to raise an exception if Appointment is called to create a meeting:
This is unsatisfactory because it involves declaring an unnecessary procedure which should never be called; it is also
unsatisfactory because if Appointment is accidentally called to initialise a meeting, detection of the error will happen
at run time instead of at compile time.
If a function is a primitive operation of a tagged type and it returns a result of that type, any derived types inherit
what is called an abstract operation, in other words a function for which no implementation exists. Since
Meeting_Type is an extension of Appointment_Type, its no good just returning an Appointment_Type result and
pretending its a Meeting_Type since the room number wont have been set up. The compiler will insist that you
explicitly override any inherited abstract operations:
You might be able to set the room number to some sort of default value, but the most sensible definition you could
provide for this function would be one that raises an exception as described above since youll probably need a
similar function for meetings anyway:
At least if you use a function the compiler will tell you that youve got a problem; when you have a problem like
this involving a procedure its up to you to realise that youve got a problem and to do something about it. For this
reason you should be very careful whenever youve got a type with a primitive procedure which has an out or in out
parameter of the type in question.
Another solution is to declare Appointment in a separate package so that it isnt a primitive operation of
Appointment_Type and so it wont be inherited by Meeting_Type. One way to do this is to use another package
inside Appointments:
package JE.Appointments is
type Appointment_Type is tagged private;
... -- as before
private
... -- as before
end JE.Appointments;
Now since Appointment is declared in a different package it wont be a primitive operation of Appointment_Type.
The inner package body will need to be defined within the body of the outer package, like this:
The constructor can now be called using the name JE.Appointments.Create.Appointment, or if youve chosen to
provide a use clause for JE.Appointments you can refer to it more simply as Create.Appointment.
Another way to do the same thing is to define the constructor as a child of the package JE.Appointments:
Of course, there are other cases where you want to override an existing primitive operation but you make a silly
mistake:
The parameters are now in a different order, so the procedures signature is different. The compiler will interpret this
as overloading the procedure name with a separate meaning rather than overriding the existing procedure with a new
meaning, and youll end up with two procedures called Appointment. To be safe, you should copy the parent
specification and then edit it (carefully!) so that you dont make mistakes like this when youre defining your
derived type.
The final verdict? Beware! Inheritance is powerful but potentially dangerous. There are lots of subtle traps that the
compiler cant detect. You have to be very careful what you do. All the same, you have to risk it; the alternative is to
end up trapped in a non-extensible cul-de-sac when its maintenance time.
Inheritance is often characterised as an is-a or an is-kind-of relationship between two types. By deriving type
Derived from type Parent, what youre saying is that a Derived is a kind of Parent. For example, it would make
sense to derive a type Car from another type Vehicle, since a car is a kind of vehicle. However, it would not make
sense to derive type Engine from type Car since an engine is not a type of car; it is instead a component of a car. The
relevant declarations might look something like this in Ada:
type Vehicle is
tagged record
... -- properties common to all Vehicles
end record;
The relationship between an engine and a car is a has-a or an is-part-of relationship (a car has an engine; an
engine is part of a car) rather than an is-kind-of relationship, or in other words a containment relationship (since a
car contains an engine). Identifying the correct relationship is very important; youve already seen how
complicated things can get if containment is used instead of inheritance at the beginning of this chapter. Similarly,
inappropriate use of inheritance can cause problems. Imagine that Car had a Headlight component and an operation
called Flash_Headlights, and that we derived Engine from Car by mistake. Engine would inherit Headlight and
Flash_Headlights from Car. I dont know about you, but Ive never come across an engine with headlights, still less
one that could flash them! Whats more, you could convert an engine into a car whenever you wanted to; this would
not go down well with the licensing authorities.
What about a Driver? Its obviously not a kind of Car, so there isnt an inheritance relationship between Car and
Driver; neither is a driver a component of a car, since a car can exist without a driver or can have different drivers at
different times; similarly a driver might drive different cars at different times. This shows that theres another
possible relationship between types: an association which can be characterised as an is-used-by or an is-associated-
with relationship.
In the case of vehicles, cars, engines and drivers the relationships are fairly obvious, but there are other cases that are
much less straightforward. For example, consider a point in a two-dimensional plane which is represented by its X
and Y coordinates. If you want to generalise this to a three-dimensional point represented by three coordinates (X, Y
and Z) you could use either containment (a 3D point contains a 2D point together with a Z coordinate) or inheritance
(a 3D point is a kind of 2D point with an extra Z coordinate). The main difference is that if inheritance is used it
becomes possible to convert a 3D point to a 2D point by discarding the Z coordinate. Depending on the sort of
primitive operations defined for 2D points, you might inherit inappropriate operations if a 3D point were derived
from a 2D point. For this reason I would probably prefer to use containment since it is likely that you would want to
choose which two-dimensional plane you project a three-dimensional point on to; there might also be considerations
such as the need for perspective transformations.
Another interesting case is the relationship between squares and rectangles. A square is a kind of rectangle, or is it
the other way round? A square has a width (which is also its length), but a rectangle might be considered to be an
extension of this which has an extra length component independently of its width:
type Square is
tagged record
Width : Float;
end record;
However, most people would prefer to say that a square is a special case of a rectangle rather than saying that a
rectangle is a generalisation of a square. Inheritance doesnt let us remove existing features, so this is something we
cant represent using inheritance. A good way to choose between containment and inheritance is to ask yourself if
you would want to be able to convert a rectangle to a square (or vice versa). In difficult cases like this where the two
types are obviously related in some way (e.g. for use in a drawing program which can draw squares, rectangles and
other shapes), but where the relationship isnt at all obvious, you can usually break the Gordian knot by deriving
them both from some other parent type Shape:
type Shape is
tagged record
... -- properties common to all shapes
end record;
This illustrates a useful design principle: difficult decisions about the relationship between two types can quite often
be resolved by introducing a third type which is used to encapsulate the common features of the other two types.
Exercises
14.1 Modify the linked list package JE.Lists to define List_Iterator as a tagged type. Derive a new
Sorted_List_Iterator with an overridden Insert operation which always inserts items into the list in the correct
position so that the items in the list are always in ascending order. You will need to supply a suitable
comparison operation as a generic parameter. Test that it works by modifying the diary program to use it for
inserting appointments into the diary.
14.2 Use a variant record to define a shape which can be a circle (with a radius) or a square (with a width) or a
rectangle (with a width and length). Define a function Area to return the area of a given shape. Write a test
program to create some shapes and display their areas.
14.3 Define a tagged type to represent a bank account with operations to deposit and withdraw money and to query
the balance of the account. Derive another bank account type which allows the account to be overdrawn up to
a defined limit and another one which charges a fixed fee for every withdrawal. Write a test program to test
the different account types.
14.4 Define a new appointment type which records the duration of an appointment in minutes, and modify the diary
program so that it asks for the duration of new appointments and so that it displays the start and end times of
each appointment (e.g. 10:00 10:30 for a 30 minute appointment at 10:00).
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent
files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 15:
Polymorphism and dispatching
Every kind of thinking, without exception,
is stamped with the brand of a class.
Mao Tse-tung, On Practice
type Appointment_Type is
tagged record
... -- properties common to all
appointments
end record;
procedure Put (Appt : in Appointment_Type); -- primitive operation
end record;
procedure Put (Appt : in Deadline_Type); -- override inherited Put
Notice that you can guarantee that youll be able to use Put to display any appointment, since any type derived directly or
indirectly from Appointment_Type either inherits a version of Put from its parent type or overrides the inherited version. In
either case there will be a procedure called Put with a single parameter which can be used to display the appointment.
We could create a linked list of Appointment_Types or a linked list of Meeting_Types using the linked list package from
chapter 12, but what we want is a list that can contain a mixture of appointments. It wouldnt be any good creating a list of
Appointment_Types since this would mean converting meetings and deadlines to normal appointments before they could be
stored in the list, and this would mean stripping off the extra components that they have in addition to any components that
Appointment_Type provides.
Tagged types provide an attribute called 'Class which gives a type describing an entire family of types (a class-wide type)
which can be used to solve this problem. For example, Appointment_Type'Class consists of Appointment_Type itself, together
with any types derived from Appointment_Type (in this case Meeting_Type and Deadline_Type). Any other types that we
derive from Appointment_Type or Meeting_Type or Deadline_Type will also be members of Appointment_Type'Class. This
means that a linked list of Appointment_Type'Class objects could be used to hold appointments, meetings, deadlines, or any
other type of appointment that we decide to add later:
However, Appointment_Type'Class is an unconstrained type. Different appointments will occupy different amounts of
memory, so you cant declare an Appointment_Type'Class variable since the compiler wont know how much memory to
allocate for it (unless you specify an initial value for it, in which case it is permanently constrained to be the same type of
appointment as the initial value). What you can do is to create an access type for Appointment_Type'Class:
Since Appointment_Access is a pointer to the class-wide type Appointment_Type'Class, an Appointment_Access variable can
be used to point to any kind of appointment at all:
This means that we can have a linked list of Appointment_Access objects and have each list element point to any kind of
appointment we feel like:
package Appointment_Lists is
new JE.Lists (Appointment_Access);
M and D above can point to any type of appointment at all. This means that if you get at the appointments that M and D point
to using M.all and D.all, the only thing the compiler knows is that theyre some sort of appointment. It doesnt know
whether theyre going to be meetings or deadlines (and they could be different at different times), so you cant apply any
meeting-specific or deadline-specific operations. The only things you can do with M.all and D.all are operations common to all
appointments, namely the primitive operations of Appointment_Type. If you want to do something to M.all which is unique to
Meeting_Type, you have to use a type conversion to convert M.all to a Meeting_Type:
Meeting_Specific_Operation( Meeting_Type(M.all) );
If you do this, its your responsibility to make sure that M does actually point to a meeting. If it doesnt youll get a
Constraint_Error when you try to do the type conversion. How you check what M really points to is discussed below.
A consequence of class-wide types is that you arent allowed to declare derived types at a deeper level than the parent type.
Library units are at library level, and each time you enter a subprogram (or go into a declare block) you are going one level
deeper, so that inside the main program you are one level deeper than the library level. Basically, entering any block which can
have a declaration section increases the depth, and exiting from the block decreases it again. Packages do not affect the level.
The reason for the restriction is similar to the reason for the accessibility limitations on general access types described in
chapter 11; for example, if you declared a derived type in a procedure called from the main program and the main program had
a pointer to a class-wide type, the procedure would be able to set the pointer to point to an object of the derived type. On exit
from the procedure, the pointer would be pointing to an object of a type that no longer exists, and any attempt to use this
pointer in the main program would probably be disastrous.
15.2 Dispatching
Since Appointment_Type'Class is an unconstrained type, one of the few things you can use it for is as the type for a procedure
parameter:
This lets you pass a meeting or a deadline or any other type of appointment to Display as its parameter. You could use this
procedure to process individual appointments from a list of appointments like this:
This uses an iterator I to go through each item in the list. Appointment_Lists.Value(I) extracts the current
Appointment_Access value from the list, and then .all is used to get the Appointment_Type'Class value it points to for use by
Display.
Display will need to deal with each appointment in an appropriate way. The parameter might be a meeting or a deadline or any
other type of appointment, so we need to ensure that the correct version of Put is called according to the actual type of the
parameter. Tagged types contain a tag (hence the name tagged type) which identifies their actual type, so that we can inspect
the parameters tag to find out if its actually a meeting or a deadline or whatever. One way to do this is to use the in operator
to check for membership of a particular class:
The test A in Meeting_Type'Class tests if the actual type of A belongs to Meeting_Type'Class, i.e. Meeting_Type itself or
any types derived from Meeting_Type. If so, A is converted to a Meeting_Type so that the version of Put which takes a
Meeting_Type parameter will be called to display the meeting. The same is done for deadlines, and if all else fails the version
of Put for the parent type Appointment_Type will be called.
This is not particularly satisfactory since the version of Put for meetings will be called for any type derived from
Meeting_Type. If you want to add a derived type called Urgent_Meeting_Type, the version of Put for Meeting_Type will be
called instead of the version for Urgent_Meeting_Type. One way to solve this is to get at the parameters tag directly using the
'Tag attribute:
This will check whether A is actually a Meeting_Type rather than a member of Meeting_Types family of derived types. The
tag belongs to the private type Ada.Tags.Tag; you can also get a printable form of the tag using the attribute 'External_Tag
(e.g. A'External_Tag) whose value is a String.
Using the tag to identify the type is still not a particularly satisfactory solution. Regardless of whether you decide to identify an
object using A in Meeting_Type'Class or A'Tag = Meeting_Type'Tag, youll still run into problems whenever a new type of
appointment is added to the program. Any class-wide operations like Display will need modifying whenever a new
appointment is derived from Appointment_Type, with resultant costs in recompiling and testing.
Fortunately you dont actually need to go to all this trouble. When you have a class-wide type like Appointment_Type'Class
and you call a primitive operation like Put, the tag is used to identify the correct version of the primitive operation
automatically. This automatic selection of primitive operations is known in Ada as dispatching. It only applies to class-wide
values where the actual type isnt known until run time; if the compiler can identify the actual type at compile time it knows
which version of the operation to use so no dispatching is necessary. It also only applies to primitive operations, since these are
the only ones that are guaranteed to be available for all types derived from the parent type. So for Appointment_Type'Class, a
call to any primitive operation of Appointment_Type will be a dispatching operation. This means that Display can be written
like this:
If A is actually a meeting, the version of Put for Meeting_Type will be called; if its a deadline, the version of Put for
Deadline_Type will be called. In fact, you can eliminate Display altogether:
In effect, you can think of calls to primitive operations of a class-wide object as asking the object to perform the primitive
operation on itself. How each object satisfies this request will depend on what type of object it really is. Youre asking each
appointment to display itself, and how this is done depends on the actual type of appointment involved. Primitive operations of
tagged types are polymorphic (literally many shaped); what the shape of the operation will be depends on the type of
object being operated on.
One possible complication arises if two tagged types are declared within the same package specification. What will happen if
you declare a subprogram which takes two parameters, one of each type? The subprogram would end up being a primitive
operation of both types, and the compiler would need some way of deciding which parameter to use for dispatching. For
example, consider the following (illegal) example:
type Appointment_Type is
tagged record
...
end record;
type Output_Device is
tagged record
...
end record;
The idea is to be able to have various types of output device and to have a procedure to output a given appointment to a given
device (e.g. a display, or a speech synthesiser, or whatever). Each appointment can override Put to output itself appropriately;
also each Output_Device can override Put to use the operations appropriate to a particular output device. If Put is called the
compiler wont know whether to call the overridden version for the derived Appointment_Type or the overridden version for
the derived Output_Device.
Ada adopts a brutally simple approach to resolving this problem. Subprograms which are primitive operations of two tagged
types (like Put above) are not allowed. Either you could move the operation itself out of the package specification so that it is
no longer a primitive operation of either type or you could move one of the type declarations out of the package so that the
operation is a primitive operation of only one of the two types. Alternatively, you could change the procedure specification so
that it only uses one of the two types. Heres how you could modify Put to eliminate the Output_Device parameter:
The parameter D isnt an Output_Device any more so the procedure is no longer a primitive operation of Output_Device.
However, inside Put an Output_Device'Class value will cause dispatching to occur if there is a call to a primitive operation of
Output_Device. This technique is known as double dispatching or redispatching. For example, imagine that Output_Device
has a primitive operation called Write_Output that looks like this:
A call to Put involving class-wide parameters corresponding to a meeting and a particular type of output device will result in
the call being dispatched to the version of Put above. This will output the meetings details using calls to Write_Output, each
of which will be dispatched to the correct version of Write_Output for the particular type of device.
You are still allowed to have two parameters of the same tagged type. For example, imagine a primitive operation to determine
the time between two appointments. The declaration might look like this:
This means that the inherited operations for Meeting_Type and Deadline_Type will look like this:
The reason that declarations like this are allowed is that the parameters in situations like this will always be the same type as
each other so there will never be a choice of types to dispatch the operation to. This means that if you call Interval with two
class-wide parameters, the actual types of the class-wide parameters must match. The compiler will insert a run-time check in
your program to make sure that the types do in fact match, and a Constraint_Error will be raised if you ever try to call Interval
with parameters whose actual types dont match.
type Shape is
tagged record
null; -- no components!
end record;
This happens often enough that there is a special abbreviated form of this declaration:
You can also use null records as type extensions if you dont want to add any extra components when you derive from a tagged
type:
For example, the only difference between Deadline_Type and Appointment_Type might be in the way theyre displayed; you
might want a Deadline_Type to be displayed in a way that makes it stand out on the screen (e.g. in bright red or boldface
characters).
We might want all shapes to have a primitive operation called Draw to guarantee that therell always be a way of drawing any
derived shape. Now, how can we implement Draw when Shape is a null record? The answer is that we cant; each shape will
have its own requirements, but theres no way to draw a generalised Shape without knowing what sort of shape it is. One
solution is to define Draw so that it does nothing:
An alternative would be to raise a Program_Error exception, since theres surely something wrong if we ever get into the
situation of drawing an amorphous shape.
However, theres an easy way to overcome this problem: Ada allows us to declare Shape as an abstract type (which is not the
same thing as an abstract data type!) by adding the word abstract to the declaration:
You cant create objects of an abstract type; the only reason for its existence is to act as a parent type for a family of related
types. Its main purpose is to provide a set of primitive operations such as Draw which will be inherited by all types derived
from it. Since you cant declare a Shape once its made abstract, you dont run the risk of calling Draw for an indeterminate
Shape. You have to derive concrete (non-abstract) types such as Square and Rectangle so that objects of these types can be
created. These can override Draw in an appropriate way, and the version of Draw that will be called from a class-wide
(Shape'Class) reference will then be the overridden versions defined for Square or Rectangle.
There is still one risk to be guarded against, and that is that when you derive a new type from Shape you might forget to
override Draw. As it is, you will simply end up with an invisible shape (or an exception, depending on how the Shape version
of Draw was implemented), but it would be far better to identify this sort of problem at compile time rather than leaving it until
run time. The way to do this is to declare Draw as an abstract operation:
This is used as the specification of Draw in the package specification, and it indicates to the compiler that this version of Draw
will not actually be implemented (i.e. there will be no definition of Draw in the package body). Only abstract types can have
abstract operations. When you derive a concrete type from an abstract type, the compiler requires you to override all the
abstract primitive operations, so that its impossible to forget to provide them. If you want to you can derive another abstract
type from a parent abstract type, in which case the derived type will just inherit the abstract operations from its parent:
type Coloured_Shape is abstract new Shape with record ... end record;
Coloured_Shape inherits Draw as an abstract primitive procedure, just as if youd written this:
Since youre not allowed to create objects belonging to an abstract type, functions which return a result of an abstract type
must be declared to be abstract, since you can't provide an implementation; theres no way to create an object to return as the
functions result.
Since the appointment list contains class-wide pointers, the availability of Put as a primitive operation will mean that Put can
just be called for each appointment in the list and dispatching will ensure that the correct version for the actual appointment
type will be used. However, this will mean that the model (Appointment_Type) is once again entangled with a particular view.
Derivation provides a simple solution to this difficulty. If the user interface needs to be redesigned you can just derive a new
type from Appointment_Type which overrides Put. In fact, by making Appointment_Type an abstract type and Put an abstract
operation, you can preserve the independence of the model from the view; each program which uses the Appointment_Type
abstraction will need to derive a concrete type from it to be able to use it and will be forced to provide an appropriate
implementation of Put. Since functions returning abstract types have to be abstract, the constructor has to be made into a
procedure so that an implementation can be given for it; it also has to be inherited by derived classes (with all the potentially
nasty consequences described in the previous chapter) since derived classes will need to use it to set up the Date and Details
components that they inherit from Appointment_Type. The appointment package from the previous chapter will need some
minor modifications as a result:
with JE.Times;
use JE.Times;
package JE.Appointments is
type Appointment_Type is abstract tagged private;
The diary will need changing to be a linked list of class-wide pointers. Add will need to take a class-wide pointer as its
parameter and Choose will need to return a class-wide result.
private
type Appointment_Access is access Appointment_Type'Class;
package Lists is new JE.Lists (Item_Type => Appointment_Access);
type Diary_Type is
limited record
List : Lists.List_Type;
end record;
end JE.Diaries;
Meetings can be derived from Appointment_Type as in the previous chapter, but Meeting_Type will need to be an abstract
type which inherits an abstract Put procedure:
package JE.Appointments.Meetings is
subtype Room_Type is Integer range 100 .. 999;
type Meeting_Type is abstract new Appointment_Type with private;
private
type Meeting_Type is abstract new Appointment_Type with
record
Room : Room_Type;
end record;
end JE.Appointments.Meetings;
Meeting_Type inherits the constructor procedure Appointment from Appointment_Type and also provides its own constructor
called Meeting. Meeting can be implemented using the inherited version of Appointment:
The main program will need to derive concrete appointment types from the abstract types Appointment_Type and
Meeting_Type as part of the view package. Because of the restrictions about where derived types can be declared (as described
at the beginning of this chapter), its no longer possible to declare the view package inside the main program; itll have to be
declared at library level. To do this, Ill create another empty parent package called JE.Views:
package JE.Views is
-- another empty package!
end JE.Views;
type Meeting_Type is
new JE.Appointments.Meetings.Meeting_Type with null record;
procedure Put (Appt : in Meeting_Type);
end JE.Views.Diary;
The package Ada.Streams.Stream_IO provides similar facilities to Text_IO and Sequential_IO. It also contains a function
Stream which takes a file as a parameter and returns a value suitable for use as the Stream parameter of the Input and Output
subprograms above:
The with clause at the start of the package body will need changing to refer to Ada.Streams.Stream_IO instead of
Ada.Sequential_IO. Heres what Save will look like:
Ada.Streams.Stream_IO.Close (File);
end Save;
Load just has to use Appointment_Type'Class'Input to read successive appointments of whatever type from the file:
Ada.Streams.Stream_IO.Close (File);
exception
when Ada.Streams.Stream_IO.Name_Error =>
raise Diary_Error;
end Load;
First of all the diary is emptied by repeatedly deleting the first appointment until there are none left. Then the file is opened and
the main loop reads appointments one by one, adding them to the diary using Add.
end Choose;
Delete needs to deallocate the appointments when the pointers to them are removed from the list:
The only changes to Add are the change to the type of its parameter and the use of new to create a class-wide object initialised
to hold a copy of the parameter:
Adding a new appointment will involve asking the user if its an ordinary appointment or a meeting. The date, time and details
are then read in. Finally, the appointment is created and added to the diary depending on the appointment type; if its a
meeting, the extra meeting-specific information (the room number) is read in first:
begin
-- Get appointment type
Put ("Appointment (A) or meeting (M)? ");
Get (Appt_Kind);
if Appt_Kind /= 'A' and Appt_Kind /= 'a' and
Appt_Kind /= 'M' and Appt_Kind /= 'm' then
raise Data_Error;
end if;
-- Get date
Put ("Enter date: ");
Get (Day);
Get (Separator);
Get (Month);
Get (Separator);
Get (Year);
Skip_Line;
-- Get time
Put ("Enter time: ");
Get (Hour);
Get (Separator);
Get (Minute);
Skip_Line;
-- Get description
Put ("Description: ");
Get_Line (Details, Length);
if Appt_Kind = 'M' or Appt_Kind = 'm' then
-- Get meeting-specific details and construct a meeting
declare
Room : JE.Diaries.Meetings.Room_Type;
Appt : Meeting_Type;
begin
Put ("Room number: ");
Get (Room);
Skip_Line;
Meeting (JE.Times.Time(Year,Month,Day,Hour,Minute),
Details(1..Length), Room, Appt);
JE.Diaries.Add (Diary, Appt);
end;
else
-- Construct a normal appointment
declare
Appt : Appointment_Type;
begin
Appointment (JE.Times.Time(Year,Month,Day,Hour,Minute),
Details(1..Length), Appt);
package JE.Appointments.Deadlines is
type Deadline_Type is abstract new Appointment_Type with null record;
-- Date, Details, Put and Appointment inherited unchanged
-- from Appointment_Type
end JE.Appointments.Deadlines;
The view package will need modifying to provide a new concrete type with a suitable implementation of Put:
Put will also need to be defined in the package body. It might simply display the message URGENT at the end; the rest of the
appointment can be displayed by converting the urgent appointment to an ordinary appointment so that the Appointment_Type
version of Put can be called, although its a bit fiddly since it involves converting to the abstract parent type
Appointment_Type and then using an aggregate extension to convert this to the derived concrete Appointment_Type:
Note that even when a derived type adds no extra data components to its parent type, an extension aggregate must still be used
to convert from the parent type to the derived type, but the extension to the parent type is specified as with null record. We
need to use a qualified expression to tell the compiler which type we expect the extension aggregate to be, since there might be
any number of derived types like Deadline_Type which have a null extension and an inherited version of Put, and it would
otherwise be ambiguous.
Add_Appointment will need modifying to ask if the appointment is a deadline or not and to create and initialise deadlines
when necessary:
... -- as before
begin
Put ("Appointment (A), meeting (M) or deadline (D)? ");
... -- as before
if Appt_Kind = 'M' or Appt_Kind = 'm' then
... -- as before
elsif Appt_Kind = 'D' or Appt_Kind = 'd' then
declare
Appt : Deadline_Type;
begin
Appointment (JE.Times.Time(Year,Month,Day,Hour,Minute),
Details(1..Length), Appt);
JE.Diaries.Add (Diary, Appt);
end;
else
... -- as before
end if;
exception
when Data_Error | Constraint_Error | JE.Diaries.Diary_Error =>
Put_Line ("Invalid input.");
end Add_Appointment;
The diary package will not need changing; a Deadline_Type will be a member of Appointment_Type'Class so it will be able to
be stored in the diary along with the other appointments. Calls to Put will be dispatched to the overridden version of Put
defined for Deadline_Type, so no modifications will be needed to the operations in the diary package itself. The only changes
will be the introduction of the new child package JE.Appointments.Deadlines and the modifications to the view package
described above. None of the existing code in the other packages will need changing; these packages wont even need to be
recompiled. Youll need to compile the new child package and the modified view package, recompile the main program
(because JE.Views.Diarys specification has been changed) and then test the changes that youve made. This is an enormous
maintenance saving compared with any non-object-oriented solution.
Exercises
15.1 Write a program which allows you to create any of the bank accounts from exercise 14.3 and then lets you deposit and
withdraw money and inspect the balance, regardless of which account type you created.
15.2 Modify the diary program to incorporate an appointment type which records the duration of an appointment in minutes,
as in exercise 14.4.
15.3 Define a tagged type to represent the details of a publication (title, author and year of publication) which could be used
to display an entry in a bibliography. Derive specialised publication types to represent journal articles (with journal name
and volume and issue numbers), books (with publishers name) and articles in collections (with the details of the book
containing the article: title, editor, publisher and so on). Write a program similar to the appointments diary example in
this chapter which uses this to implement a bibliographic database. You should be able to add the details of a publication
to the database, display the contents of the database in alphabetical order of author, delete publications, and save the
database to a file.
15.4 A chessboard consists of an 88 grid of squares, each of which can be empty or can hold a piece. Each piece is coloured
white or black. Different pieces can move in different ways: for example, a rook can move along either the row or the
column it is on, whereas a bishop can move along either of the two diagonals through the square it is on. Rooks and
bishops cannot move through other pieces; they must stop on the square before a piece of the same colour or on the same
square as a piece of the opposite colour (in which case the opposing piece is captured and removed from the board).
Define a tagged type to represent a chess piece with a primitive operation which takes a chessboard and a position on the
board as its parameters and returns a linked list of board positions that the piece can move to, then derive types from this
to represent rooks and bishops. Write a program which will read the positions of a set of rooks and bishops on a
chessboard and generate a list of all legal moves for each piece.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are
distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 16:
Controlled types
A retentive memory is a good thing, but the
ability to forget is the true token of greatness.
Elbert Hubbard, Epigrams
Some systems (but not all) use a garbage collector as a way of solving this problem. At some point (e.g. when you try to
allocate some memory) the garbage collector scans the heap looking for blocks of memory that are no longer accessible and
reclaims them for recycling. Unfortunately, since not all systems use garbage collection, you cant rely on it being there to
tidy up after you. The only time that Ada guarantees to reclaim inaccessible objects is at the point where the declaration of
the access type itself goes out of scope, since by then there wont be any access variables left which might point to the
objects. In the case of a generic package the access type is effectively declared at the point where you instantiate the package
so this isnt as bad as it might sound. The only other alternative is to use Unchecked_Deallocation as described in chapter 11.
As the name implies, this is done at your own risk; if you deallocate something and you still have an access variable which
points to it (a dangling pointer) you run the risk of crashing your system. Using a dangling pointer to access something that
isnt there any more can make a real mess of things. However, in the case of a linked list you can usually make sure that this
doesnt happen.
So to guarantee that your linked lists dont cause memory leaks you will need to provide a procedure which will use
Unchecked_Deallocation to clean up the linked list (possibly called Close by analogy with closing a file):
declare
X : List_Type;
begin
Process (X); -- use the list X
Close (X); -- free memory allocated to X
end;
The disadvantage of doing this is that it puts an extra burden on the users of your linked list package; they will have to
remember to call Close at the end of every block where a list is declared, and if they forget they will end up with memory
leaks.
You can derive new controlled types by deriving them from Controlled or Limited_Controlled. Controlled types inherit a
primitive operation called Finalize from their parent type:
Finalize is unusual in that it is automatically called when a controlled object goes out of scope (i.e. at the end of the block
where it is declared). Finalize is therefore like the Close procedure described above except that you dont need to call it
explicitly; the compiler will automatically provide a call to Finalize at the end of the block. Finalize can be used for any last-
minute cleaning up (finalisation) that needs to be done; this often involves deallocating memory, but you could use it to save
the object automatically to disk, add an entry to a log file, clear the screen, play a tune, or do anything else you might think is
appropriate. The versions of Finalize that you inherit from Controlled and Limited_Controlled do nothing, so that you dont
have to override Finalize for your derived type if you dont need to do any finalisation operations.
British readers should be careful not to use the normal British spelling (Finalise instead of Finalize) when attempting to
override operations of controlled types. If you declared a procedure called Finalise, you'd end up with a brand new primitive
operation called Finalise in addition to the inherited version of Finalize, and Finalise would not be called automatically. A
good compiler should warn you about this!
In the case of a linked list you can define Finalize to deallocate all the items in a list automatically at the end of its scope.
Heres how the linked list package from chapter 12 can be modified to allow this:
private
... -- as before
type List_Type is new Limited_Controlled with
record
Header : List_Access := new List_Header;
end record;
List_Type is automatically a limited type because it is derived from the limited type Limited_Controlled. Finalize is declared
in the private part of the package; this makes it a primitive operation of List_Type (so that it overrides the do-nothing
version inherited from Limited_Controlled) but its invisible to package clients. Child packages can still access it; this means
that if a further type derived from List_Type wants to override Finalize, it can call List_Types version of Finalize as part of
its own finalisation code. Note that if the visible part of the package revealed that List_Type were derived from
Limited_Controlled, package clients could call Finalize directly since Finalize is visibly known to be a primitive operation of
Limited_Controlled types and so would be known to be inherited by List_Type.
Since Ada.Finalization.Limited_Controlled and Ada.Finalization.Controlled are both library-level types, all controlled types
must be derived by instantiation at library level. Since generic packages are treated as being declared at the point of
instantiation, this means that JE.Lists can only be instantiated at library level, usually within the specification of another
library package. In particular, this means that we can no longer use JE.Lists to build opaque types as described in chapter 13;
instantiating JE.Lists inside a package body is no longer permissible. Moving the instantiation into the package specification
solves the problem, although this means that the type is no longer opaque since implementation information has to go in the
package specification. Another solution is to have a non-generic definition for the list types, and then use inheritance to
derive an extended type for the list nodes which includes the data to be stored in the list, although getting this right isnt
trivial either.
The definition of Finalize for a linked list might look like this:
This loops until the list is empty, removing and deleting the first element of the list each time around the loop, and then
deletes the list header.
As well as Finalize, controlled types have another primitive operation called Initialize. Initialize is declared in exactly the
same way as Finalize is:
Like Finalize, the default behaviour is to do nothing. If you override Initialize for a derived controlled type, it is called
automatically whenever an object of that type is created that has no initial value specified. If an initial value is specified in
the object declaration, Initialize doesnt get called.
The Finalize operation for Pointer_Type can be used to delete the pointer it contains. Well also need an accessor function to
let us get at the pointer inside the Pointer_Type object:
This is best done in a generic package so that we can instantiate it for use with any access type:
generic
type Item_Type(<>) is limited private;
type Access_Type is access Item_Type;
package JE.Pointers is
...
end JE.Pointers;
Note that Item_Type is defined to be an unconstrained limited private type; this means that it can be instantiated using
absolutely any type at all. Access_Type can be any (pool-specific) access type which accesses Item_Type.
Inside the package, the declaration of Pointer_Type doesnt need to specify publicly that Pointer_Type is a tagged type since
this would give clients the freedom to derive from Pointer_Type and override the Finalize operation if they wanted to, so the
package can look something like this:
with Ada.Finalization;
generic
type Item_Type(<>) is limited private;
type Access_Type is access Item_Type;
package JE.Pointers is
type Pointer_Type is private;
with JE.Pointers;
package JE.Coords is
type Coord_Type is
record
X, Y : Float;
end record;
type Coord_Access is access Coord_Type;
package Coord_Pointers is
new JE.Pointers (Item_Type => Coord_Type, Access_Type =>
Coord_Access);
A : Coord_Access;
B : Coord_Pointers.Pointer_Type;
end JE.Coords;
This instantiates JE.Pointers in a library-level package; this is necessary because of the rule that derived types must be
declared at the same level as the parent type.
There are two objects declared in this package: A is a normal access variable while B is a smart pointer. Where you would
refer to the components of the object that A points to as A.X, A.Y or A.all, you refer to Value(B).X, Value(B).Y and
Value(B).all to get at the components of the object that B points to. B starts off as a null pointer, so youll need a constructor
function that generates a Pointer_Type object from an Access_Type value:
This just takes a copy of the parameter and encapsulates it in a smart pointer. This means that you should avoid using the
parameter elsewhere; dont deallocate it using Ada.Unchecked_Deallocation or use it to initialise another smart pointer, since
it will then end up being deallocated twice. This will usually be fatal for the memory management system and your program
will probably crash shortly afterwards in a mystifying way. Here is how you should use Pointer:
The parameter to Pointer is a value created using new which wont be used elsewhere. As you can see, you can use Pointer to
generate an initial value for use in a declaration; if a declaration doesnt provide an initial value, the variable will start off
with a null pointer as usual.
There is of course no version of Adjust for Limited_Controlled since Limited_Controlled (as well as any type derived from
it) is a limited type and so assignment isnt possible anyway.
When a value is assigned to a controlled object, Adjust is automatically called after copying the new value into the object. If
A and B are objects of the same controlled type, the assignment A := B involves taking a copy of B, finalising the current
value of A before it gets destroyed by being overwritten, copying the copy of B into A, adjusting A, and then finalising the
copy of B before its destroyed:
This looks like a laborious business but the compiler can generally optimise it so that it ends up like this:
The extra copy of B is usually unnecessary, but it caters for the case where an object is assigned to itself:
A copy of Bs value is made before B is finalised, and its this copy thats assigned back into B and then adjusted.
Adjust can take care of any post-assignment adjustments that may be needed. In the case of Pointer_Type we can make
assignment work properly by using Adjust and Finalize to maintain a reference count showing how many references there
are to the same object, and only deleting an object when the reference count reaches zero. This means that Pointer_Type will
need to point to an object which holds the reference count as well as the actual pointer to the data; keeping the reference
count in the Pointer_Type object wouldnt work since other Pointer_Type objects would have no idea it was there. Having
multiple Pointer_Type objects point to a common item means that the item itself can keep track of the reference count. This
means that well need Pointer_Type objects to point to reference counted objects which contain a reference count and a
pointer to the actual data being managed. This will involve a number of changes to the existing package:
with Ada.Finalization;
generic
type Item_Type(<>) is limited private;
type Access_Type is access Item_Type;
package JE.Pointers is
type Pointer_Type is private;
function Pointer (Value : Access_Type) return Pointer_Type;
function Value (Pointer : Pointer_Type) return Access_Type;
private
type Reference_Counted_Object is
record
Value : Access_Type;
Count : Natural;
end record;
Pointer_Type now points to a Reference_Counted_Pointer which contains the actual pointer value together with a reference
count. You still have to be a bit careful since circular lists of reference counted objects will never be deallocated because each
object will always be pointed to by the preceding object in the list so the reference count will never become zero. A
Pointer_Type object will start off as a null pointer, so itll be necessary to check for null within each of the operations
provided by the package. The definitions of Pointer and Value both need changing to reflect this:
procedure Delete_Item is
new Ada.Unchecked_Deallocation (Item_Type, Access_Type);
begin
if Pointer.Pointer = null then
return null;
else
return Pointer.Pointer.Value;
end if;
end Value;
Pointer has to create an object which points to a new Reference_Counted_Object with a reference count of 1 while Value has
to check whether its parameter contains a null pointer and then return either null or the access value in the
Reference_Counted_Object it points to.
If a Pointer_Type object being finalised doesnt contain a null pointer, Finalize will need to decrement the reference count
and then delete the object if the count has reached zero:
procedure Delete_Pointer is
new Ada.Unchecked_Deallocation (Reference_Counted_Object,
Reference_Counted_Pointer);
Adjust will need to increment the reference count when a non-null value is assigned to a Pointer_Type object:
Assigning another Pointer_Type value to Object means that Object will just point to the same reference counted object as the
other one does. The old value of Object will be finalised, so that the reference counted object it points to will have its
reference count decremented and will be deleted if necessary. The new value will then be copied into Object. Finally, Adjust
will increment the reference count if it isnt a null pointer to mark the fact that there is now one extra Pointer_Type object
pointing to the same Reference_Counted_Object.
Note that Adjust will also be called if an initial value is specified as part of the declaration of a Pointer_Type object:
The value of A will be copied into B so that A and B will end up pointing to the same reference counted object, and then
Adjust will be called to increment the objects reference count.
A little ingenuity with global variables in the package body where these functions are declared will let you associate unique
identifiers with each Pointer_Type object thats created (as in the Bank_Account example mentioned in chapter 6) to display
even better messages to keep track of whats going on. In the case of Pointer_Type, you might want to include a numeric ID
in each Reference_Counted_Object and display the object ID and reference count every time the object is updated:
type Reference_Counted_Object is
record
Value : Access_Type;
Count : Natural;
Ident : Positive; -- a unique object number
end record;
The function Pointer will need to allocate numbers for the Ident component of Reference_Counted_Objects as they are
created:
Count => 1,
Ident => RCO_Number);
RCO_Number := RCO_Number + 1;
return Object;
end Pointer;
Finalize and Adjust can display the Ident component and the corresponding reference count as changes are made:
This will make it much easier to track down the exact order of events when youre dealing with controlled objects. You cant
use your text editor to track down the places where Initialize, Finalize and Adjust are called since the compiler inserts
invisible calls to these procedures, so you need to design controlled types so that they will provide the debugging information
automatically. At this point its worth pointing out a possible use for the parent package JE:
package JE is
procedure Debug (On : in Boolean); -- set a hidden flag
function Debugging return Boolean; -- test the hidden flag
end JE;
This lets you turn a flag on or off and then test it in the child units to determine whether to display debugging information.
You could even use an enumeration or an integer value instead of a Boolean to choose between different levels of detail. That
way if anything goes wrong, your debugging tools are ready and waiting to be turned on if you need them. They make your
application larger when you dont need them, but discarding your debugging tools to save space has been compared to
leaving your parachute at home once you get your pilots licence. Need I say more?
Exercises
16.1 Modify the playing card package from exercise 9.2 to make a pack of cards a controlled type. Declaring a pack of cards
should create a shuffled set of all 52 cards automatically.
16.2 Create a generic package which defines a controlled type containing a random number generator from
Ada.Numerics.Discrete_Random (see exercise 5.1). The controlled types Initialize operation should call Reset to
guarantee that the random number generator is initially randomised.
16.3 Modify the diary program so that diaries are loaded and saved automatically at initialisation and finalisation. For the
sake of simplicity, use filenames suffixed by a number (Diary1, Diary2 and so on) for each diary object you declare; to
do this, use a global diary number which is incremented every time a diary object is declared and use the diary number
to generate the filename.
16.4 Modify the linked list package to make it possible to do deep copying of lists. This means that when one list is
assigned to another, copies of all the items in the list are made so that a completely separate duplicate copy is produced.
You can do this by making List_Type a controlled type and overriding Adjust.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are
distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 17:
An object-oriented calculator
We shall have to evolve problem-solvers galore
since each problem they solve creates ten problems more.
Piet Hein, More Grooks
Apart from the string containing the expression to be recognised, the other main component of the evaluation process is a
specification of the expressions syntax. In previous versions of the program, the syntax was implicit in the subprograms which
did the analysis. This means that it couldnt be altered without modifying the existing subprograms. By using a tagged type
Expression_Type, the subprograms can be made primitive operations of the type and overridden in derived classes as
necessary. This will allow new types of operators and operands to be added without affecting existing code, as well as allowing
changes to be made to the syntax of expressions. A tagged type can therefore be seen as a collection of subprograms rather
than just as an extensible collection of data components. This view is central to the object-oriented way of looking at the
world. Heres the declaration of Expression_Type:
Expression_Type doesnt need to contain any data, since its just a collection of primitive operations. The full declaration in
the private part of the package will just look like this:
Expression_Type is a limited type because there is no need to allow assignment and comparison of Expression_Type values.
All Expression_Type objects will effectively be identical since theyll all embody the same syntax rules. The main primitive
operation will be a function called Evaluate to evaluate a string representing an expression and produce an integer result:
The parameter Syntax will in effect embody the syntax rules for an expression, and Evaluate will use these rules to process the
expression given in the Expr parameter to produce an integer result. Evaluate will be a primitive operation of Expression_Type
so that it can be overridden by descendants of Expression_Type if the format of an expression needs to be changed. Well also
need an exception that Evaluate can use to report syntax errors:
Syntax_Error : exception;
These declarations are the only ones that clients of the package need to be able to see. Everything else can be put into the
private part of the package so that its hidden from clients but is still accessible to allow child packages to define types derived
from Expression_Type.
17.2 Tokens
An expression consists of a series of tokens arranged according to the syntax rules embodied in Evaluate. The types of token
that the calculator in chapter 13 handled included numbers, various operators, left and right parentheses, and an end-of-
expression marker (the end of the line or a full stop or something similar). To allow the set of legal tokens to be extended we
need yet another tagged type:
This is an abstract type since we wont want to allow clients to create amorphous Token_Type objects; you should only be able
to deal with specific tokens like numbers, the addition operator and so on. There isnt any data that all tokens will have in
common, so Ive declared it to be a null record. Token_Type is there so that all tokens will have a common parent and the class-
wide type Token_Type'Class will be able to be used for any token at all. We can derive some concrete classes directly from
Token_Type:
We can also derive further abstract types to represent specific classes of token which add some necessary primitive operations:
We know that there are a number of different operators; these obviously all have something in common, so Ive introduced a
type called Operator_Type to capture the features which are common to all operators. Operators have a priority (i.e. a
precedence), so there is a primitive function of Operator_Type called Priority which returns the operators priority as a value of
type Operator_Priority_Type. This allows for nine different levels of priority; Ill use 1 for the highest priority and 9 for the
lowest. The reason for making Operator_Priority_Type a subtype of Priority_Type is to allow 0 to be used for operands; Ill
explain this in more detail later. Priority is an abstract function so concrete derivations of Operator_Type will be forced to
provide an overriding definition for it.
Rather than defining a type Number_Type to represent numeric operands directly, Ive defined a more general type
Operand_Type so that different types of operands can be added later. The common feature of operands is that they have a
value, so Operand_Type has a primitive function to return the value. This is also an abstract operation which will need to be
overridden when we know what type of operand were dealing with.
Operators come in two basic types: binary operators with two operands and unary operators with only one. We can derive
further types from Operator_Type which provide primitive functions called Apply to apply the operator to its operands:
Some operators can be either binary or unary; unfortunately Ada does not allow multiple inheritance where a derived type
can have more than one parent (although its possible to use workarounds involving generics or access discriminants), but in
this case we can achieve something close to the desired effect by rederiving from Binary_Operator_Type:
This has two versions of Apply: the first is inherited from Binary_Operator_Type to deal with the case where the operator is
used in its binary form and the second is a new one introduced to deal with the case where it is used in its unary form. There is
also a separate function Unary_Priority to return the priority of the unary form of the operator. For now well have two main
categories of operator: adding operators (+ and ) and multiplication operators (* and /). Here are the declarations we need for
these:
type Multiplying_Operator_Type is
abstract new Binary_Operator_Type with null record;
function Priority (Operator : Multiplying_Operator_Type)
return Operator_Priority_Type;
type Adding_Operator_Type is
abstract new Variadic_Operator_Type with null record;
Priority is no longer an abstract operation since we know what the priorities of multiplication and addition operators are:
The priorities 5 and 6 have been chosen to allow other operators to have even lower priorities if necessary; the unary priority
for the adding operators is the second highest to allow for one higher level. Now we can derive the final concrete types. Here is
the definition of a type to represent :
The other operators are similar; Ill leave it to you to come up with their declarations. Here are the bodies of the Apply
primitives for Minus_Operator:
It may seem strange to have a data type that contains no data, but in fact there is one item of data that these types all have: their
tag. The tags are the reason that we need these types at all, so that we can dispatch to the correct operation. Once again, you
can think of these data types as collections of operations rather than as collections of data.
type Number_Type (Value : Integer) is abstract new Operand_Type with null record;
function Value (Operand : Number_Type) return Integer;
Unlike the other types, this does contain one item of data apart from the tag, namely the value of the number. This is supplied
as a discriminant so that the value of a Number_Type object must be set when it is declared. The primitive function Value
simply returns the value of the discriminant:
Next_Token needs to start at the position in Expr specified by From, skip over any spaces and then use the character its found
to determine the actual token type. It will then need to create a token of the appropriate type and return a class-wide pointer
using the Token parameter. The package JE.Pointers from the previous chapter is used to ensure that all pointers are
automatically destroyed after use. Its also possible that Next_Token will go past the end of the expression buffer, so it needs
to check for this and return an End_Of_Expression token if it happens. Heres the implementation of Next_Token:
This begins by skipping over any spaces and then checking if the current position is beyond the end of the string. If so, it
creates a new End_Of_Expression token to be the result of the procedure. If not it calls another primitive function
Fetch_Token to extract the next token from the string.
Using new to create tokens like End_Of_Expression is somewhat wasteful since all objects of this type are identical.
Try to modify the design so that there would only be one token of each type that operations like Next_Token would
return pointers to.
Note that Syntax is converted to a class-wide value so that the call to Fetch_Token will be a dispatching call; if you forget to
do this, overriding Fetch_Token will have no effect since Next_Token will still be calling the Expression_Type version of the
procedure. Fetch_Token is declared like this:
A case statement is used to select amongst alternatives based on the first character of the token. Ive assumed that there are
descendants of Token_Type called Plus_Operator, Times_Operator and Over_Operator which have been defined by a similar
process to Minus_Operator. Numbers are dealt with using yet another version of Get defined in Ada.Integer_Text_IO which
reads a value from a string. The first parameter is the string to read from (in this case, everything from the current position to
the end of the string), the second is the variable to store the result in, and the third is an output which is set to the position of
the last character that was read. At the end of Fetch_Token, the current position is incremented to get past the last character of
the token. In the case where the character thats been read in isnt recognised at all, Ada.Exceptions.Raise_Exception is used to
raise a Syntax_Error exception. Ive done it this way instead of using a raise statement because Raise_Exception provides the
ability to specify a message which will be associated with the exception.
Parse will do the actual expression evaluation, storing the result in Value as well as returning the terminating token. If the
terminating token is not an End_Of_Expression token, there is either an operator missing or too many right parentheses (i.e. a
missing left parenthesis) so a Syntax_Error exception is reported.
Prio : in Priority_Type;
Result : out Integer;
Next : in out Token_Pointer);
The intention here is to do recursive descent parsing as described in chapter 13. The reason for the Prio parameter is that an
expression can be considered to be a sequence of terms separated by low-priority (priority 9) operators; each term is a
sequence of terms separated by priority 8 operators, each of which is a sequence of terms separated by priority 7 operators, and
so on. This can be represented as a set of syntax rules using the same notation as was used in chapter 13:
You can see the sense in this if you consider how similar the functions Expression and Term were in chapter 13. Parse can
simply call itself recursively with the value of N (the operator priority) as the value of the parameter Prio. The recursion will
end when the highest operator priority is reached; each term will then be an operand (i.e. a number, a unary operator followed
by a subexpression or a subexpression in parentheses). A priority of zero can be used to indicate that we need to read an
operand. This is the reason why Priority_Type has a wider range than Operator_Priority_Type. Parse can therefore be
implemented like this:
end;
end if;
end Parse;
If the supplied priority is zero, we need to get an operand. This is done by calling Get_Operand (which is another dispatching
call, thanks to the conversion to Expression_Type'Class). If the priority is non-zero, Parse is called recursively to get a term
involving operators at the next higher priority (i.e. Prio1). The value of the term will be stored in Value, and the token which
terminated it will be stored in Op. The terminating token might be a binary operator of the required priority, in which case we
need to get the next term by calling Parse again. This time the value is stored in Right and the terminating token is stored in
Next. The operator in Op is then applied to the two operands in Value and Right, and Next is then copied into Op to be used as
the operator the next time around the loop. When the loop exits, the token in Op (the final terminating token) is copied back
into Next before returning.
Why didnt I need to convert the Syntax parameter to a class-wide type in the recursive calls to Parse?
All thats left to do now is to define Get_Operand. An operand can be a number, an expression enclosed in parentheses or a
unary operator followed by an expression involving operators with a higher priority than the unary operator. The syntax of
operands can therefore be written like this:
Operand = Number
| ( Expression )
| Unary_Operator_N Term_(N-1)
This uses Unary_Operator_N to symbolise a unary operator with priority N. Heres the implementation:
If the next token is an operand, its Value operation is used to set the value of the Result parameter and the next token is
returned. If its a left parenthesis, an expression is processed using Parse to get its value into Result and a check is then made
that the token returned by Parse is a right parenthesis. If it is, the token after the right parenthesis is read; otherwise, a syntax
error is reported. If the token is a unary operator, Parse is called to process the term that follows; thanks to the lack of multiple
inheritance this code has to be replicated for Variadic_Operator_Type as well. If the token is none of these, its an error: either
an unexpected end of expression or an invalid token.
Exercises
17.1 Modify the calculator to support an exponentiation operator represented by ^ with the same precedence as the Ada
operator **, i.e. with a higher priority than any other operator.
17.2 Modify the calculator to allow expressions to contain hexadecimal values which are indicated by a prefix of $, e.g.
$03FF for 16#03FF#.
17.3 Modify the calculator to allow postfix operators (i.e. operators which appear after their operands), and provide a factorial
operator ! (see chapter 13) so that, for example, 5! evaluates to 120.
17.4 Modify the program in this chapter to convert an arithmetic expression into reverse Polish notation. The reverse Polish
form of an expression consists of the operands of an operator followed by the operator itself, so that 5+3 is translated to
5 3 +, and (1+2)*(3+4) is translated to 1 2 + 3 4 + *. This can be done by modifying Get_Operand to display
operands as they are read and modifying Term to display operators after the operands have been dealt with, rather than
using Apply to evaluate the result of the operation as at present.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are
distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 18:
Designing a spreadsheet
Write the vision, and make it plain upon tables.
Habakkuk, 2:2
18.1 Spreadsheets
18.2 Defining the program
18.3 The spreadsheet class
18.4 Cell implementation
18.5 Formula cells
18.6 Deriving a new expression type
Exercises
18.1 Spreadsheets
Now that the principal features of tagged types have been covered, its time to take another look at the design process and
see how all this fits in. In the first part of the book I described how top-down design could be used to break problems
down into a number of smaller problems; in the second part I described how the design process should revolve around the
types of data that the program is intended to model using a combination of top-down and bottom-up design. The object-
oriented design process also involves identifying type classes. As I mentioned in connection with abstract data types in
chapter 10, nouns in a specification often correspond to data types and verbs to the operations on those types. Adjectives
also provide a useful clue for identifying type classes by their inheritance relationships; for example, an urgent
appointment is clearly a form of appointment so there will presumably be an inheritance relationship between
Appointment_Type and Urgent_Appointment_Type (or between these and some common parent type). Also, as I showed
you in the last chapter, not all type classes contain data; sometimes classes which just encapsulate a set of operations are a
useful abstraction.
The design process is therefore similar to the approach I described for use with abstract data types except that there are a
few more questions you need to ask. If two types are similar, is there an inheritance relationship between them or should
there be a common parent type which encapsulates the features they share? In either case, you want to use tagged types.
Then you need to consider what operations of the types are primitive, which should be class-wide, which should not be
primitive. Might any of the types need extending at some point in the future? If so, you need to use tagged types again.
This case requires some foresight and a feeling for possible maintenance scenarios in order to decide what sort of
provision to make for future extensions.
In this chapter Im going to illustrate this by developing a spreadsheet. Spreadsheets are among the most widely used
applications for computers, and Ive chosen it as an example because its one I expect most people to be familiar with. A
spreadsheet consists of a grid of cells; the rows are numbered from 1 upwards and the columns are named A, B, C and so
on. To allow for more than 26 columns, columns after Z are named AA, AB, AC and so on up to AZ, then BA, BB, BC
and so on. Individual cells are referred to by their grid coordinate, e.g. A1 or BC100.
There are several possible different types of cell. Initially all cells are empty; the user can enter an expression to be stored
in a cell (a formula cell) or a literal string that is displayed as it is (a string cell) for use as table headings and so on. Other
types of cell are also possible. Naturally, the expression evaluator developed in the previous chapter will be a useful
component for implementing formula cells.
Whenever the spreadsheet changes it is recalculated and redisplayed. Recalculation involves evaluating the expression in
each formula cell. Expressions can refer to the values in other formula cells, so a change to a single cell might affect the
values of several other cells. Empty cells and string cells have no value (they are undefined); if a formula refers to an
empty cell or string cell its value is also undefined. Cells cant refer to their own value, directly or indirectly; for example,
if cell A1 was defined to be A2+1 and A2 was defined as A11 it would be impossible to work out the values of A1 or A2.
If this happens its an error; the value of any cell whose definition is circular is undefined.
Looking at this specification, we can start work on it by identifying the classes that will be needed. We will obviously
need types for spreadsheets and cells. Should these be tagged or untagged? Making a spreadsheet a tagged type will allow
it to be extended in the future, so this seems to be a good idea. The specification above mentions several types of cell, so a
cell should definitely be a tagged type and specific types of cell can then be derived from it (string cells and formula cells,
for now). The spreadsheet can deal with a grid of class-wide pointers to cells to allow different cell types to be used within
a single spreadsheet. As an implementation detail, empty cells can be dealt with by the spreadsheet itself; any cell that
isnt in use is empty. This will avoid having to store empty cells in memory.
What operations are needed on these types? The specification tells us that spreadsheets can be recalculated and displayed,
and cell values can be changed. Changing an existing cell might involve changing its type, so this will have to be done by
deleting the existing cell (if any) and creating a new one. Since expressions in formula cells can refer to other cells, we
need some way of locating a particular cell as well. Recalculation is needed if a cell is changed, so it would probably be a
good idea to have a procedure that a cell can call to notify the spreadsheet that a change has taken place. Doing it this way
rather than recalculating every time a cell changes allows the spreadsheet to decide when recalculation is necessary (e.g.
just before redisplaying the spreadsheet) to minimise the number of times that it gets recalculated. These considerations
also imply that a cell must be able to identify the spreadsheet it belongs to.
Well also need a procedure to be called to cancel the change notification after the spreadsheet has been recalculated and a
function to test if the spreadsheet has changed; these should only be called from within Recalculate, so they can go in the
private part of the package. Although the only thing theyll do will be to access a Boolean variable, its still a good idea to
provide primitive operations to do this rather than just accessing the variable directly so that any future derived types can
override them to provide different behaviour if necessary. This is an application of the sort of foresight I mentioned
earlier. Finally, the problem of circular definitions for cells can be reported with an exception; syntax errors in expressions
will also be reported by an exception. Heres a first stab at a spreadsheet type:
Circularity_Error : exception;
The Display procedure is abstract since this is view dependent; a program using a spreadsheet will need to derive a
concrete type which displays the spreadsheet in an appropriate way. The spreadsheet type is limited to prevent assignment
of one spreadsheet to another (since the effect would be to copy the pointers to the cells rather than the cells themselves).
Each cell needs to know which spreadsheet it belongs to (so it can notify the spreadsheet whenever it changes) so Ive
used an access discriminant as described in chapter 11 to act as a pointer to the spreadsheet its part of. As explained in
chapter 11, access discriminants are only allowed for limited types, so Cell_Type has to be limited; they cant be null, so it
isnt possible to create cells without reference to a specific spreadsheet, and the accessibility checks used on named access
types dont apply. Cell_Type will be derived from Limited_Controlled since destroying a cell might well involve some
clean-up action; for example, the cell could notify the spreadsheet that it had changed as the result of a cell being
destroyed. Again, Im applying some foresight to the design.
Cells have a number of common properties. They have a value that can be displayed on the screen; the value can also be
accessed as an integer if it isnt undefined. If the value is undefined, we can raise an exception to report it. Cells need to be
evaluated as part of the spreadsheet recalculation, so a procedure to re-evaluate a cell will be needed. It might also be
useful to be able to inspect the actual cell contents rather than just the evaluated result of the expression. Heres a list of
some plausible primitive operations for Cell_Type:
Undefined_Cell_Error : exception;
The operations declared here are all abstract, so derived cell types will need to override them in an appropriate way.
Formula cells can be derived from Cell_Type by adding an extra discriminant and overriding the abstract operations:
You cant inherit from a discriminated type without providing the necessary discriminants, so Formula_Cell_Type has an
access discriminant called Sheet which is then used as the constraint for Cell_Type in the type declaration.
Formula_Cell_Type also has a Natural as a discriminant which will be used for the length of the expression associated
with the cell. String cells will also need a similar set of discriminants:
The extra discriminant in this case is the size of the string in the cell. Constructor functions can be declared to construct a
cell from a spreadsheet pointer and a string:
Notice that these are not primitive operations of the cell types, so the inheritance problems related to constructors that I
described earlier will be avoided.
with JE.Spreadsheets;
package JE.Views.Spreadsheet is
type Command_Type is (Modify, Display, Quit);
function Next_Command return Command_Type;
private
type Sheet_Extension is new JE.Spreadsheets.Spreadsheet_Type
with null record;
type Sheet_Type is
limited record
Innards : aliased Sheet_Extension;
end record;
end JE.Views.Spreadsheet;
Sheet_Type is a limited private type; its actually a record containing a single Sheet_Extension component, where
Sheet_Extension is derived from Spreadsheet_Type. The reason for this is that we need to be able to supply cells with a
discriminant value which points to the spreadsheet theyre part of, so the spreadsheet is made into an aliased component of
Sheet_Type. Since the component is aliased, the 'Access attribute can be used to get a pointer to it which can then be used
when creating new cells.
The commands are Modify, Display and Quit. The intention is that Modify will ask the user to select a cell, display its
current contents and then ask for a new value. The main program will use the view package to process commands from the
user:
with JE.Views.Spreadsheet;
procedure Spreadsheet is
Sheet : JE.Views.Spreadsheet.Sheet_Type;
begin
JE.Views.Spreadsheet.Display (Sheet);
loop
case JE.Views.Spreadsheet.Next_Command is
when JE.Views.Spreadsheet.Modify =>
JE.Views.Spreadsheet.Modify (Sheet);
when JE.Views.Spreadsheet.Display =>
JE.Views.Spreadsheet.Display (Sheet);
when JE.Views.Spreadsheet.Quit =>
exit;
end case;
end loop;
end Spreadsheet;
Now well need to implement the package body. This version will just use Ada.Text_IO; its a lowest-common-
denominator interface that could easily be improved:
Skip_Line;
case Command is
when 'M' | 'm' =>
return Modify;
when 'D' | d' =>
return Display;
when 'Q' | 'q' =>
return Quit;
when others =>
Put_Line ("Invalid command -- " &
"please enter M, D or Q.");
end case;
end loop;
exception
when End_Error =>
return Quit;
end Next_Command;
The version of Display for Sheet_Type will just call the version of Display for Sheet_Extensions to display its Innards
component:
Ill assume that this program will be run on a standard 80-column text screen with 25 lines of text. Ill use a couple of
constants for this which you can change if you need to:
Ill display the cells in columns which are 12 columns wide; this will allow for six columns (A to F) with a seven-
character left margin for the row number and a one-character right margin (to prevent the cursor wrapping to a new line
if text is displayed in the last column of the screen). Ill leave one row for the column headings and another four for the
menu and user responses, which leaves 20 rows. The spreadsheet itself might be bigger than 20 rows of six columns each,
but if so the extra cells wont get displayed. Ill need some more constants for the rows and columns:
Right_Margin : constant
1; :=
Column_Width : constant
12; :=
Column_Count : constant :=
(Screen_Width - Right_Margin) / Column_Width;
Left_Margin : constant :=
Screen_Width - Right_Margin -
(Column_Count * Column_Width);
Top_Margin : constant := 1;
Bottom_Margin : constant := 4;
Row_Count : constant := Screen_Length - Top_Margin - Bottom_Margin;
The spreadsheet is recalculated before its displayed in case anythings changed recently. Notice that the code above
assumes that well never have more than 26 columns (A to Z); multicharacter column names arent catered for. It uses a
procedure called Set_Col from Ada.Text_IO; Set_Col moves the cursor to the specified column (character position) of the
current screen line. The screen layout constants are used to calculate where each item being displayed should go. Note that
the innermost loop which displays the actual values uses the character in Col together with the row number to construct a
string which is the coordinate of the required cell; the row number is converted to a string using Integer'Image and the
second character onwards is used in the name of the cell coordinate (the first character is the sign, either a space or a
minus sign). If the text to be displayed is wider than one character less than the column width it will be truncated. This is
the purpose of the variable Width; it is set to the minimum of Column_Width1 and the width of the cells value, and the
result is used to slice out the appropriate number of characters from the cells value.
Modifying the current cell will involve getting the cell coordinates, displaying the current cell contents and inviting the
user to type in a new value. Ill use the convention that string cells are created by typing a value beginning with a quote,
empty cells are created by typing in a full stop, and formula cells are created for any other input. Entering a blank line will
with JE.Lists;
package Spreadsheets is
type Spreadsheet_Type is abstract tagged limited private;
type Cell_Type (Sheet : access Spreadsheet_Type'Class) is
abstract tagged limited private;
type Cell_Access is access Cell_Type'Class;
... -- etc.
private
Cell_Name_Length : constant := 6;
subtype Cell_Size is Integer range 0..Cell_Name_Length;
type Cell_Record is
record
Where : String(1..Cell_Name_Length);
Size : Cell_Size;
Cell : Cell_Pointers.Pointer_Type;
end record;
package Cell_Lists is new JE.Lists (Cell_Record);
type Spreadsheet_Type is
abstract tagged limited record
Cells : Cell_Lists.List_Type;
Dirty : Boolean := False;
end record;
end Spreadsheets;
Ive used a constant so that cell names are limited to a maximum of six characters. See if you can devise a method
which will allow names to be unlimited in length, or a method which will allow the maximum length to be changed
without recompiling the package and all its clients.
The spreadsheet consists of a list of (non-empty) cells and a dirty flag. Despite the fact that the spreadsheet is
theoretically a grid of cells, theres no reason why a linked list cant be used to implement it. After all, a spreadsheet is
really just a collection of cells which happens to be presented as a rectangular grid; the external representation neednt
have anything to do with the internal representation. Each Cell_Record in the list contains the cells coordinate and a
smart pointer to the cell itself (as described in chapter 16). Using a smart pointer ensures that the cell will be destroyed
automatically when its removed from the list.
Finding a cell with a given coordinate is just a matter of searching the list for a cell with that coordinate. If you cant find
it, its an empty cell. The Dirty flag is set whenever any cell has changed; this indicates when a recalculation is necessary.
The primitive procedure Change sets the dirty flag, Updated clears it and the primitive function Changed tests it:
Cell needs to scan through the linked list looking for a Cell_Record whose coordinate (the Where component) matches the
coordinate given as its parameter. As I mentioned earlier, if the cell isnt found Cell will just return a null pointer to
indicate that its an empty cell:
This uses the function To_Upper from Ada.Characters.Handling to ignore case differences when comparing cell
coordinates so that a1 will be recognised as referring to the same cell as A1.
Delete searches for the named cell and deletes it from the list if its there, or does nothing if it isnt :
end if;
Iter := Succ(Iter);
end loop;
end Delete;
Deleting a cell will cause the smart pointer inside it to finalise itself, so the Cell_Type it points to will be deallocated
properly.
Insert deletes the cell with the given coordinates if it already exists and then creates a new Cell_Record using the
coordinate and cell pointer supplied as parameters (but if the cell pointer is null theres nothing to insert, so this needs
checking for) and then adds it to the end of the list:
Recalculate checks if the spreadsheet has changed by calling Changed, and then goes through the list asking each cell to
evaluate itself if it has:
This is not going to be terribly efficient; the expression in a formula cell can refer to the names of other formula cells, so
evaluating a formula cell will involve evaluating any other formula cells that it refers to. This means that individual cells
can end up being evaluated several times. One way to overcome this is to get cells to remember when they were last
evaluated. If we keep an evaluation number in the spreadsheet which is updated on each call to Recalculate, each cell
can copy the evaluation number when its evaluated and then just return the current value without re-evaluating it if the
spreadsheets current evaluation number is the same as the copy in the cell. Only two evaluation numbers are needed to
distinguish between two successive calls to Recalculate. Heres how the declaration of Spreadsheet_Type will need to
change:
type Spreadsheet_Type is
tagged limited record
Cells : Cell_Lists.List_Type;
Dirty : Boolean := False;
Eval : Evaluation_Number := Evaluation_Number'First;
end record;
Therell also need to be another primitive function to enable cells to access the current evaluation number:
Recalculate will need to increment the evaluation number at the very beginning:
Since the evaluation number is modular, it will go 0, 1, 0, 1 and so on on successive calls to Recalculate.
A pointer to the enclosing spreadsheet. This is already there in the form of an access discriminant.
A flag to record if it is currently being evaluated. If this flag is set and the cell is asked to evaluate itself it indicates
that theres a circular reference, and we can use this to detect circular definitions and raise a Circularity_Error.
The last three can be combined into a state variable based on an enumerated type since theyre mutually exclusive; a cell
is either being evaluated, or its erroneous, or its either defined or undefined. Ill also have another value for when its
state is unknown:
The Unknown state can be used as an initial value before the cell has been evaluated for the first time:
During evaluation the cell will be in the Evaluating state; at the end of evaluation it will end up as Defined, Undefined or
Error.
Now we need to consider the derived types String_Cell_Type and Formula_Cell_Type. Heres the full declaration for
these types:
Notice how the full declarations have to provide the necessary discriminant (Sheet) for Cell_Type, unlike the declarations
in the visible part of the package:
Both types have a string to hold the cell contents, the size of which is given by their Size discriminants;
Formula_Cell_Type also has an Integer component to record the result of evaluating the cell. The constructor functions for
the two types can be defined like this:
Now we need to override the abstract operations inherited from Cell_Type. Ill deal with String_Cell_Type first of all
since its going to be simpler than Formula_Cell_Type.
The Text_Value and Contents operations just need to return the value of the string contained in the cell:
The value of a string cell is always undefined, so Evaluate just needs to set the state to Undefined and Num_Value just
needs to raise Undefined_Cell_Error:
Text_Value needs to return the current value as a String if the state of the cell is Defined. If the state of the cell is Error
then it should return an error message. Otherwise, the cell value is unknown so it should just return a null string:
Num_Value needs to return the current value as an Integer. If the value isnt defined it can just raise an
Undefined_Cell_Error exception:
This only leaves us with Evaluate to be defined. If the cell is already being evaluated, it needs to raise a Circularity_Error.
If the state is unknown or the evaluation number is out of date, the cell needs to be evaluated; otherwise, its already been
evaluated and nothing needs to be done. If it does need evaluating, the evaluation number needs to be updated and the
formula needs to be evaluated. The cell state must be set to Evaluating while the formula is being evaluated; afterwards
the state can be set to Defined if all is well, or Undefined if a reference to an undefined cell occurs (which will be reported
by Value as an Undefined_Cell_Error), or Error if an error occurs. The error can be a Syntax_Error from the expression
evaluation, a Constraint_Error because the result is out of range, or a Circularity_Error as described above:
exception
when Undefined_Cell_Error =>
if Cell.State /= Error then -- don't change state if
Cell.State := Undefined; -- there's already been an
end if; -- error reported
when Syntax_Error | Constraint_Error | Circularity_Error =>
Cell.State := Error;
end Evaluate;
The formula will need to be supplied with a pointer to the spreadsheet its associated with so that cells referenced in the
expression can be looked up. This is done by providing Formula_Type with an access discriminant, which means that
Formula_Type will need to be a limited type.
As Ive mentioned before, handling errors inside a package is generally a bad idea. It leads to a lack of flexibility, and
providing flexibility is what object-oriented programming is all about. Another type of spreadsheet derived in the future
might want to report errors to the user as they arise and give the option of carrying on or aborting the recalculation, but
this isnt possible with the current design. A better idea would be to provide a primitive operation of Spreadsheet called
Handle_Error and call this from the exception handler above, like this:
Since Sheet is a class-wide access discriminant, the call to Handle_Error will be a dispatching call. This means that
Handle_Error will need to be declared like this:
The access parameter Sheet allows any access-to-Spreadsheet value to be used to call Handle_Error; also, as mentioned
in chapter 14, an access parameter is treated as a controlling parameter so that Handle_Error will be a primitive operation
of Spreadsheet_Type. The Error parameter allows Handle_Error to use the operations in Ada.Exceptions to get more
information about the exception.
The default action can just be to do nothing, but making it a primitive operation of Spreadsheet means that derived
spreadsheets can override it. Parent types in class hierarchies quite often end up with primitive operations which do
nothing to act as hooks to allow extra processing to be added in later by derived classes if its needed. You should
always look carefully at do-nothing bits of your code (null clauses in case statements, missing else parts in if statements
and so on) and consider whether there will ever be a need to change it to do something. If so, add a primitive operation to
do nothing for you. However, it takes some experience to be able to spot these things, because theres nothing there to
make you notice them!
Returning from Handle_Error will effectively mean that the error has been ignored, but an overridden version of
Handle_Error could raise another exception (or the same one) in which case it will be raised at the point where Evaluate
was called from (remember that if an exception is raised inside an exception handler, you immediately exit from the block
containing the handler and you then look for an exception handler in the block youve returned to).
with JE.Spreadsheets;
package JE.Expressions.Spreadsheet is
type Formula_Type (Sheet : access Spreadsheet_Type'Class) is
new Expression_Type with private;
private
type Formula_Type (Sheet : access Spreadsheet_Type'Class) is
new Expression_Type with null record;
... -- other declarations
end JE.Expressions.Spreadsheet;
The only difference between Formula_Type and Expression_Type is that Formula_Type needs to recognise cell
coordinates as a new type of operand. This means we need a new type derived from Operand_Type together with an
overriding declaration for the primitive operation Value:
These declarations will need to go in the private part of the package. This type has a Cell_Access value as a discriminant
which points to the cell being referenced as an operand. The Value operation will involve evaluating the cell (in case it
hasnt been evaluated yet) and then returning its value:
The only other thing thats necessary is to recognise cell coordinates as a new type of token within the expression. This
can be done by overriding the Fetch_Token primitive inherited from Expression_Type. Heres the declaration for an
overridden Fetch_Token procedure for Formula_Type:
Cell coordinates always begin with a letter and consist entirely of letters and digits. Fetch_Token will need to check that
the current character is a letter and then search for the end of the token. The characters making up the token can then be
used to obtain a pointer to the corresponding cell, and this can be used to create a Cell_Operand_Type to be returned as
the value of the function. To make things easier Ill use some more functions from Ada.Characters.Handling: Is_Letter,
which tests if its parameter is a letter, and: Is_Letter Is_Alphanumeric, which tests if its parameter is a letter or a digit:
If the token doesnt start with a letter, the Formula_Type parameter is converted to an Expression_Type value. The
original Expression_Type version of Fetch_Token will then be used to extract the token, so this will deal with numbers,
operators and so on.
Exercises
18.1 Modify the existing spreadsheet design so that the cell coordinates are stored in the cells themselves, rather than in a
separate Cell_Record structure.
18.2 At the moment you cant save a spreadsheet to a file or load a spreadsheet from a file. Provide primitive operations
to load and save the spreadsheet. These should each take a String parameter representing the name of the file to load
from or save to.
18.3 Modify the spreadsheet to support a new cell type which is like a formula cell except that its value represents a
students percentage grade (which should be between 0 and 100) and displays it as a letter grade: A for 80 or above,
B for 65 to 79, C for 55 to 64, D for 40 to 54, E for 20 to 39 or F for below 20.
18.4 Modify the spreadsheet to support a new cell type which is like a formula cell except that it has a second
discriminant called Base, which is a value between 2 and 16 specifying the number base to be used for the text value
of the cell; for example, when Base = 2 the cells value will be displayed in binary.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are
distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 19:
Multitasking
Tell me your tasks in order.
Dylan Thomas, Under Milk Wood
Ada allows you to define tasks which are executed independently. The program itself is executed by an
environment task, and this is allowed to create further tasks which are executed in parallel with all the other tasks
in the program. You could define a new type of spreadsheet cell which contained a task. As soon as such a cell was
created, the task associated with it would begin executing in parallel with the rest of the program. Active cells like
this would be able to change their values in response to external conditions and then call on the spreadsheet to
recalculate itself. The spreadsheet itself could be an active object which waits for recalculation requests and then
recalculates and displays itself, in parallel with everything else thats happening.
Note that this feature of Ada is heavily dependent on the underlying operating system; an operating system like
MS-DOS has no multitasking capabilities (and, indeed, is positively hostile to multitasking) so that
implementations of Ada for MS-DOS systems can only provide multitasking capabilities poorly, if at all.
Fortunately the situation is changing, and real operating systems like Unix, OS/2 and Windows/NT are driving
out the toy systems which sprang up in the infancy of microcomputers and have stayed with us like some
debilitating disease ever since.
The task specification defines a task type. The simplest form that this can take looks like this:
This defines a type called Repeat; you can then declare as many tasks of this type as you require:
These tasks will be started at the end of the declaration section where they were created, i.e. immediately before
the first statement in the block where they are declared:
declare
A, B : Repeat;
C : array (1..100) of Repeat;
begin -- all 102 tasks are started at this point
...
end; -- wait here for all 102 tasks to end
When they are started, they will each execute a copy of the task body in parallel with everything else thats
happening. The tasks are local to the block, so they cease to exist at the end of the block. When the task executing
the block reaches the end of the block, it will have to wait for all 102 tasks to end before it can proceed. The task
executing the block is said to be the master of the tasks created within it, and the tasks within the block are known
as the dependents of their master task.
The task body will normally contain a loop. Heres a simple example:
This will display the message Hello! five times before terminating. After the message is displayed, the task will
be delayed for two seconds before continuing by the delay statement:
delay 2.0;
Here 2.0 is a value of the standard fixed point type Duration which specifies the length of the delay in seconds.
You can also delay until a particular time like this:
This statement will cause the task which executes it to wait until Christmas 1999. The time is specified as a value
of type Ada.Calendar.Time.
Task types are limited types, so you cant assign tasks to one another or compare them; this also means that any
type which contains a task as a component must also be a limited type. If you only want a single task of a
particular type, you can declare the task specification like this:
task Repeat;
The task type is now anonymous, and Repeat is the only object belonging to this anonymous type. In other words,
this is effectively like writing the following:
task type ???; -- ??? is the "name" of the anonymous task type
Repeat : ???; -- declare one object of this type
Tasks can also have discriminants, which can be a useful feature for providing initial values:
Notice that the discriminant is only specified in the task specification and not in the body, but it can still be
referred to from within the body. Also, since task types are limited types, access discriminants are perfectly
acceptable.
This task type provides an entry specification which another task can call just like a procedure; for example, a
task can ask Sheet to recalculate with an entry call like this:
Sheet.Recalculate;
The task body has to provide a way of servicing calls to its entries. This is done using an accept statement:
When the task body starts executing, it will wait at the accept statement until an entry call to Recalculate is made.
It will then call a procedure Do_Recalculation to perform the recalculation and go round the loop again to wait for
the next call to Recalculate. If another task calls Recalculate before the spreadsheet task has got back to the accept
statement again, the calling task is forced to wait. Thus the calling task and the one being called will wait for each
other until they are both ready, which is when the caller is waiting for its entry call to be accepted and the one
being called is waiting at the accept statement. This synchronisation of the two tasks is known as a rendezvous.
The task body above has one major problem; since its an infinite loop theres no way to get it to terminate, so the
master task wont be able to terminate either; itll end up waiting forever at the end of the block where the
spreadsheet task was declared. One way to get around this is to abort the task with an abort statement:
abort Sheet;
This will force the task and any dependent tasks it might have to terminate. However, this is rather drastic since the
spreadsheet might be halfway through a recalculation at the time. A better way would be to add another entry to
allow the master task to ask it to shut down in an orderly manner:
Now the task body needs to be able to respond to calls to either entry. Its no good accepting them one after the
other in a loop since this will force the entries to be called alternately. One solution would be to test if any calls are
pending before accepting them. You can do this using the 'Count attribute for an entry which gives the number of
pending calls for that entry:
However, this isnt particularly reliable. As youll see later, tasks can choose to abandon entry calls if they arent
responded to within a certain time period, and this means that even if Recalculate'Count is non-zero, by the time
you execute the accept statement for Recalculate the calling task might have timed out and abandoned its call, in
which case youll be stuck at the accept statement until some other task calls Recalculate. And if that never
happens, youll never be able to accept a call to Shutdown. The correct solution to this is to put the calls inside a
select statement:
or
accept Shutdown;
exit;
end select;
end loop;
end Spreadsheet_Task;
This select statement contains two accept alternatives which must each be headed by an accept statement. It will
wait until one of the entries named in the accept statements is called, and it will then execute the appropriate
alternative. If calls to both entries are already pending, one will be accepted non-deterministically. The select
statement ends after the chosen alternative has been executed; if Recalculate was called, the task will then go
around the loop again and wait for another entry call, but if Shutdown was called it will exit from the loop and
terminate. Note that the task will not respond to calls to Shutdown if a recalculation is in progress; it will only
respond when its waiting for an entry call in the select statement, which will happen after the call to Recalculate
finishes and the loop is repeated.
This solution requires the master task to call Shutdown explicitly when it wants to terminate the task. The
disadvantage with this approach is that its possible to forget to call Shutdown. A better solution is to add a
terminate alternative to the select statement:
The terminate alternative must be the last one in a select statement, and it cant contain anything except a
terminate statement like the one shown above. When the master task gets to the end of the block where the
spreadsheet was declared, the spreadsheet task will terminate the next time that the select statement is executed (or
immediately, if the task is already waiting in the select statement). This means that the master doesnt have to do
anything special to terminate the task, but it can still call Shutdown if it wants to terminate the task before the end
of the block where it was declared. Note that once a task has terminated, youll be rewarded with a Tasking_Error
exception if you try to call any of its entries.
Select statements can also be used for entry calls. If a calling task isnt prepared to wait for a rendezvous, it can
use a select statement with an else alternative like this:
select
Sheet.Recalculate;
else
Put_Line ("Sheet is busy -- giving up");
end select;
In this case, if Sheet is not able to accept the entry call to Recalculate immediately, the entry call will be
abandoned and the else alternative will be executed. If the calling task is willing to wait for a limited time, it can
use a select statement with a delay alternative like this:
select
Sheet.Recalculate;
or
delay 5.0;
Put_Line ("Sheet has been busy for 5 seconds -- giving up");
end select;
If the entry call is not accepted within the time specified in the delay statement (five seconds in this case), its
abandoned and the delay alternative is executed instead. A delay until statement can always be used instead of a
delay statement:
select
Sheet.Recalculate;
or
delay until Christmas;
Put_Line ("Sheet has been busy for ages -- giving up");
end select;
You can also set an upper limit on the time it takes to process an entry call:
select
delay 5.0;
Put_Line ("Sheet not recalculated yet -- recalculation abandoned");
then abort
Sheet.Recalculate;
end select;
This starts evaluating the statements between then abort and end select, which in this case is a call to
Sheet.Recalculate. If the delay specified by the delay (or delay until) statement after select expires before this
completes, the call to Sheet.Recalculate is aborted (as if by an abort statement) and the message Sheet not
recalculated yet -- recalculation abandoned will be displayed. You arent restricted to using this in connection
with multitasking; for example, you could use it to abort lengthy calculations where the total execution time is
important (or where the calculation might diverge to give potentially infinite execution times):
select
delay 5.0;
Put_Line ("Horribly long calculation abandoned");
then abort
Horribly_Long_And_Possibly_Divergent_Calculation;
end select;
A select statement inside a task body can also have an else alternative or one or more delay alternatives instead of
a terminate alternative. You must also have at least one accept alternative. An else alternative is activated if none
of the accept statements have a pending entry call; a delay alternative is activated if none of the accept statements
accept an entry call within the time specified in the delay statement. These three possibilities (else, delay and
terminate) are mutually exclusive; you cannot have a delay alternative as well as a terminate alternative, for
example.
The accept statement in this task body acts basically like a procedure which is invoked by the entry call. It can
even contain return statements just like a procedure. When the entry call is accepted, any in parameters are
transferred from the caller. The body of the accept statement is then executed, and at the end any out parameters
are transferred back to the caller. The rendezvous is then complete, and the caller is allowed to continue. In this
case the task will generate ever-increasing integer values each time Get is called.
You might not always be willing to accept an entry call. Consider this task which contains a stack which other
tasks can push data onto or pop items off:
An exception will be raised if an attempt is made to call Pop on an empty stack. Note that if an exception occurs
during a rendezvous, the exception will be raised in the calling task as well as the one being called. To prevent this
happening, we can add a guard to the accept statement for Pop like this:
A guarded accept statement can only be activated when the condition specified in the guard is True, which in this
case means that Pop can only be called when the stack is not empty. Any task which calls Pop when the stack is
empty will be forced to wait until another task calls Push. As soon as Push has been called, the stack will no longer
be empty so that the next time the select statement is executed the pending call to Pop will immediately be
accepted.
Guarded entries can also be useful for aborting actions in a select ... then abort construct. A select ... then abort
construct is governed by a triggering alternative (the first statement after select) which must be an entry call or a
delay statement. When the triggering alternative is activated (the entry call is accepted or the delay expires) the
abortable part between then abort and end select is aborted as if by an abort statement:
select
User.Interrupt;
Put_Line ("Horribly long calculation interrupted by user");
then abort
Horribly_Long_And_Possibly_Divergent_Calculation;
end select;
In this case, if the call to User.Interrupt (the Interrupt entry of the task User) is ever accepted, the horribly long
calculation will be aborted. If User.Interrupt has a guard, this means that when the guard condition becomes True
the horribly long calculation between then abort and end select will be aborted.
The stack could of course be declared in the same scope as the task types that need to access it, but this is
extremely risky. Consider the following section of code from chapter 13:
This shows how Pop is implemented using an array. In an environment where only one task is executing this code,
its perfectly safe. If more than one task is executing it simultaneously, both tasks might execute statement 1 at the
same time so that they will both be given a copy of the same item. When they execute statement 2, Stack.Top
might be decremented twice or both tasks might retrieve the same value for Stack.Top, subtract 1 from it and then
store the result in Stack.Top so that Stack.Top will appear to have been decremented only once.
In other words, the result will be completely unpredictable since it depends on the precise timing relationship
between the two tasks. Unpredictability on this scale is rarely a good property for computer systems to have. The
moral of the story is that tasks should never access external data; they should only ever access their own local
objects.
To get around this problem, Ada allows data to be encapsulated in a protected record which guarantees that this
sort of situation cant arise. A protected record is a passive data type rather than an active type like a task, so the
costs of a rendezvous and the scheduling of an extra task are avoided. Protected records are divided into a
specification and a body, just like tasks. The specification contains a visible part which declares a set of functions,
procedures and entries that tasks are allowed to call as well as a private part which contains the data to be
protected. Heres a protected record which encapsulates a stack of integers:
As with tasks, you can declare a single protected record of an anonymous type by leaving out the word type:
The body provides the implementations of the functions, procedures and entries declared in the specification. The
difference between the three types of operation is that functions are only allowed to read the values of private data
items; such items appear to a function as if they were constants and the function is unable to alter them. Since its
safe for several tasks to read the same data at the same time, multiple tasks are allowed to execute functions in a
protected object at the same time. Procedures and entries are allowed to alter the private data, so a task cant call
any protected operations while another task is executing a procedure or entry call. The difference between a
procedure and an entry is that entries have guards which act like the guards on accept statements; an entry can
only be executed when its guard is True, and any task which calls an entry whose guard is False will be suspended
until the guard becomes True (at which point the entry call can then be executed).
In the protected type Shared_Stack_Type, there are three functions (Top, Size and Empty) which dont affect the
private stack it contains. Tasks will be able to call these functions as long as no procedure or entry call is in
progress; if there is already a procedure or entry call in progress, the task calling the function will not be allowed to
proceed until the active call finishes executing. There is one procedure (Push); any task calling Push will have to
wait until any other active calls have finished executing. There is one entry (Pop); any task calling Pop will have to
wait, not only until any other active calls have finished executing, but also until the entry guard is True.
Heres the protected body. The guard condition for the entry Pop is specified after the parameter list between when
and is:
end Shared_Stack_Type;
So, as many tasks as want to can simultaneously inspect the top item on the stack, find out the size of the stack or
test if its empty as long as no-ones pushing an item onto the stack or popping one off it. Popping an item off the
stack is only allowed if the stack isnt empty; if it is empty the caller task will have to wait until another task calls
Push. Calls to Push and Pop will only go ahead when the protected record isnt in use by any other task.
This task has an access discriminant which will be set to point to the spreadsheet containing the task. The body of
this task will look like this:
All this does is to sit in a loop accepting entry calls to Get or Stop or delaying until the update time is reached.
Note that a delay statement which said delay 5.0 wouldnt be any good; the delay would then be five seconds
plus the time it took to get around the loop and back to the delay statement again; although the time it takes to get
around the loop may be very small it will gradually accumulate. This would be unacceptable in a time-critical
application.
Get returns the current counter value and Stop terminates the task. If neither of these is called before the update
time is reached, the delay will expire with the result that the update time and the count are both updated and the
spreadsheet is notified that a change has taken place. A modular type is used for Count so that when it reaches its
maximum value it will go back to zero rather than raising a Constraint_Error.
A problem with this is that calling Change will update an unprotected data item (the Dirty flag in the spreadsheet);
we could derive a new type of spreadsheet which incorporates a protected record to get around this if its a
problem:
end Shared_Flag_Type;
procedure Set is
begin
State_Flag := True;
end Set;
procedure Clear is
begin
State_Flag := False;
end Clear;
end Shared_Flag_Type;
type Active_Spreadsheet_Type is
abstract new Spreadsheet_Type with
record
Modified : Shared_Flag_Type;
end record;
The primitive operations Change, Updated and Changed will need overriding to use the operations of
Shared_Flag_Type:
The next thing we need is a derived Cell_Type to hold an instance of the counter task:
record
Counter : Counter_Task(Sheet);
end record;
Since the parent type Cell_Type is derived from Limited_Controlled, we can override Finalize to stop the counter
task:
The primitive operations Contents, Cell_Value and Num_Value and Evalute which were inherited from Cell_Type
will all need to be overridden. Contents can be defined to return a string identifying the cell as a five-second
counter:
Text_Value can be implemented by returning the current value of the cell as a string. The current value can be got
by calling Num_Value:
Num_Value needs to rendezvous with the task to get the current value of the counter:
Evaluate just needs to set the cell state to Defined since the value of a counting cell is always well-defined:
end Evaluate;
Finally, a constructor function is needed, just like the constructors for String_Cell_Type and Formula_Cell_Type:
The view package will also need some modification. Obviously the declaration of Sheet_Type must be changed to
use an Active_Spreadsheet_Type instead of an ordinary Spreadsheet_Type. If youre waiting for a command and
the spreadsheet gets updated, youll need to redisplay the spreadsheet. One way to do this is to supply the
spreadsheet as a parameter to Next_Command and get Next_Command to redisplay it if its been updated. You can
use the procedure Get_Immediate from Ada.Text_IO to do this:
This procedure doesnt wait for a key to be pressed; if a key has been pressed it returns it in Item and sets
Available to True, but if not it just returns immediately with Available set to False. Heres how it could be used:
As long as no key is pressed, the inner loop will keep checking the state of the spreadsheet and redisplay it if
necessary, but as soon as a key is pressed itll cause the inner loop to exit and command processing will then be
done as normal.
This isnt an ideal solution; probably the best thing would be to put the spreadsheet inside a task in the view
package and get the task to monitor the spreadsheet for changes and redraw it whenever necessary, rather than
depending on Next_Command to do all the work. However, this depends on your ability to write to particular
places on the screen without affecting the input cursor, so this would be a very system-dependent solution.
The final step is to change Modify so that it can create counting cells. Ill use the character # to create counting
cells:
Apart from anything else, this shows how easy it can be to modify the existing spreadsheet. The object-oriented
approach accommodates new types of cells since the spreadsheet can handle any type derived from Cell_Type, so
as long as the services provided by Cell_Type are adequate we can override them to provide whatever behaviour
we want without affecting the spreadsheet itself; similarly the spreadsheet itself can be modified by overriding any
operations whose implementation needs to change. In this case Ive added a protected record; since all that its
used for is to protect a single Boolean variable its unlikely to make any practical difference, but it shows how
important it is that the original spreadsheet provided a set of primitive operations for manipulating its own internal
state rather than assuming that the internal state would always be managed in the same way.
Exercises
19.1 Modify Counting_Cell_Type so that you can specify the delay rather than having a fixed five-second delay.
19.2 Modify the spreadsheet program in this chapter to use an abortable select statement instead of calling
Get_Immediate to get commands from the user. The select statement should call Get or Get_Line and abort
the call if it has not completed within one second, redisplaying the spreadsheet if it has changed.
19.3 Modify the guessing game program from exercise 5.1 to impose a maximum time limit within which the user
must guess the secret value.
19.4 Define a bank account type similar to that described in exercise 14.2 which is based on a protected record to
make it safe for use in a multitasking program (so that multiple tasks can simultaneously deposit and
withdraw money). Test it using two tasks which deposit and withdraw amounts of money at random
intervals; check that the totals deposited and withdrawn by each task match up with the final balance of the
account.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent
files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Chapter 20:
Loose ends
I thought at first that you had done something clever,
but I see there was nothing in it, after all.
Sir Arthur Conan Doyle, The Red-Headed League
The goto statement. Ever since the 1960s the goto statement has been denounced as the root of
all unstructured evil in programming, the source from which spaghetti code is created. Its so
bad I wont even discuss it here. Suffice it to say that three decades after this heresy was exposed,
Ada still has a goto statement. Its really not something you want to know about.
Pragmas and representation clauses. Pragmas are clauses which provide hints to the compiler
about possible optimisations and other compilation-related issues. Compilers are allowed to
ignore pragmas or to add extra pragmas to the ones recommended by the language definition, so
Ive avoided mentioning them on the grounds that theyre implementation dependent.
Representation clauses allow you to control details of the internal representation of parts of your
program such as the physical memory layout of record components and the physical location of
individual objects. This again is such an implementation-dependent issue that Ive ignored it
entirely.
The standard packages. These havent been described in full simply because of lack of space;
there are a lot of them, and it would be very difficult to think up examples to illustrate everything
in them. You should nevertheless make the effort to familiarise yourself with whats available if
you want to be a serious Ada programmer; the standard environment is described in detail in
Annex A of the Ada 95 Reference Manual. Unless youre familiar with whats available youll
end up wasting time reinventing the wheel; for example, I used the To_Upper function from
Ada.Characters.Handling in a couple of examples rather than write an upper case conversion
function. It would have been easy enough to write one, but Ive got better things to do with my
time. Also, To_Upper deals correctly with accented and other non-English characters like , and
whereas I might easily have overlooked that sort of detail. There are also packages for direct
access input/output, command line processing, variable-length string handling, character-to-
character translation, random number generation, mathematical functions, complex numbers and
more. Some day youre likely to need to use at least some of these things, so you need at least to
know that they exist.
Loop labels and multilevel exit statements. These are a lesser heresy than goto statements; they
enable you to exit from multiple levels of loop at once (in a single bound Jack was free!).
Although some people find them acceptable, my experience is that there is usually a better way to
do the same thing; a lot of the time its better to consider returning from a subprogram, or possibly
raising an exception if the reason you want to get out is that an error has occurred.
Variant records. Tagged types can generally be used more easily instead of variant records to
achieve the same effect. Variant records are a feature inherited (ahem!) from Ada 83, which didnt
have tagged types. If you ever feel the urge to use a variant record, think about using a family of
tagged types instead.
Decimal types. These are new in Ada 95 but compilers are not required to support them. For this
reason I didn't feel that it was worth spending very much time on them.
Multitasking. Although I devoted the previous chapter to this, its a complex enough topic that
its impossible to do more than to scratch the surface of it in a book this size. Some features (e.g
the requeue statement and entry families) weren't mentioned at all. Rather than aim for complete
coverage, I felt it would be better to concentrate on a brief description of the essential features of
multitasking in Ada 95 and content myself with illustrating its use and interaction with object-
oriented design.
A lot of documentation about Ada is freely available, usually electronically via the Internet (either on the
World-Wide Web or by FTP). This includes the Ada 95 Reference Manual and the accompanying
Rationale as well as the Annotated Reference Manual and Style Guide. Another useful document is the
Ada Programming FAQ (Frequently Asked Questions list). A lot of software is also available in the same
way, including the GNAT compiler system (which is available for most development platforms) and a
variety of other development tools, as well as an ever-growing library of useful packages.
Heres a list of good places to start looking if youre interested in online resources:
AdaPower (http://www.adapower.com/) a website with links to most of the other sites listed
here as well as many other places. This is your best starting point for any web browsing.
Paper copies of some documents can be had for the asking, and most online resources are available on
CD if you dont have convenient access to the Internet. Here are some sources:
Ada 95 Reference Manual and Rationale paper copies of these are available on request from
the National Technical Information Service at 5285 Port Royal Road, Springfield, VA 22151,
USA. Youll need to quote the correct order number for the document you want: for the Ada 95
Reference Manual its AD A293760, for the Rationale its AD A293708 and for the Annotated
Reference Manual its AD A298367.
ASE is a collection of resources for Ada and Software Engineering which is available as a set
of (currently) two CDs produced by Richard Conn
BURKS is a collection of documentation and Windows software intended for use by students
which includes material on Ada. Produced on a non-profit basis by the author of this book,
it is available as a set of (currently) four CDs or as a single DVD.
comp.lang.ada if you have problems understanding some of the trickier bits of Ada or
you want information about getting hold of particular resources, this is a good place to ask.
However, youre advised to read the Ada FAQ (see above) and think carefully before you
post in accordance with the standard netiquette; what you write will be seen by people all
There are also a number of Ada-related special interest groups around the world:
ACM SIGAda a special interest group of the Association for Computing Machinery.
SIGAda publishes a quarterly journal called SIGAda Notes. There is also a SIGAda website
at http://www.acm.org/sigada/. For more information contact the ACM at 1515 Broadway,
New York, NY 10036, USA, or at Avenue Marcel Thiry 204, 1200 Brussels, Belgium, or by e-
mail to [email protected].
Ada-UK a British Ada special interest group. Ada-UK publishes a journal called Ada
User in collaboration with Ada-Europe. There is a website at http://www.adauk.org.uk/. For
more information contact Ada-UK at PO Box 322, York YO1 3GY, England.
Ada-Europe a European Ada special interest group which hosts an annual conference
and publishes a quarterly journal called Ada-Europe News. Ada-Europe can be contacted at
ATM Computer GmbH, Bcklestrasse 15, D-78467 Konstanz, Germany, or online at
http://www.ada-europe.org/.
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all
the constituent files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Appendices
A. Language summary
B. Selected standard packages
C. Language-defined attributes
D. Package Listings
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the
constituent files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
This appendix gives an informal summary of the syntax of Ada 95. Ive deliberately tried to keep it informal to make it easier
to understand at a glance, but because of its informality it is incomplete and not everything is spelled out in full. You should
consult the Ada 95 Reference Manual if you need a more detailed syntax of the language.
In the descriptions below, items in italics refer to syntactic categories, items in bold must be entered as shown and items
enclosed in square brackets [like this] are optional and may be omitted. Unless otherwise noted, sequence-of-X means one or
more Xs (so sequence-of-statements means one or more statements) while list-of-X means one or more Xs separated by
commas (so list-of-names means one or more names separated by commas).
[sequence-of-context-clauses]
[private] package-or-subprogram-declaration
[sequence-of-context-clauses]
separate ( parent-unit-name )
body-declaration
The body-declaration can be the body of a subprogram, package, task or protected record.
with list-of-library-units ;
use list-of-packages ; -- packages in use clauses can't be generic
use type list-of-types ;
A parameter-list consists of one or more parameter declarations separated by semicolons. Each parameter declaration looks
like this:
For a procedure, mode is either in, out, in out or access. For a function, mode is either in or access. If it is omitted, in is
assumed.
package name is
sequence-of-declarations
[private
sequence-of-declarations]
end name ;
A.2 Statements
A.2.1 The null statement (chapter 3)
null;
variable-name := expression ;
procedure-name [( list-of-parameters )] ;
Named parameters must come after any parameters which do not specify a name.
if condition then
sequence-of-statements
[sequence-of-elsif-parts]
[else
sequence-of-statements]
end if;
case expression is
sequence-of-alternatives
end case;
A choice-list is a list of one or more choices separated by vertical bars (|). Choices can take either of the following forms:
The expressions must be able to be evaluated at compile time. There must be a choice for every possible value of the type of
the controlling expression (or an others choice).
If a loop-name is specified at the beginning of the loop, it must also be repeated after end loop.
[loop-name :]
while condition loop -- repeat while condition is true
sequence-of-statements
end loop [loop-name] ;
[loop-name :]
for name in [reverse] subtype-specification loop
sequence-of-statements
end loop [loop-name] ;
[declare
sequence-of-declarations]
begin
sequence-of-statements
[exception
sequence-of-exception-handlers]
end;
A.3 Declarations
A.3.1 Object declarations (chapter 2, chapter 3)
The initial-value is any expression of the appropriate type and is assigned to all the variables defined in the list-of-names.
The numeric-value is any expression with a real or integer value which must be static (i.e. it must be able to be evaluated at
compile time). The list-of-names will be defined as universal values (universal real or universal integer).
A sequence-of-component-declarations which does not contain any components must be specified as null.
A component-declaration looks like an object declaration (but may not be a constant or an anonymous array).
A discriminant-part has the same format as the parameter list in a subprogram declaration except that the mode must be
either access or omitted, and the type of a discriminant must be either a discrete type or an access type.
type name ;
type name ( discriminant-part );
type name (<>);
A.5 Exceptions
A.5.1 Exception handlers (chapter 3, chapter 7)
The exception-list is a list of one or more exception names separated by bars (|). If there is an others choice it must come
last. If there is a name specified, it declares an object of the type Ada.Exceptions.Exception_Occurrence which is initialised
list-of-names : exception;
raise [exception-name] ;
The exception-name can only be omitted inside an exception handler, in which case the original exception is reraised.
A.6 Expressions
A.6.1 Expressions (chapter 2, chapter 3)
An expression is a sequence of one or more terms separated by operators. The operators are applied to the operands (highest-
priority operators first) to yield a value of a specific type.
Terms within an expression can be object names or literal values, or can take any of the following forms:
An array aggregate is a comma-separated list of one or more values for specifying the components of an array. Values in an
array aggregate take the following form:
A component-selector-list consists of one or more component selectors separated by vertical bars (|). Component selectors
are as follows:
expression
expression .. expression
others -- must be used by itself as the last selector
A record aggregate is either a comma-separated list of one or more values for specifying the components of a record, or null
record. Values in an aggregate other than a null record take the following form:
A component-selector-list consists of one or more component names separated by vertical bars (|).
A.7 Generics
A.7.1 Generic units (chapter 12)
generic
[sequence-of-generic-parameters]
subprogram-or-package-specification
accept entry-name ;
select
sequence-of-accept-alternatives
[or
delay-or-terminate-alternatives]
[else
sequence-of-statements]
end select;
You cannot have both a delay-or-terminate-alternative and an else part. The sequence-of-accept-alternatives consists of one
or more accept-alternatives separated from each other by or. The delay-or-terminate-alternatives consist of either a single
terminate alternative, or one or more delay alternatives separated by or.
delay-statement
[sequence-of-statements]
A select statement may not contain more than one terminate alternative.
select
entry-call
[sequence-of-statements]
[or
delay-alternative]
[else
sequence-of-statements]
end select;
You can have either a delay-alternative or an else part, but not both.
select
entry-call-or-delay-statement
[sequence-of-statements]
then abort
sequence-of-statements
end select;
abort list-of-task-names ;
sequence-of-subprogram-or-entry-bodies
end name ;
Composite types
- array types
- record types
- untagged records
- tagged records
- tasks
- protected records
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
This appendix gives the specifications of the standard Ada library packages used in this book. For full details of the
packages available, refer to Annex A of the Ada 95 Language Reference Manual, which is where the packages described in
this appendix were copied from (with minor formatting changes). The following copyright notice applies:
Copyright 1992, 1993, 1994, 1995 Intermetrics, Inc. This copyright is assigned to the U.S. Government. All rights
reserved. This document may be copied, in whole or in part, in any form or by any means, as is or with alterations, provided
that (1) alterations are clearly marked as alterations and (2) this copyright notice is included unmodified in any copy.
Compiled copies of standard library units and examples need not contain this copyright notice so long as the notice is
included in all copies of source code and documentation.
In addition to the packages listed here, chapter 9 gives the declaration of the package Ada.Calendar (section 9.10).
Asynchronous_Task_Control Streams
Calendar Stream_IO
Characters Strings
Handling Bounded
Latin_1 Fixed
Command_Line Maps
Decimal Constants
Direct_IO Unbounded
Dynamic_Priorities Wide_Bounded
Exceptions Wide_Fixed
Finalisation Wide_Maps
Interrupts Wide_Constants
Names Wide_Unbounded
IO_Exceptions Synchronous_Task_Control
Numerics Tags
Complex_Elementary_Functions Task_Attributes
Complex_Types Task_Identification
Discrete_Random Text_IO
Elementary_Functions Complex_IO
Float_Random Editing
Generic_Complex_Elementary_Funct Text_Streamsions
Generic_Complex_Types Unchecked_Conversion
Generic_Elementary_Functions Unchecked_Deallocation
Real_Time Wide_Text_IO
Sequential_IO Complex_IO
Storage_IO Editing
Text_Streams
package Standard is
pragma Pure(Standard);
type Character is
(nul, soh, stx, etx, eot, enq, ack, bel,
bs, ht, lf, vt, ff, cr, so, si,
dle, dc1, dc2, dc3, dc4, nak, syn, etb,
can, em, sub, esc, fs, gs, rs, us,
Constraint_Error : exception;
Program_Error : exception;
Storage_Error : exception;
Tasking_Error : exception;
end Standard;
with Ada.IO_Exceptions;
package Ada.Text_IO is
type File_Type is limited private;
type File_Mode is (In_File, Out_File, Append_File);
-- File Management
procedure Create (File : in out File_Type;
Mode : in File_Mode := Out_File;
Name : in String := "";
Form : in String := "");
-- Buffer control
procedure Flush (File : in out File_Type);
procedure Flush;
-- Character Input-Output
procedure Get (File : in File_Type; Item : out Character);
procedure Get (Item : out Character);
-- String Input-Output
procedure Get (File : in File_Type; Item : out String);
procedure Get (Item : out String);
generic
type Num is mod <>;
package Modular_IO is
Default_Width : Field := Num'Width;
Default_Base : Number_Base := 10;
package Float_IO is
Default_Fore : Field := 2;
Default_Aft : Field := Num'Digits-1;
Default_Exp : Field := 3;
generic
type Num is delta <>;
package Fixed_IO is
Default_Fore : Field := Num'Fore;
Default_Aft : Field := Num'Aft;
Default_Exp : Field := 0;
generic
type Num is delta <> digits <>;
package Decimal_IO is
Default_Fore : Field := Num'Fore;
Default_Aft : Field := Num'Aft;
Default_Exp : Field := 0;
-- Exceptions
Status_Error : exception renames IO_Exceptions.Status_Error;
Mode_Error : exception renames IO_Exceptions.Mode_Error;
Name_Error : exception renames IO_Exceptions.Name_Error;
Use_Error : exception renames IO_Exceptions.Use_Error;
Device_Error : exception renames IO_Exceptions.Device_Error;
End_Error : exception renames IO_Exceptions.End_Error;
Data_Error : exception renames IO_Exceptions.Data_Error;
Layout_Error : exception renames IO_Exceptions.Layout_Error;
private
... -- not specified by the language
end Ada.Text_IO;
with Ada.IO_Exceptions;
generic
type Element_Type(<>) is private;
package Ada.Sequential_IO is
type File_Type is limited private;
type File_Mode is (In_File, Out_File, Append_File);
-- File management
-- Exceptions
private
... -- not specified by the language
end Ada.Sequential_IO;
with Ada.IO_Exceptions;
package Ada.Streams.Stream_IO is
type Stream_Access is access all Root_Stream_Type'Class;
-- Exceptions
private
... -- not specified by the language
end Ada.Streams.Stream_IO;
package Ada.Characters.Handling is
pragma Preelaborate(Handling);
end Ada.Characters.Handling;
package Ada.Characters.Latin_1 is
pragma Pure(Latin_1);
-- Control characters:
Character'Val(229);
LC_AE_Diphthong : constant Character := ''; --
Character'Val(230);
LC_C_Cedilla : constant Character := ''; --
Character'Val(231);
LC_E_Grave : constant Character := ''; --
Character'Val(232);
LC_E_Acute : constant Character := ''; --
Character'Val(233);
LC_E_Circumflex : constant Character := ''; --
Character'Val(234);
LC_E_Diaeresis : constant Character := ''; --
Character'Val(235);
LC_I_Grave : constant Character := ''; --
Character'Val(236);
LC_I_Acute : constant Character := ''; --
Character'Val(237);
LC_I_Circumflex : constant Character := ''; --
Character'Val(238);
LC_I_Diaeresis : constant Character := ''; --
Character'Val(239);
end Ada.Characters.Latin_1;
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are
distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
This appendix is fairly lax with its terminology. In particular I use the term type instead of subtype for
simplicity, but this is not entirely accurate, particularly for floating point types. Treat this as an outline
guide, and see annex K of the Ada 95 Reference Manual for the full story. The language-defined
attributes are as follows:
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the
constituent files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
This appendix gives the final versions of the packages developed in this book. There are minor differences between the forms
of the packages shown here and those in the main text; apart from some changes in layout, all with clauses are now shown in
full and use and use type clauses are sometimes placed differently. In some cases, use clauses that were assumed for the sake
of clarity of exposition in the main text are omitted in favour of fully qualified names. These changes do not affect the meaning
or the behaviour of the code.
D.1 JE
(See chapter 4)
package JE is
-- an empty package!
end JE;
D.2 JE.Appointments
(See chapters 10, 14 and 15)
with JE.Times;
use JE.Times;
package JE.Appointments is
end JE.Appointments;
D.3 JE.Appointments.Meetings
package JE.Appointments.Meetings is
subtype Room_Type is Integer range 100 .. 999;
type Meeting_Type is abstract new Appointment_Type with private;
private
type Meeting_Type is abstract new Appointment_Type with
record
Room : Room_Type;
end record;
end JE.Appointments.Meetings;
end JE.Appointments.Meetings;
D.4 JE.Appointments.Deadlines
(See chapter 15)
package JE.Appointments.Deadlines is
type Deadline_Type is abstract new Appointment_Type with null record;
D.5 JE.Diaries
(See chapters 10, 11, 12 and 15)
Diary_Error : exception;
private
type Appointment_Access is access Appointment_Type'Class;
package Lists is new JE.Lists (Item_Type => Appointment_Access);
type Diary_Type is
limited record
List : Lists.List_Type;
end record;
end JE.Diaries;
Ada.Streams.Stream_IO.Close (File);
end Save;
exception
when Ada.Streams.Stream_IO.Name_Error =>
raise Diary_Error;
end Load;
end JE.Diaries;
D.6 JE.Expressions
(See chapter 17)
with JE.Pointers;
package JE.Expressions is
Syntax_Error : exception;
private
type Priority_Type is range 0..9;
subtype Operator_Priority_Type is Priority_Type range 1..Priority_Type'Last;
record;
function Apply (Operator : Variadic_Operator_Type;
Right : Integer) return Integer is abstract;
type Multiplying_Operator_Type is abstract new Binary_Operator_Type with null
record;
end JE.Expressions;
begin
Next_Token (Expression_Type'Class(Syntax), Expr, From, Next);
else
Ada.Exceptions.Raise_Exception (Syntax_Error'Identity, "Illegal
token");
end if;
end Get_Operand;
end JE.Expressions;
D.7 JE.Expressions.Spreadsheet
(See chapter 18)
with JE.Spreadsheets;
use JE.Spreadsheets;
package JE.Expressions.Spreadsheet is
type Formula_Type (Sheet : access Spreadsheet_Type'Class) is
new Expression_Type with private;
private
type Cell_Operand_Type (Cell : Cell_Access) is new Operand_Type with null
record;
with JE.Spreadsheets;
use JE.Spreadsheets;
package body JE.Expressions.Spreadsheet is
use JE.Expressions.Token_Pointers;
end JE.Expressions.Spreadsheet;
D.8 JE.Lists
(See chapters 12 and 16)
with Ada.Finalization;
use Ada.Finalization;
generic
type Item_Type is private;
package JE.Lists is
type List_Type is new Limited_Controlled with private;
type List_Iterator is private;
List_Error : exception;
private
type Item_Record;
type Item_Access is access Item_Record;
type Item_Record is
record
Item : Item_Type;
Next : Item_Access;
Pred : Item_Access;
end record;
type List_Header is
record
First : Item_Access;
Last : Item_Access;
Count : Natural := 0;
end record;
type List_Access is access List_Header;
type List_Iterator is
record
List : List_Access;
Current : Item_Access;
end record;
end JE.Lists;
with Ada.Unchecked_Deallocation;
package body JE.Lists is
procedure Delete_Item is new Ada.Unchecked_Deallocation (Item_Record,
Item_Access);
else
return Iterator.Current.Item;
end if;
end Value;
Iterator.List.Count := Iterator.List.Count + 1;
end if;
end Insert;
end JE.Lists;
D.9 JE.Menus
(See chapter 12)
with JE.Lists;
generic
package JE.Menus is
type Action_Type is access procedure;
type Menu_Type is limited private;
private
type Menu_Item_Type is
record
Title : String (1..40);
Length : Natural;
Choice : Character;
Action : Action_Type;
end record;
type Menu_Type is
limited record
Menu_List : Menu_Lists.List_Type;
end record;
end JE.Menus;
D.10 JE.Pointers
(See chapter 16)
with Ada.Finalization;
generic
type Item_Type(<>) is limited private;
type Access_Type is access Item_Type;
package JE.Pointers is
type Pointer_Type is private;
function Pointer (Value : Access_Type) return Pointer_Type;
function Value (Pointer : Pointer_Type) return Access_Type;
private
type Reference_Counted_Object is
record
Value : Access_Type;
Count : Natural;
end record;
type Reference_Counted_Pointer is access Reference_Counted_Object;
with Ada.Unchecked_Deallocation;
package body JE.Pointers is
procedure Delete_Item is
new Ada.Unchecked_Deallocation (Item_Type, Access_Type);
procedure Delete_Pointer is
new Ada.Unchecked_Deallocation (Reference_Counted_Object,
Reference_Counted_Pointer);
end JE.Pointers;
D.11 JE.Spreadsheets
(See chapter 18)
Cell_Name_Length : constant := 6;
Circularity_Error : exception;
Undefined_Cell_Error : exception;
private
type Cell_Record is
record
Where : String (1..Cell_Name_Length);
Size : Cell_Size;
Cell : Cell_Pointers.Pointer_Type;
end record;
type Spreadsheet_Type is
abstract tagged limited record
Cells : Cell_Lists.List_Type;
Dirty : Boolean := False;
Eval : Evaluation_Number := Evaluation_Number'First;
end record;
end JE.Spreadsheets;
New_Cell.Size := Integer'Min(Cell_Name_Length,Where'Length);
New_Cell.Where (1..New_Cell.Size) :=
Where (Where'First .. Where'First+New_Cell.Size-1);
New_Cell.Cell := Pointer(What);
Cell_Lists.Insert (Last(Sheet.Cells), New_Cell);
end if;
Change (Spreadsheet_Type'Class(Sheet));
end Insert;
Cell.Eval := Evaluation(Cell.Sheet.all);
Cell.State := Evaluating;
Cell.Value := Evaluate (Expr, Cell.Text);
Cell.State := Defined;
end if;
exception
when Undefined_Cell_Error =>
if Cell.State /= Error then
Cell.State := Undefined;
end if;
when Fault : Circularity_Error | JE.Expressions.Syntax_Error |
Constraint_Error =>
Cell.State := Error;
Handle_Error (Cell.Sheet, Fault);
end Evaluate;
end JE.Spreadsheets;
D.12 JE.Spreadsheets.Active
(See chapter 19)
package JE.Spreadsheets.Active is
use JE.Spreadsheets;
type Active_Spreadsheet_Type is abstract new Spreadsheet_Type with private;
private
protected type Shared_Flag_Type is
function State return Boolean;
procedure Set;
procedure Clear;
private
State_Flag : Boolean := False;
end Shared_Flag_Type;
end JE.Spreadsheets.Active;
with Ada.Calendar;
package body JE.Spreadsheets.Active is
use type Ada.Calendar.Time; -- to allow use of "+"
select
accept Get (Value : out Integer) do
Value := Integer(Count);
end Get;
or
accept Stop;
exit;
or
delay until Update_Time;
Update_Time := Update_Time + 5.0;
Count := Count + 1;
Change (Sheet.all);
end select;
end loop;
end Counter_Task;
procedure Set is
begin
State_Flag := True;
end Set;
procedure Clear is
begin
State_Flag := False;
end Clear;
end Shared_Flag_Type;
D.13 JE.Stacks
(See chapter 13)
with JE.Lists;
generic
type Item_Type is private;
package JE.Stacks is
type Stack_Type is limited private;
private
package Lists is new JE.Lists (Item_Type);
type Stack_Item;
type Stack_Type is access Stack_Item;
end JE.Stacks;
end JE.Stacks;
D.14 JE.Times
(See chapter 9)
with Ada.Calendar;
package JE.Times is
subtype Time_Type is Ada.Calendar.Time;
end JE.Times;
This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are
distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $
Glossary
(Note: Some of the terms included here are technical terms which Ive defined loosely and informally.
Such definitions are for your guidance only, and are not necessarily complete or entirely accurate.)
Abstract operation:
An operation for which no implementation can be provided. Only abstract types can have abstract
operations. Derived concrete types must override any inherited abstract operations.
Abstract type:
A type declared using the reserved word abstract. Only abstract types can have abstract
operations. You cannot declare objects belonging to an abstract type; the purpose of an abstract
type is to act as the parent for a class of derived types.
Access discriminant:
A discriminant whose type is an anonymous general access type. Access discriminants can only
be given for limited types; the values supplied for them cannot be null and they cannot be copied
or altered.
Access parameter:
A form of input parameter which is an anonymous general access type. Access parameters
cannot be null and they cannot be copied or altered.
Access type:
A type whose values are access values. Pool-specific access types can only refer to objects
created using new, while general access types can also refer to aliased objects.
Access value:
A value which refers to (points to) other objects defined elsewhere, commonly referred to as a
pointer.
Accessor:
A function which accesses an internal component of a private type.
Actual parameter:
The parameter value supplied in a subprogram call (often referred to as an argument) or a
generic instantiation.
ADT:
An acronym for abstract data type.
Algorithm:
A method for solving a particular problem which is guaranteed to terminate in a finite time.
Aliased object:
An object declared using the reserved word aliased which can be accessed by a general access
value, so that the same object might be referred to by more than one alias.
Anonymous type:
A type whose name is not known so that objects of that type cannot be declared explicitly.
Argument:
Often used as a synonym for parameter. Technically speaking, arguments are the actual
parameters supplied when calling a subprogram (or instantiating a generic unit) to match the
formal parameters given in its specification.
Array:
A collection of objects of the same type which can be selected by an index belonging to the index
subtype of the array.
Attribute:
A characteristic of a type which can be used to access various details of the type and its
implementation.
Block:
A section of a program incorporating an optional set of declarations valid within the block, a set
of statements defining the processing to be carried out, and an optional set of exception
handlers.
Body:
The portion of a package, subprogram, task or protected record which contains the statements
which define its implementation.
Bottom Up:
The opposite of top down; an approach which builds simple low-level objects into more complex
higher-level objects.
Bug:
An error in a program.
Call:
The invocation of a subprogram which causes in parameters to be transferred into the
subprogram, followed by execution of the subprogram body and the transfer of out parameters
back to the surrounding environment.
Child:
A package or subprogram which acts as an extension of a parent package, thus allowing
packages to be extended without modifying the original specifications.
Class:
A family of derived types with a common parent type.
Class-wide type:
A type which can refer to an object of any type within a class. Class-wide types are created by
applying the 'Class attribute to a tagged type.
Client:
A program unit which makes use of the services provided by a particular package.
Compilation unit:
A portion of a program which can be submitted to the compiler independently; a package
specification or body, a subprogram body or a generic instantiation.
Compiler:
A program which translates the source code for a library unit into an object module in a
program library.
Component:
An element of a composite type.
Composite type:
An array or record type which represents a collection of smaller components.
Concrete type:
A non-abstract type (see abstract type).
Constant:
An object whose value cannot be changed.
Constrained type:
A type whose constraints have all been specified so that the compiler knows exactly how it
should be represented in memory.
Constraint:
A discriminant for a record or a range for a subtype or array index subtype which usually
determines the amount of memory the compiler must allocate for objects of the type.
Constructor:
A subprogram which constructs a value of a private type from its component parts.
Container:
A type which contains a number of simpler elements. Array types are provided as a built-in
container type in Ada.
Context clause:
A with or use clause at the start of a compilation unit which specifies the context within which a
program unit should be compiled (i.e. which names should be recognised as being valid).
Controlled type:
A type which provides for user-defined initialisation and finalisation procedures.
Controlling operand:
A parameter of a primitive operation of a tagged type (or a result in the case of a function) which
belongs to that tagged type.
Coupling:
A linkage between two parts of a program such that if one part of the program is modified, the
behaviour of the other part may also be affected. This can lead to maintenance problems.
Crash:
What happens when something goes wrong and your program (or worse, its operating
environment) ceases to function.
Dangling pointer:
A pointer (access value) which points to an object that no longer exists.
Debugger:
A tool for tracing program execution to help track down bugs.
Debugging:
Declaration:
The definition of a name and its meaning.
Dependents:
For a particular unit of a program, all those parts of the program which depend on it (e.g. via a
with clause) and which will need recompiling as a result of any changes.
Derivation:
The definition of a new type in terms of an existing one.
Derived type:
A type created from an existing parent type which inherits the primitive operations of its parent.
Objects of a derived type can always be converted to a parent type and vice versa.
Discrete type:
An integer or enumeration type.
Discriminant:
A parameter to a record type whose value may be used in array index constraints for record
components, as an initial value for components, or to select between record variants.
Dispatching:
The selection of the correct primitive operation to be executed based on the actual tag of a class-
wide value.
Entry:
The means by which one task can request a service from another task or from a protected
record.
Enumeration type:
A type whose possible values are enumerated (i.e. listed) as part of its declaration.
Exception:
An error indication which can be raised when an error is detected and which can be handled by
an exception handler elsewhere in the program.
Exception handler:
A section of code at the end of a block which defines the recovery actions to be performed in
response to exceptions raised within that block.
Executable program:
A subprogram which has been compiled into the program library and then linked with any other
library units it depends on.
Expression:
Something which can be evaluated to produce a value which can be stored or otherwise
processed.
Extensibility:
The ability to add new features to an existing program without disturbing any existing code.
Field:
A synonym for component.
Finalisation:
The actions which take place immediately before an object is destroyed.
Fixed point:
A variety of real number whose value is accurate to within a given magnitude.
Flag:
A Boolean value which can be set to True or reset to False.
Floating point:
A variety of real number which is represented to a specified number of significant figures
regardless of how big or small it is.
Formal parameter:
The definition of a parameter in a specification.
Full view:
The unrestricted view of a private type available from the package body where the type is
declared, or from the body of a child package or from the private parts of any of the associated
specifications.
Function:
A subprogram which returns a value of a specified type which is invoked as part of an
expression.
Garbage collection:
The automatic reclamation of dynamically allocated objects that are no longer accessible. Garbage
Generic unit:
A unit defined in terms of generalised types or subprograms and which can be instantiated to use
any compatible types or subprograms in their place.
Global object:
An object declared outside the current block (especially one declared at library level) so that the
current block is not the only place where it can be referenced.
GNAT:
The GNU Ada Translator, a free Ada 95 compiler available for a range of platforms. See Chapter
20 for more details.
Guard:
A condition used with an accept statement or a protected entry that specifies when it can be
invoked.
Heap:
A synonym for what Ada refers to as a storage pool.
Identifier:
A name given to a program entity that can be used to refer to it. Identifiers must begin with a
letter and must consist entirely of letters, digits and underline characters. The last character must
not be an underline character.
Indefinite type:
A type with unknown discriminants; a class-wide type or a type whose discriminants are
specified as (<>).
Index subtype:
A subtype specifying the range of possible subscripts for an array.
Inheritance:
The automatic definition of the characteristics of a type based on the characteristics of its parent
type.
Initialisation:
The actions which are performed immediately after an object is created to prepare it for use.
Instantiation:
The act of creating an instance of a generic unit by replacing its formal parameters by a set of
matching actual parameters.
Integer type:
A type capable of holding any whole number from a specified range of values. In Ada these can
be signed integer types or modular types.
Iteration:
A synonym for repetition, as in a loop.
Iterator:
A data type used to mark a position in a collection of data (e.g. a linked list) and to move from
item to item within the collection.
Library:
A repository managed by the compiler which is used to hold information about units which have
been compiled.
Library level:
The outermost level of scope in a program. Only the names of library units and names declared
in a library-level package are at library level.
Library unit:
A subprogram or package compiled as a stand-alone unit (i.e. not embedded within any other
unit) and added to the program library.
Limited type:
A type which cannot be copied by assignment and for which the standard equality tests are not
provided (although you can overload "=" and "/=" to provide these yourself).
Linked list:
A data collection where each item in the collection points to its neighbours using access values.
Linker:
A program which binds a library subprogram together with any other library units it refers to
into an executable program.
Literal:
A representation of a value of a particular type, as with integer literals (e.g. 123) or string literals
(e.g. "xyzzy").
Loop:
A section of code which is repeated (iterated).
Magic number:
A number appearing in a program whose appearance tells you nothing about its intended purpose
or meaning.
Maintenance:
The process of fixing bugs in, and adding new features to, existing software.
Model:
A representation of some aspect of external reality in a program.
Multitasking:
The ability to execute several parts of a program in parallel (or apparently in parallel).
Namespace:
The set of names accessible at a given point in a program.
Null pointer:
An access value which does not refer to any object.
Object:
A constant or variable of a specified type.
Opaque type:
A private type whose implementation details are made completely invisible by moving them into
the package body and using an access value for an incompletely declared type in the package
specification.
Overloading:
Giving multiple meanings to the same name, but making them distinguishable by context. For
example, two procedures with the same name are overloading that name as long as the compiler
can determine which one you mean from contextual information such as the type and number of
parameters that you supply when you call it.
Overriding:
Providing a declaration which matches another declaration of the same name, thereby hiding the
existing declaration.
Package:
A collection of logically related declarations (both public and private); typically, the declaration
of a type together with the operations of that type. Implementation details are concealed within
the corresponding package body.
Parameter:
A value or object which is used to transfer information to or from subprograms. Input parameters
can be thought of as being copied into the subprogram when it is called, and output parameters
can be thought of as having their values copied back to the caller when the subprogram returns.
Parent type:
The type from which a derived type was created.
Partial view:
The view of a private type to a client where not all characteristics of the type are visible.
Pointer:
A common synonym for what Ada calls an access value.
Portability:
A measure of system independence; portable programs can be moved to a new system by
recompiling without having to make any other changes.
Precedence:
The order in which arithmetic operations are performed.
Prettyprinter:
A tool for automatically formatting Ada source code.
Primitive operation:
An operation of a type which is inherited by all types derived from it. The primitive operations of
a type are those operations with a controlling operand or result of that type declared in the same
package specification as the type itself.
Private type:
A type whose internal implementation is inaccessible outside the package where it is declared.
Procedure:
Program:
A subprogram (usually a parameterless procedure) which has been linked to produce an
executable file.
Protected record:
A data structure which provides synchronised access to the data it contains for use in
multitasking situations.
Real type:
A type representing numeric values which include a fractional part. In Ada these can be floating
point or fixed point types.
Record:
A composite type consisting of a collection of named components, not necessarily all of the same
type.
Recursion:
The definition of an operation in terms of itself.
Rendezvous:
A form of intertask communication and synchronisation, where one task calls an entry of another
task and the called task executes an accept statement for the entry being called.
Reusability:
The ability of a package or subprogram to be used again without modification as a building block
in a different program from the one it was originally written for.
Scalar type:
A type representing single values which cannot be broken down into smaller components, namely
a discrete type or a real type.
Scope:
The region of a program where a name is visible, extending from its declaration to the end of the
block which contains the declaration.
Slice:
A section of an array selected by specifying its lower and upper limits.
Source code:
The original textual form of a program.
Specification:
A description of the interface provided by a subprogram, package, task or protected record. The
implementation details are hidden in the corresponding body.
Statement:
An instruction to carry out some action; a single step within a program.
Stepwise refinement:
The development of a program by breaking the original problem into smaller subproblems and
then applying the same process to each subproblem.
Storage pool:
A block of unused memory, often referred to as a heap, which can be used to allocate objects
dynamically using new for use with access types.
String:
A sequence of characters.
Stub:
A temporary implementaion of part of a program for debugging purposes.
Subprogram:
A set of statements which can be executed by calling the subprogram by name.
Subscript:
An index into an array which is used to specify an individual array component.
Subtype:
A type together with a constraint which possibly restricts the allowed range of values. Strictly
speaking, all types in Ada are actually subtypes of anonymous unconstrained types.
Syntax:
The rules of a language which determine what is and is not acceptable to the compiler.
Syntax analysis:
The process of checking that something conforms to the rules of a given syntax and analysing its
structure according to those rules.
Tagged type:
A type which can be extended by derivation to add new components or operations.
Task:
A construct consisting of a specification and a body whose body is executed in parallel with
other tasks in the program.
Token:
Anything treated as a single symbol during syntax analysis such as a name, a literal or an
operator.
Top-down:
An approach which starts from a high (generalised) level and works towards lower (more
specific) levels.
Top-down design:
A synonym for stepwise refinement.
Type:
Every object has a type which specifies the set of values allowed and the primitive operations
which it provides. Types are grouped into classes which share the same primitive operations.
Unconstrained type:
A type which is not fully specified, e.g. an array whose index subtype is incompletely specified
or a type with discriminants whose values are not known. You cannot declare objects of an
unconstrained type without supplying the missing constraints (although you can supply them by
providing an initial value as part of the declaration).
Undefined:
Having an unpredicatable (and not necessarily valid) value.
Variable:
An object of a specified type whose value can be changed.
Variant record:
A record which can take different forms depending on the value of a discriminant.
View:
An external representation of an internal processing model which is used to interact with the
internal model.
Wrapper:
A package which changes the interface to an existing package without substantially increasing its
functionality.
This file is part of Ada 95: The Craft of Object-Oriented Programmingby John English.
Copyright John English2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the
constituent files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $