Journal Articles

CVu Journal Vol 13, #2 - Apr 2001 + Programming Topics
Browse in : All > Journals > CVu > 132 (14)
All > Topics > Programming (877)
Any of these categories - All of these categories

Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.

Title: Ada Overview - Part 2

Author: Administrator

Date: 03 April 2001 13:15:44 +01:00 or Tue, 03 April 2001 13:15:44 +01:00

Summary: 

Body: 

Procedures and Functions.

Procedures and functions in Ada are similar to functions in C: they all allow a group of statements to be executed by merely mentioning their name. The difference is that functions must return a value whilst procedures do not. So procedures must appear in a statement on their own, whilst the value returned by a function must be used somehow. So functions can only be used where expressions are allowed.

C nominally only has functions and simulates procedures by allowing a function to return a variable of type void which means it returns nothing. A simple procedure might look like:

procedure scale (
  s       : out float;
  x, y    : in integer
  )
is
  grad    : constant := 27;
  const   : constant := 12;
  xy      : integer;
begin
  xy := x * y;
  s := float (grad * xy + const);
end scale;

This defines a procedure called scale which has three formal parameters: s which is a float and x and y which are both integer.

A formal parameter has a mode which is out for s. This means that the value in s when the procedure completes will be copied to the corresponding actual parameter. Parameters of mode out are not initialised to the value of their corresponding actual parameter on entry, but, as a change introduced by Ada 95, once the procedure has written a value to such a parameter, its contents can be used by the procedure.

So mode out is how results are got out of procedures. There is no need to remember to give the procedure the address of a variable whose value is to be changed with C's "&" operator, and then de-reference that in the procedure body with C's "*" operator.

x and y are of mode in which means that the value of their corresponding actual parameter can be read, but not changed, not even locally within just the scope of the procedure. There is also a third mode of "in out" which is both in and out at once. It allows the original value of the corresponding actual parameter to be read in the procedure and subsequently changed.

Two constants, grad and const are declared locally to the procedure, which the compiler deduces are of one of the integer types because they are initialised to integral values. xy is a local variable which is set immediately in the body of the procedure. The procedure then computes an expression, converts the result to a float and assigns that to s.

The procedure declaration is completed by the end which repeats the name of the procedure.

It is generally understood, and I shall follow this convention here, that when one talks of "procedures" one means "functions" as well, but a discussion on "functions" usually means only "functions".

Functions look similar, though they also have to say what sort of type they return:

function the_answer
  return integer
is
begin
  return 42;
end the_answer;

The return statement is used by procedures to leave them and by functions to additionally declare the value to be returned. Ada functions may return any type including records and arrays. Procedure parameters may also be of any type.

This example also shows that procedures generally need not have any parameters. However, if a function does have any parameters, they must all be of mode in. This is to prevent functions from having side effects by appearing to explicitly return only one value whilst quietly changing the value of some of their actual parameters as well. A use of the above procedure and function might be:

ultimate_answer : integer;
why     : float;
ultimate_answer := the_answer;
scale (why, ultimate_answer, 2);

which should set why to 2280.0.

Procedures are re-entrant, which means that recursive algorithms can be written.

Procedures may also be defined within the scope of other procedures. C does not have this, though Pascal does. So a main program is simply the outermost procedure that the compiler finds, with all the procedures defined within its scope, and so visible only to itself, being the subroutines it uses.

Packages.

Packages are Ada's way of supporting modular programming. A package will generally contain a number of constants, types, variables and procedures (and functions) all related in some way. So a package might contain a series of graphing procedures, or string manipulation routines.

Of the objects declared, some, which represent the package interface, the user will be able to see, and use. The rest he will not, and represent the internal workings of the package. The best you can do in C to hide an object from the user is to make it static.

Ada separates the interface specification from the package body, which is the implementation of the interface, and encourages these to be written in separate files. This then means that, with the interfaces defined, coded and compiled, the package bodies may be written without touching the interface. This in turn means that the compiler need re-compile all the code in a program only when it sees an interface changed. We see here Ada starting to implement a source code control system within the overall language.

Were scale and the_answer of the previous section to be part of a package, the interface would be:

package two_proc is
procedure scale (
  s       : out float;
  x, y    : in integer
  );
function the_answer
  return integer;
end two_proc;

And the package body would be

package body two_proc is
procedure scale (
  s       : out float;
  x, y    : in integer
  )
is
  grad    : constant := 27;
  const   : constant := 12;
  xy      : integer;
begin
  xy := x * y;
  s := float (grad * xy + const);
end scale;
function the_answer
  return integer
is
begin
  return 42;
end the_answer;
end two_proc;

which is a little repetitive of material, but does give a complete example.

Having written the package two_proc, or at least its interface, the compiler must be told when the procedures within it are to be used, or made visible.

The with clause makes the objects within a package visible, or available, to the subsequently declared procedure or package. Then a construct of the form package_name.procedure_name will select the named procedure from the named package. This makes it clear where a procedure which does not otherwise appear in the source file is meant to come from. Alternatively, a use clause may be written after the with clause. This tells the compiler to search the named package for procedures which cannot otherwise be found.

This is illustrated in the two equivalent examples:

with two_proc;
procedure use_it_1 is
begin
  why     : float;
  two_proc.scale (why, two_proc.the_answer,2);
end use_it_1;
with two_proc; use two_proc;
procedure use_it_2 is
begin
  why     : float;
  scale (why, the_answer, 2);
end use_it_2;

Many coding standards mandate the use of the first form, including for uses of the standard packages, so that it is always explicitly clear where a non-local object is coming from.

Generic Procedures and Packages.

Generic procedures (and packages) are akin to the templates of C++ in that they give the complete details of how to do something, but omit a vital piece of information, such as the type of the object on which their operation is to be performed. So, having written, say, a sort procedure, the compiler can be told to build the procedure for a specific type by instantiating the generic procedure to get a specific procedure.

So the interface for that generic sort procedure might be:

generic
  type elem is range <>;
  type elem_array is array
             (integer range <>) of elem;
procedure qsort (
  a       : elem_array
  );

and the function qsort itself could then be written without worrying (or knowing) exactly what types elem and elem_array actually are, but using the generic formal types as necessary to complete the code. If you like, elem and elem_array are parameters which must be replaced with the actual types to complete building the procedure. However, the actual types must match the templates of the corresponding formal types.

In this example, the size of the array is given as integer range <>. This says that the type of the array index is an integer, but <>, pronounced "box", says you are not going to say exactly what the range of the index is at the moment. By careful use of the first, last and range attributes, you can put off needing this information for quite a long time, though it will be needed when you eventually define an integer array.

To instantiate this generic procedure to create a new procedure which would sort an array of integers you would need to write:

type int_array is array (integer range <>) of integer;
procedure int_qsort is new qsort (integer, int_array);

int_qsort would then be used like any other procedure declared within the current scope.

ia      : int_array(1 .. 100);

- Set all the values in ia somehow.

int_qsort (ia);

Of course, a practical sort function would sort an array of records, and so need a function to tell it which of two objects should be the earlier. And so a generic formal procedure parameter can be used to represent a procedure that is replaced with an actual procedure's name when the package is instantiated.

Generic packages and procedures are an important part of Ada. I shall shortly show that many of the supplied packages are actually generic packages which must first be instantiated with the type to hand.

Dynamic Memory Allocation.

To dynamically create an object in C requires the use of one of the library functions malloc and calloc. Ada takes the opposite approach of using the reserved word new to dynamically allocate storage for an object. When followed by the name of a type (which will have to include the dimensions of the type if it is that of an array), a value is returned which must be assigned to the access type for the object created.

So following on from the node_type for a binary tree earlier:

this_node.right := new node_type;
this_node := this_node.right;

will create a new node for the binary tree, insert it as the right sub-tree of the current node, and then make this new node the current node.

To return dynamically allocated memory, there is a generic procedure, unchecked_deallocation, which must be instantiated with the name of the type to be deallocated together with the name of the access type for that object.

with unchecked_deallocation;
procedure free_node is
  new unchecked_deallocation (node_type, node_ptr_type);
will create a procedure to free a node object, and 
free_node (this_node.left); 

will achieve this whilst simultaneously setting this_node.left to null to trap any future inadvertent use of the associated access variable.

The generic function unchecked_deallocation is so called because it does not check that some other access variable does not also point to the object being deallocated. That is the user's responsibility and failure to ensure this will result in the usual problems of objects mysteriously changing their value.

A difficulty with Ada 83 is that the storage so freed is not returned to the system for re-use until the object's access type goes out of scope. This may be never, in which case an Ada 83 program may require far more memory to run in than would an equivalent C program. The reason for this is to try to ensure that as long as objects accessing a particular type exist, so will the memory they point to, thus trying to reduce the detrimental effects of referencing de-allocated memory. The language designers were trying to be safe here, knowing that they had to have access types to support dynamic structures like trees, but being all too well aware of the problems access types (or their users) cause.

Ada 95 has had a major re-think in this area. Whilst unchecked_deallocation still exists, there are additional procedures to control the storage pools from which objects are allocated together with an automatic garbage collector to return storage when there are no longer any access variables pointing to an object. Hopefully the memory requirements of Ada 95 programs are now more reasonable.

Standard Packages.

The standard says that a number of packages must be supplied with an Ada compiler. It also specifies a number of packages that might optionally be supplied. The compiler does not have to supply this latter list of packages, but if it does it must do so the way the standard says.

Library packages may contain packages that are then called child packages. And the mandated packages contain a number of both mandated and optional child packages.

There were a number of changes to the packages specified and their organisation when Ada 95 superseded Ada 83. I am writing these sections based on the Ada 95 standard and will mention some of the more important packages.

The first package, which must always be present, is called standard and contains things like the types character and boolean and all the arithmetic operators for all the integer and floating point variable types supported. As the compiler is required to implicitly know everything this package contains, it rarely has to be explicitly mentioned in a program.

The package ada contains a number of packages. The first is ada.characters which itself contains a number of packages. ada.characters.handling contains character manipulation procedures such as is_control and to_lower. I am sure you can guess what they do and name their equivalent in C.

The package ada.characters.latin_1 defines a name for every character in the ISO 8859-1 character set (this is the 256 character version of the 128 character set you probably call ASCII). Use of the identifiers in this package lets you place control characters, or any other character for which you do not have a key on your terminal's keyboard, in strings using the concatenation operator "&".

String handling is provided in the package ada.strings which itself contains several packages. ada.strings.maps performs character to character mappings. ada.strings.fixed provides procedures for copying parts of one string to another, of possibly differing lengths, or finding one string in another.

The package ada.numerics supports mathematical computation, so ada.numerics.generic_elementary_functions defines the trigonometric functions, but only after you have instantiated the package with the type of floating point number you are using. Ada also supplies instantiated versions of this package in ada.numerics.

Ada side-steps the problem of whether a trigonometric function should work in radians, degrees or grads by supplying versions of all the usual functions that work in radians, then a second version which takes an extra parameter which says how many of the measurement unit there are in a complete circle. So if you wanted the sine of x, where x is measured in degrees, you might write sin (x, 360.0).

Input and Output.

Other than a brief mention in two examples, I have so far avoided all mention of how Ada does input and output. This is because Ada uses procedures from standard packages to do this. All the packages mentioned in this section are required.

The package ada.text_io opens, reads, writes, and closes text files. Having opened a file, you can ask what its full path name is. There are also procedures for deleting files and resetting them to the start of the file. The concepts of standard_input, standard_output and standard_error are supported.

There are a large number of procedures for terminating lines and pages, or finding out the page number, line number and column number of where you are in a file. When reading, there is also an end_of_file function.

There are a number of generic packages in ada.text_io which provide procedures for reading and writing integers, fixed point and floating point, and enumerated types. Pre-instantiated versions are available for the common types.

Writing to a text file is through the procedure put which is overloaded to write out every elementary type. And get is overloaded to read every elementary type. Hence when you create a record type, you should immediately overload the get and put procedures with declarations which will sensibly read and write objects of your new type. So you do not need to remember the name of the procedure which writes a particular type: put will always write it and get will always read it and the compiler will resolve the overloaded name to give you the right one.

The consequence is that what is a single call to fprintf in C becomes a list of calls to put in Ada, each writing out one element of the line in turn, followed by a call to new_line (or put_line) to terminate the line and start the next one. Similarly, fscanf in C becomes a list of calls to get terminated by skip_line.

A simple, but complete, Ada program which reads two numbers from the terminal and writes out their sum is:

with ada.text_io; use ada.text_io;
with ada.integer_text_io; use ada.integer_text_io;
procedure sum
is
  a       : integer;
  b       : integer;
begin
  put ("a: ");
  get (a);
  put ("b: ");
  get (b);
  put ("sum = ");
  put (a + b);
  new_line;
end sum;

Binary input output is supported by the generic packages ada.sequential_io if sequential access is adequate, and ada.direct_io if a direct access (random access) file is required. Both have to be instantiated with the type to be read or written. But again, the emphasis is on using the same names, irrespective of the type operated on, to provide a simple user interface.

Complex Arithmetic.

Of interest to those doing scientific or engineering calculations, the optional package ada.numerics.generic_complex_types provides a complex arithmetic object type together with all the elementary arithmetic and relational operations on these. As suggested by the package name, it must be instantiated with the type of floating point number to be used for the components of the complex numbers. The associated elementary functions are contained in the package generic_complex_elementary_ functions in ada.numerics. Input - Output of complex numbers is performed using ada.text_io.complex_io which is a generic package.

The Calendar Package.

The mandatory package ada.calendar provides access to the current date and time through the clock function. As the type clock returns is the private, or implementation defined, type time, there are functions to decode this into year number, month number, day number and time of day in seconds. The reverse function to build a time from the individual fields is also supplied.

The operators "+" and "-" are overloaded to find the difference between two times, or find the time a given period later or earlier.

Despite all the publicity recently about a certain "Y2K" problem, the calendar package is only specified for the years 1901 to 2099. In the year 2000, ages, and life expectancies, of 100 years are quite reasonable. So this limitation, which exists in both Ada 83 and Ada 95, seems somewhat restrictive.

A second point is that the number of seconds in the day may range between the inclusive limits of 0.0 and 86400.0. This is reasonable as it allows the astronomers to insert one leap second into the length of a day from time to time to realign UTC (what used to be called GMT) with the rotation of Earth. That is, to ensure that high noon is when the clock says it is 12:00:00. But the corresponding part of the C standard, tm_sec in <time.h> suggests that room for two leap seconds should be allowed.

Somebody, somewhere, has not been doing their homework.

Interfaces to Other Languages.

To increase its potential user base, Ada acknowledges that other computer languages exist, and specifies how procedures written in these might be called. It talks specifically about C, COBOL and Fortran, but says how the list might be extended. Note that this means it says how to call, for example, a C function from an Ada procedure. This is not the same as calling an Ada procedure from a C function: for that you will have to ask the C standard committee.

Inclusion of the appropriate packages is mandatory.

As an example of their content and level, the C interface, in interfaces.c, provides a number of procedures for converting between Ada style strings and C style strings, or Ada style access types and C style pointers.

I have not tried using this package, but it looks as though it will quickly become target operating system, Ada compiler and C compiler dependent. Even so, the existence of this package is recognition that practical programs often have to merge procedures from a number of languages to obtain access to the pre-existing functionality they require.

Features Not Described Here.

Given that the Ada 95 standard committee has produced a Language Reference Manual that is a book of over 550 pages, that there is then a Rationale of a further 450 pages explaining why much of the standard is the way it is, and that there is an annotated Language Reference Manual which contains discussions mainly of interest to compiler writers, it is inevitable that some features of Ada will have been omitted from an article such as this. But I can at least outline some of those and say that the reason for their exclusion is that I have not had cause to use them in the work I do, so prefer to leave their exposition to someone with experience in the appropriate areas.

Ada supports tasking so that the language may be used for real-time control systems. There is a rendezvous, rather than a signal, mechanism to synchronise tasks and controlled access to global variables to prevent several tasks simultaneously changing a variable. A pre-emptive, priority queued, scheduler runs each ready task in turn. So having written the hardware drivers, for which Ada also provides interrupt access, everything you need for a complete embedded system should be there. There are a number of optional packages to assist this aim.

At the other end of the scale, Ada supports distributed programs running on multiple processors. Again, support is in part through an optional package.

An optional package providing an interface to the operating system is specified for those who need more facilities than the mandated packages can otherwise offer, though, of course, Ada cannot say what this interface does.

There is a very large number of attributes which allow you to discover virtually anything you need to know about the computer on which your program is running: you can even ask if your computer is a big- or little- endian machine.

And I shall close this list with pragmas, which are directives to the compiler providing it with information which cannot otherwise be expressed in the language. For example, "pragma pack (name)" says that the storage used for the object name is to be minimised, even at the expense of run time speed.

Language Standard.

As I mentioned above, there have been two issues of the standard: the earlier, now largely superseded, Ada 83 and the current Ada 95. The language now has international standing through the International Standards Organisation, number ISO-8652:1995. The language is supervised by the Ada Joint Program Office, AJPO, which is an on-going organisation within the American Department of Defense.

The AJPO provides a standard interpretation service and a compiler validation service. It has retained the copyright in the "Ada" name, and will only allow its use by a compiler that has successfully passed validation (and paid the fee). This does not mean that Ada compilers are bug-free, but that the compilers attempt to support all the non-optional features of the language, and also support those optional features they claim to support. So there are no sub-set Ada compilers doing the rounds to trip you up.

Because of the language origins, the Ada standard may be copied free of charge. But if a publisher does so, he must copy all of it. The reprinter may insert additional text, but he must be careful to distinguish between the original text and his additions. So a compiler author cannot put something into his compiler, make it look like part of the standard, and try to lock you into his product if you use it.

So a book that comes without a copy of the standard, or a compiler that does not supply a copy of the standard annotated with the details of implementation dependent features, is probably not worth considering.

And please do read the standard. Unlike many standards, it is actually quite readable (though it does not try to teach computer programming). There are lots of examples, so knowing roughly what you want, you can always modify the example when you do not understand the text.

Although Ada does not specify a program layout, the standard does recommend you follow the layout style of the syntax definitions and examples. But I do not think you will find anything unexpected there. I have tended not to follow this here because I prefer to keep the layout of my code language independent for my ease of debugging. Beauty is in the eye of the beholder, but remember that your Quality Assurance Manager regards his coding standards document as beautiful, not your code.

Where Next?

Buy a book, get a compiler, code some examples, write your next project in Ada.

There are several books on Ada, with the usual one being by John Barnes entitled Programming in Ada 95 and published by Addison Wesley. John Barnes has been active on the Ada standard's committees since the language's inception, so his book should at least be correct.

Barnes' book is currently at second edition and based on an earlier book describing Ada 83 that reached three editions. The book comes with a CD-ROM with the standard on it, together with a full-feature demonstration compiler. Whilst limited to 2000 lines of source, that should get you going and you could buy the unlimited compiler if you wished.

As I have not done a compiler survey I cannot suggest other compilers, though I am told there is one in GNU.

So over to you, write something, and give the Editor the headache of how he is going to fit an Ada column into this magazine.

[Well, if they will write it I will find space somehow. Ed]

Notes: 

More fields may be available via dynamicdata ..