up↑ |
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!
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.
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.
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.
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.
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.
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.
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!
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.
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!
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.
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.
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.
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!
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'Last
x.
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.
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$) |