up

The Briefest Introduction to Ada 95

 

1. A silly program that does nothing

We assume that you are using GNAT under Unix. If you are using GNAT on DOS, OS/2, or NT, then things are almost the same, except that you do not need to say ./ to run a program, and the executable files have .EXE added as an extension

Let's start with a program that does nothing

   procedure Silly is
   begin
      null;
   end Silly;

A main program is a procedure with no parameters. You can call it anything you like. To compile this program, make a file called silly.adb and edit the above text into it. Note that the file name always matches the unit name (the unit name is the name you use inside the Ada text, "silly" in this case).

Then compile it

     gnatmake silly

Then run it

  ./silly

Nothing will happen, you will just get a prompt. That's not surprising, it is a program which does nothing!

Note that in these examples, we always use lower cas for keywords (procedure, begin, end, null etc.) and Mixed_Case_With_Underlines for identifiers. That's just a convention, Ada 95 does not care about case of letters in identifiers, so you could spell an identifier as Silly in one place and SILLY in another, but that would be a silly thing to do, since stylistic consistency is a valuable goal!

2. Do something using standard types, but you can't see results

   procedure Add is
      A : Integer;
      B : Integer;
   begin
      A := 3;
      B := A + 4;
   end Add;

You can compile and run this if you like, editing it into the file add.adb, but it still won't do anything. It is doing an addition, but it does not output any results. It takes quite a bit of machinery before we can do I/O, and in this presentation we don't want to cheat, you can't see I/O till you know enough to understand it, so be patient for a few more examples.

In the above, A and B are declared to be of Integer type. This is a built in type whose size depends on the implementation (like the corresponding types in Pascal or C). Later on we will see how to make our own integer types with defined ranges that are independent of the implementation. Many Ada programmers have a rule never to use the built-in types (but they know how to make their own!)

Note that here we see the general framework of a procedure body (we will find out later why it is called a procedure body). A bunch of declarations follow the "is", then "begin" marks the start of the statements, up to the "end". Repeating the Add on the "end" line is optional, but a good idea. Note that declarations and statements cannot be mixed. Declarations can be in any order, providing you don't have any forward references. Statements are executed in the order you give them.

3. Some more operations on integer values

   procedure Ops is
      A,B,C : Integer;  -- same as 3 separate declarations
   begin
      A := 5;
      B := A * 3 + 4;   -- result is 19 (multiplication done first)
      B := A * (3 + 4); -- using parens to override the order
      C := A / 3;       -- result is 1  (division truncates)
      C := A rem 3;     -- result is 2
      C := A - 1;
      C := A ** 2;      -- exponentiation
   end;

Here we see some basic arithmetic operations, the exact set of operators is similar to, but a little different from Pascal and C, we will give the complete set later.

4. A function computing the square

   function Square (Arg : Integer) return Integer is
   begin
      return Arg * Arg;
   end Square;

This function takes an Integer argument, which can only be read (there is no way for a function to modify its arguments), and returns a single result. There can be one or more "return" statements in the body of the function, execution must terminate by running into one of these return statements.

This function would be edited into its own file, square.adb. You can't try to run this function on its own, since it is not suitable for use as a main program.

To use this function, we have a main program looking like:

   with Square;
   procedure Compute is
      A : Integer := 3;    -- initializing a variable
      B : Integer;
   begin
      B := Square (A + 1); -- result is 16
   end Compute;

The "with Square" announces the intention of using this function in this procedure, and as seen we just use it in the natural way, with the argument in parentheses.

To run this program, we prepare the two separate files square.adb and compute.adb, and then do

     gnatmake compute

the gnatmake command always references the main program. Then we can run it using:

   ./compute

but it still won't do anything that we can see, since we still don't know how to do I/O.

5. Using separate specs for functions

One trouble with the previous approach is that if we had to go and change something in the body of square, we would have to recompile the main program. When you enter a gnatmake command, it compiles everything necessary. Basically it recompiles anything you have changed (square if you edited square), and anything that with's something that has changed (compute in this case).

But the guy writing compute really only needs to know the way that Square is called, no the details of how it works. Furthermore in a large program, it is a definite advantage to only know the bare details of how to call something and what it does -- you don't need to know the details of how to compute it. For example, if you call a function to compute a hyperbolic arctangent, all you need to know is the appropriate range of the argument, you don't need to know how to compute such a function. In fact if you are like most computer scientists, you don't WANT to know how to compute such a function.

Let's see how we deal with this in Ada, let's consider another function

   with Square;
   function Cube (Arg : Integer) return Integer is
   begin
      return Arg * Square (Arg);
   end Cube;

This is the body of the function, in file cube.adb. We can make a separate file called cube.ads that has only the following text:

   function Cube (Arg : Integer) return Integer;
   --  Compute and return cube of argument

That's it, just the header, nothing else. The header must exactly match the corresponding header in the function body except that the "is" is replaced by a semicolon.

This new file is called the function spec (an Ada purist would call it a function declaration, but no one uses that, so we won't either). It acts as a kind of contract between the user and producer. The idea is that everyone agrees on the spec of Cube, and the file cube.ads is generated, with the hope that it will not need to be changed. As in our example above, it is particularly important that specs have adequate comments explaining what they do. Comments in Ada 95 start with -- and extend to the end of the line. There is no way to get a block of comments except to put -- on each line of the block.

Once the spec of Cube is complete we can work separately on the body of Cube and the main program that will be using it. Of course we can't gnatmake the main program till everything is ready, but it may be useful to check out our pieces of the program on the way.

For example, suppose that the programmer writing the main program has completed the task:

   with Cube;
   procedure Compute2 is
      A : Integer := 3;
      B : Integer;
   begin
      B := Cube (A + 1); -- result is 64
   end Compute2;

and wishes to check it out. Entering:

gnatmake compute2

will result in a complaint that the body of cube is missing, but we can instead just compile our piece of the program:

   gcc -c compute2.adb

now the main program is compiled. If there are any errors in this main program, the gcc command will display them. Of course we still can't run the program till all the pieces are complete.

Do an experiment. Edit the files cube.ads, cube.adb, compute2.adb as described above. Do a gnatmake on compute2.

Now modify the body (cube.adb) of the function (add a comment for example), and do another gnatmake on compute2. Now you will see that ONLY the body of cube is recompiled.

Now modify the spec (cube.ads) of the function and do a gnatmake. Now both the body of cube and the main compute2 program have to be recompiled, since both depend on the spec of cube.

6. Packaging up two functions in a package

It can be quite a nuisance to have every function in a separate file, especially if we use separate specs for each function. Luckily we can avoid this, because Ada 95 provides the concept of a package to deal with packaging functions into a single file. Typically we bundle up a set of related functions. In this case, Cube and Square are related, so let's bundle them up into a package called Powers.

For packages, it is absolutely required to have two files, one for the spec and one for the body. The spec looks like:

   package Powers is
      function Square (Arg : Integer) return Integer;
      function Cube   (Arg : Integer) return Integer;
   end Powers;

This goes in a file called powers.ads. The body, which goes in the file called powers.adb, looks like:

   package body Powers is
   
      function Square (Arg : Integer) return Integer is
      begin
         return Arg * Arg;
      end Square;
   
      function Cube (Arg : Integer) return Integer is
      begin
         return Arg * Square (Arg);
      end Cube;
   end Powers;

The package spec is just a series of declarations, including function specs. Later we will see that it typically can contain type declarations and even variable declarations that are logically related.

The package body has the bodies of the functions. These look exactly like the bodies when we compiled them separately, with one interesting exception. The "with Square" on the body of Cube is gone. That's because with statements belong only at the start of the unit (if Powers needed to "with" anything then the "with" statements would go before the keyword "package"). But more importantly, you don't need to with things that are in the same package as you are. A package is a family of declarations which can all see one another without any fiddling.

To use the package, we make a main program that with's the package:

   with Powers;
   procedure Withp is
      A : constant Integer := 10;  -- defining a constant
      B : Integer;
   begin
      B := Powers.Cube (A);        -- result is 1000
   end Withp;

When we call the Cube function, we have to precede it by Powers and a period to indicate which package the Cube function lives in, otherwise using a function inside a package is the same as using it in the standalone case. In practice, nearly all functions live in packages, typically only the main program lives on its own as a separate file.

7. Avoiding the need to prefix with Powers, the use clause

The requirement to put Powers and a period is a bit of a pain. But it has its advantages. In a big program, it can make it easier to see where things come from. On the other hand it clutters up the text. As always in Ada 95, we favor the reader of the code over the writer, so we aren't very sensitive to the argument that it is a nuisance to *write* the extra junk, but we might worry about the cluttering causing it to be harder to read. If we feel that way, we can use the "use" clause:

   with Powers; use Powers;
   procedure Usep is
      A : constant Integer := 10;
      B : Integer;
   begin
      B := Cube (A);
   end Usep;

and then we don't need the prefix. The "use" clause makes all the items declared in the package visible without the prefix.

To "use" or not to "use", that is the question! This is a controversial subject. Some Ada 95 programmers regard the use clause as being in the same category as a goto statement (yes Ada 95 has a goto but we won't tell you about it). Other Ada 95 programmers find them "use"ful :-) It's up to you to decide, but try to adopt some consistent style.

One point to note. It is perfectly valid to have the same names declared in multiple packages. In this case, you generally have to use the prefix notation anyway, so that the compiler knows which instance of a name you are talking about.

8. Procedures with parameters

Functions take parameters, which are read only, and compute a single result. Procedures can take parameters also, and in addition, can specify parameters as being writable as well as readable. This allows a procedure to return multiple results. Here is an example:

   procedure Procp
     (A   : Integer;         -- default is "in"
      Ai  : in  Integer;     -- actual can be expression
      Ao  : out Integer;     -- actual must be variable
      Aio : in out Integer)  -- actual must be variable
   is
   begin
      Ao  := A + Ai;    -- can read in params, write out params
      Aio := Ai + Aio;  -- can read or write in out parames
      Aio := Aio + Ao;  -- can read out params after setting them
   end Procp;

This procedure could live in its own file as procp.adb, or could be placed inside a package. In either case, it would probably have a separate spec:

   procedure Procp
     (A   : Integer;
      Ai  : in  Integer;
      Ao  : out Integer;
      Aio : in out Integer);
   -- Fascinating comments explaining what Procp does

To call a procedure, we "with" it, and then we can call it:

   with Procp;
   procedure Pcall is
      A,B,C : Integer;
   begin
      Procp (3,6,A,B);  
      Procp (Aio => B, Ao => A, A => 3, Ai => 6); -- named parameters
   end Pcall;

As shown here, procedures can be called using positional notation, in which we have to remember the order of the parameters, or the very useful named notation, in which we can put the parameters in any order. Named notation should almost always be used except in a very simple case. For example:

   V := Square (Q);

seems OK. Although we could write:

   V := Square (Arg => Q);

that doesn't help much. On the other hand, we much prefer to see:

  Integrate (Func => Cosine, From => 0.12, To => 0.15, Result => Area);

to

  Integrate (Func,0.12,0.15,Area);

Note how the nice choice of parameter names makes the procedure call read well so that you can pretty much guess what it does without even needing to look at the spec. Carefully designing your functions and procedures (collectively called subprograms in Ada 95) to work this way will ease the job of the poor fellow who has to read your code.

Writing readable, and hence easily maintainable, code is what Ada 95 is all about. In practice the life-cycle cost of software (i.e. the full cost from beginning to end, including all maintenance) is mostly maintenance, so making code easier to read and maintain is a critical goal.

We are getting closer to being able to do some output (output uses a procedure call), but we still need a little more. We will get there soon!

9. Arrays

   procedure Amain is
      type Arr is array
        (Integer range 1 .. 10) of Integer; -- fixed bounds
   
      type Aru is array
        (Integer range <>)      of Integer; -- variable bounds
   
      M : Arr;             -- bounds are 1 .. 10
      N : Aru (1 .. 10);   -- bounds must be given in this case
   
   begin
      M (3) := 4;
      N (2) := M (1) * 3;
   end Amain;

This shows how arrays work in Ada 95. As you can see there are two types of arrays, one with fixed bounds, and one with variable bounds. In the variable bound case, each object has fixed bounds, but you can have several objects of the same type, all with different bounds.

10. Integer subtypes

   procedure Subt is
      subtype Day is Integer range 1 .. 7;
      Q : Day;
   
      M : Natural;  -- built in subtype: Integer range 0 .. max integer
      N : Positive; -- built in subtype: Integer range 1 .. max integer
   
   begin
      Q := 8;  -- raises runtime exception (error)
      M := -1;  -- so does this
   end;

The subtype mechanism allows defining variables that have a limited range. This is useful for debugging and documentation purposes and is very similar to the corresponding feature in Pascal. Actually Ada 95 is inspired originally by Pascal, so this is not so surprising!

11. Built in types Character and String

   procedure Chars is
      X : Character;
      M : String (1 .. 5); -- built in array type
      type Str is array (Positive range <>) of Character; -- same as String
   
   begin
      X := 'A';      -- assigning a character
      X := ''';      -- single quote, not a special case
      M := "abcde";  -- string literal, length must match
      M (3) := X;    -- M now contains "abAde"
      M := "xy""ca"; -- quotes must be doubled inside strings
                     -- M contains one embedded quote character
   end Chars;

Type Character is like char in C or Pascal. The String type is simply a built-in array type as shown by this example. String literals provide a convenient way of writing constants of this array type.

12. Boolean operations and types

   procedure Bool is
      X,Y : Boolean;
      A : constant Integer := 3;
      B : constant Integer := 4;
   
   begin
      X := A =  3;  -- result is True
      Y := A >  3;  -- result is False
      Y := A >= 3;  -- result is True
      Y := A <  3;  -- Result is False
      Y := A <= 3;  -- Result is True
      Y := A /= 3;  -- Result is False
   
      Y := A = 3 and then B = 4; -- Result is True
      Y := A = 3 or else  B = 3; -- Result is False
   
      Y := A = 2 and then B = (1 / 0); -- False, 1/0 not executed
      Y := A = 2 and      B = (1 / 0); -- Bomb,  1/0 computed
      Y := A = 3 or else  B = (1 / 0); -- True,  1/0 not executed
      Y := A = 3 or       B = (1 / 0); -- Bomb,  1/0 computed
   end Bool;

Boolean is a separate type, as in Pascal, with values True and False. As shown in this example, there are two kinds of logical operators. The "and" and "or" operators always compute both of their arguments. The short circuit operators "and then" and "or else" compute from left to right and quit as soon as the answer is known. Generally it is better to use the short-circuit forms.

13. If statements

   procedure Ifs is
     X : Integer := 3;
     Y : Integer := 4;
   begin
      if X = 3 then
         X := 4;        -- executed
         Y := 3;        -- executed
      else
         X := 2;        -- skipped
         Y := 2;        -- skipped
      end if;
   
      if X /= 2 then
         Y := 3;        -- executed
      end if;
   end Ifs;

As we see from the above, the form of an if is a little different from C or Pascal, a sequence of statements can be used in either the "then" part or in the "else" part, with the "end if" marking the end of the if statement. There is no need to use begin and end (or the C equivalent {}) when multiple statements are needed.

It is a good idea to indent Ada 95 programs nicely. We will always indent our examples following standard style. Copy it!

One problem with the "end if" style is that nested if's can be a pain

  if B = 0 then
     ...
  else
     if B = 1 then
        ... 
     else
        if B = 2 then
           ...
        else
           if B = 3 then
              ...

and soon we have wandered off to the right side, we will return later with

              ...
           end if;
        end if;
     end if;
  end if;

but that's a real pain. But there is a new keyword "elsif" to the rescue. This is not quite equivalent to "else if" precisely in that it does not open a new "if" range and does not require a separate "end if", so the above becomes:

   if B = 0 then
      ...
   elsif B = 1 then
      ...
   elsif B = 2 then
      ...

   else
      ...
   end if;

which is a little neater. In fact once you get used to this layout and approach you will find the Ada 95 approach nice, easy to write and easy to read.

14. Loops

   procedure Write_Int (A : Integer);
   --  Writes out an integer

   with Write_Int;
   procedure Loops is
      Q : Integer;

   begin
      Q := 1;
      while Q <= 5 loop
         Write_Int (Q);
         Q := Q + 1;
      end loop;

      for J in 1 .. 5 loop
         Write_Int (Q);
      end loop;

      Q := 1;
      loop
         Write_Int (Q);
         exit when Q = 5;
         Q := Q + 1;
      end loop;
   end Loops;

Ah ha! Some I/O, but unfortunately we can't actually run this program since we don't know how to write the body of Write_Int. We will get to that later. But since we have the spec, we can write (and understand) the body of the main program that uses Write_Int.

This shows three loops that all write out 1,2,3,4,5 using the three different looping methods in Ada. Note tha there is no "until" loop although the "exit" statement can be used to construct the equivalent.

Note that we did not need to declare J in the "for" loop, it gets declared automatically for us. This is an important difference from Pascal. If you try to declare the loop variable:

   with Write_Int;
   procedure Wrong is
      J : Integer;
   begin
      for J in 1 .. 5 loop
         Write_Int (J);
      end loop;
   end Wrong;

You will get a warning that the first J is never assigned. That's because the loop declares its own J which has nothing to do with the J you declared outside, which is indeed never assigned (or referenced for that matter!)

The last form of loop, with no "while" or "for" had better have an exit statement, or it is an infinite loop. There are some programs that are legitimate infinite loops, the main control loop of the program controlling an aircraft had better not terminate, but you won't be writing programs like this for a while!

15. Procedures taking variable length arrays

   procedure Chg (Arg : in out String) is
   begin
      for J in Arg'First .. Arg'Last loop  
         if Arg (J) = ' ' then
            Arg (J) := '*';
         end if;
      end loop;
   end Chg;

This procedure changes all blanks to asterisks in a string. It works with a string of any length, since Arg'First and Arg'Last are the lower and upper bounds of the actual argument. You can also write for this case:

   for J in Arg'Range loop

which is a shorthand for Arg'First .. Arg'Lastx.

16. Finally we can use some built-in I/O packages to do output

   with Text_IO;
   procedure Hello is
   begin
      Text_IO.Put_Line ("Hello World (finally!)");
   end;

Finally! we can do some I/O. Text_IO is a predefined package which contains many useful procedures, but for now we are interested in only four of them:

   procedure Put      (Item : in Character);
   procedure Put      (Item : in String);
   procedure Put_Line (Item : in String);
   procedure New_Line;

Put outputs a character or string with no terminating line feed, Put_Line outputs a string followed by a new line, and New_Line outputs only a new line. In fact it is easy to guess the coding of the body of Put_Line:

   procedure Put_Line (Item : in String) is
   begin
      Put (Item);
      New_Line;
   end Put_Line;

but it is convenient to have this predefined.

One interesting thing to notice is that we have two procedures called Put. This is called overloading. No confusion arises because the compiler can tell from the type of the parameter which version you need:

   Put ('A');   -- must be the Character version
   Put ("AB");  -- must be the String version

Overloading is quite useful when you have a group of logical operations that reasonably have the same name. Most languages allow overloading of operators (e.g. + used for both integers and floats), but Ada 95 is a little unusual in allowing overloading for user defined subprograms.

17. Outputting integer values

We can write our own routines for outputting integer values. Here is one possible procedure, which is an interesting example of recursion. We won't bother with negative values for the moment, just to keep the example simple:

   with Text_IO;
   procedure Write_Integer (N : Natural) is
      D : constant array (0 .. 9) of Character := "0123456789";

   begin
      if N > 9 then
         Write_Integer (N / 10);
      end if;
      Text_IO.Put (D (N rem 10));
   end Write_Integer;

The declaration of the array D shows a couple of new things. First we can declare an array without a separate type declaration. Second we can make the array be a constant by using the "constant" keyword. All constants must be initialized (since there is no way to assign to them), and in this case we can initialize D with a string of appropriate length.

Another approach to the Put call above is:

   Text_IO.Put (Character'Val (D (N rem 10) + Character'Pos ('0'));

The built in Character'Pos function converts a character to its internal code, and the built in Character'Val function converts such an internal code back to a Character value. The expression here makes use of the fact that the codes for the characters '0'..'9' are contiguous. The array is easier to understand, but the fiddling with codes may be a bit more efficient.

Finally, there is another even more useful attribute function (an attribute function is one of these built-in functions using the apostrophe).

   with Text_IO;
   procedure Write_Integer (N : Natural) is
   begin
      Text_IO.Put (Natural'Image (N));
   end Write_Integer;

The function x'Image, for any integer type x, converts the value of its argument (N in this case) to a string of digits with a leading blank or minus sign (for negative values). This means that this version of Write_Integer is not quite the same as the previous one, because of the extra space. Here is another version that avoids this extra blank, and also works fine for negative values:

   with Text_IO;
   procedure Write_Integer (N : Integer) is
      Str : constant String := Integer'Image (N);

   begin
      if Str (Str'First) /= ' ' then
         Put (Str);
      else
         Put (Str (Str'First + 1 .. Str'Last));
      end if;
   end Write_Integer;

This illustrates a couple of new points. First, a constant String can be initialized with a computed expression which provides both the bounds and value for the string constant. Second, the notation arr (F .. L) is called an array slice, it allows a contiguous section of a one-dimensional array to be selected. Here we are using the slice to remove the junk blank.

Used by permission of Robert Dewar, New York University ($Id$)