Learning Ada 2: more on packages, naming conventions, and bits of types
Packages are very importat to organize your project, like modules in other languages, and they are also useful to “encapsulate data” — you don't need to go object oriented in order to benefit from that.
On package specs and body again
Reading the previous article you could have deduced the idea that for each specification files (.ads
) you need a body file (.adb
).
It isn't so.
In fact the specification contains whatever you want to make it usable from the users of your package, and the body contains the rest, and the implementations. In case when there's such a thing.
But you can use a package just as a container of string constants, for example.
package Greetings is
Hello : constant String := "hello";
Hi : constant String := "hi";
Good_Morning : constant String := "good morning";
What_s_Up : constant String := "what's up";
end Greetings;
That's it. No .adb
file.
In Greetings
we want some behaviour: we want to add “greeting swears”, but only if you are an adult. An adult is a person, hence we have a Person
package — only specs (short for specifications file) needed, person.ads
.
package Person is
subtype Age is Natural range 0 .. 140;
Adult_Age : constant Age := 18;
end Person;
I am going to say something about subtype Age is Natural range 0 .. 140
later; for now, just consider it a type of variable which is useful to hold a number which represents the age of a person.
Our original greetings.ads
grows like this:
with Person;
package Greetings is
Unknown_Greeting : exception;
-- ...
procedure Swear (Greeting : String;
Speaker_Age : Person.Age := Person.Age'First);
private
Swear_Hello : constant String := "fucking hello";
Swear_Hi : constant String := "fuck you";
Swear_Morning : constant String := "good fucking morning";
Swear_Whats_Up : constant String := "what's the fuck";
end Greetings;
All the swearing greetings are private: only the procecure Swear
can access them and decide to swear or not according to the age. The Speaker_Age
is a default parameter: if omitted, it takes the value of the first valid value of the range of the type Age
, i.e., 0.
Now the package Greetings
must have a body, because we need to “complete” the specs with the implementation of Swear
:
with Ada.Text_IO; use Ada.Text_IO;
package body Greetings is
procedure Swear (Greeting : String;
Speaker_Age : Person.Age := Person.Age'First) is
begin
if Speaker_Age >= Person.Adult_Age then
Put_Line ((if Greeting = Hello then Swear_Hello
elsif Greeting = Hi then Swear_Hi
elsif Greeting = Good_Morning then Swear_Morning
elsif Greeting = What_s_Up then Swear_Whats_Up
else raise Unknown_Greeting));
else
Put_Line (Hello);
end if;
end Swear;
end Greetings;
The if
as an expression is an Ada 2012 feature, so you need a compiler that understands Ada 2012.
If the greeting is said by an adult, then it becomes a swearing greeting, but only if it matches one of the known greetings; otherwise an exception is raised. For example
Greetings.Swear (Greetings.Hello, 20);
works, but
Greetings.Swear ("good evening", 20);
raises.
You can also see that in Greeter we have
with Ada.Text_IO; use Ada.Text_IO;
because in fact we use Put_Line
. We need it even if the Greetings package had a with Ada.Text_IO
line. Compare with C (and similarly C++):
/* in file greetings.h */
#include <stdio.h>
/* in file greeter.c */
#include "greetings.h"
In greeter.c
we can call printf()
, no need to include stdio.h
again, in fact is already included by greetings.h
. This is because C is “file based” and we are really including the file (a work done by the preprocessor).
In Ada, even if packages are inside files (and there are rules, at least in GNAT, to name correctly a file according to the package it contains), the “logical unit” is the package, and with Xxx
doesn't mean “include the file Xxx.ads” but “this unit is using the feature of the package Xxx” (then, thanks to naming conventions, the compiler knows where to look for the package Xxx).
I have to say that C and C++ are behind other languages with respect to modularity. In C and C++ the preprocessor instruction #include
literally includes the file. Maybe future C++ standard will change this according to existing proposals, but so far C/C++ programmers haven't anything like packages in Ada (or Java, or other languages).
Variables inside a package
Already seen the Y
(and X
to test symbol clashes) in the Greeter package of the previous article. Now, one can wonder if in each unit using the package, the Y
is the same or is different each time. The answer is it's the very same thing, but let's check it in practice using something we have in this lesson: the package Person
:
package Person is
subtype Age is Natural range 0 .. 140;
Adult_Age : constant Age := 18;
Unit_Age : Age;
end Person;
Then, let's use this package in two different packages, A and B, each with a procedure which prints and then modifies Unit_Age
's value.
-- a.ads
package A is
procedure Print_Modify;
end A;
-- b.ads
package B is
procedure Print_Modify;
end B;
You compile test_pkg.adb
; the output of a run is
Test_Pkg: 0
0
Test_Pkg: 10
10
Test_Pkg: 20
The private part to complete a type
In a package specs you have a “public” part and a private part, but when you define a new type maybe you don't want the client (i.e., the user of the package) to see and access the details of the type — it is a sort of opaque type. But then, they can't use it in anyway: it would be unuseful, except if the package also give subprograms to manipulate the type. E.g.:
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
package Person2 is
subtype Age_Type is Natural range 0 .. 140;
type Person is private;
procedure Set_Name (A_Person : out Person;
Name : in Unbounded_String);
procedure Set_Age (A_Person : out Person;
Her_Age : in Age_Type);
function Get_Name (A_Person : Person) return Unbounded_String;
function Get_Age (A_Person : Person) return Age_Type;
function Is_Adult (A_Person : Person) return Boolean;
private
Adult_Age : constant Age_Type := 18;
type Person is
record
Name : Unbounded_String;
Age : Age_Type;
end record;
end Person2;
The first argument is used as output, i.e., it is modified only. If we have
I_Am_Hugo : Person;
the only way to manipulate this “thing” is to use the functions of the package. The “thing” can be in fact considered as an “object”, even if not yet like in the OO paradigm — but almost.
That is, packages allow for data encapsulation and information hiding.
The complete (boring) example's files are:
Packages in a package
Packages can be nested, both in private or public part.
package N1 is
C1 : constant := 1;
package N2 is
type N2_Type is new Integer;
PC : constant := 2;
private
Cnt : Integer;
end N2;
C2 : N2.N2_Type;
private
package N3 is
PC : constant := 3;
private
Cnt : Integer;
end N3;
end N1;
Wherever you do with N1
, you can access N1.C1
, N1.N2.PC
, but not N1.N3.PC
(except in the body of the package N1
). Also, inside the package there can be reference to other package or declarations in general, provided that they are already seen. For instance C2
can refer to N2
because N2
comes before. If we move C2
above N2
, then C2
can't be N2.N2_Type
, because N2
isn't seen yet ("N2" undefined).
It seems that there isn't a way to do a forward declaration like in C++.
class ClassB; // forward decl.
class ClassA
{
private:
ClassB* m_that;
// ...
};
// ...
class ClassB
{
// ...
};
Package inside a subprogram
You can put a package also inside the declarative part of a procedure or function. The declarative part of a subprogram can become rather thick, containing types, functions, procedures, variables, constants, and packages too!
The example, thick_hello.adb
, is totally meaningless, but shows how a “full” package can be inside the declarative part of a procedure!
The code contains also other interesting things, but they are not package-related, so we'll see them later.
Hierarchies
Instead of nesting packages, we can arrange them in a sort of hierarchy.
package Father is
-- ...
end Father;
package Father.Son is
-- ...
end Father.Son;
The child package Son
can see everything the package Father
has. (There are exceptions, but I haven't encoutered them yet).
It looks like a more useful feature with respect to the nesting.
Currently I can't think of a meaningful example, though. A not-so-meaningful example is Father.Son. This example is more interesting from the point of view of the types and the inherited operations, indeed. I consider this aspects in the next section.
The example just shows that Father.Son
sees Father
, no need for with Father
inside the package Father.Son
: we use Father_Type
without problem, and also the Father
's private procedure Father_What
, as if it were part of the private part of the child package Father.Son
.
Naming conventions
The Father
- Father.Son
example shows also a simple naming convention: you maybe have already deduced that if you have the package This_Is_A_Package
, the file which keeps it has name this_is_a_package.ads
(or .adb
). Now you can deduce that the dot becomes a -
; so the child package Father.Son
is inside the file father-son.ads
(and father-son.adb
).
These rules are easy.
There's an exception: the -
becomes ~
when the previous and only letter, the first one of the file name, is one of a
, g
, i
, s
, because these are used for standard packages.
So the specs for the child package I.A.Robot
is in a file named i~a-robot.ads
.
If you want to use non-conventional file names, GNAT offers a pragma which tells where to find the code for a certain package; this pragma must be put inside a configuration file (gnat.adc
). I haven't experimented yet with this, so I'll say something more when I will do.
A little bit of a type
Ada is very type oriented, so to say; its type system is a huge (and important) part of it, because types establish the domain of your problem… Thus, another thing you likely will put into your specifications file are a bunch a “custom” type definitions, according to the specific problem you are trying to “model”.
For example,
subtype Age is Natural'Base range 0 .. 140;
Here we are saying that Age
is a subtype of the base type of Natural
with a restricted range of values, between 0 (newborn) and 140 (a very old person). Of course this age is good for human beings — unless they are biblical human beings! —, not for trees…
The type Natural
is itself a subtype of Integer
, defined like this:
subtype Natural is Integer range 0 .. Integer'Last;
This is why I think the 'Base
attribute isn't necessary:
subtype Age is Integer range 0 .. 140;
-- or
subtype Age is Natural range 0 .. 140;
All these three subtypes work (that is, the compiler compiles without complaints), but I am not sure which is the more “correct”. If I had to pick one, I'd say Natural range 0 .. 140
, and that's why: negative ages are nonsense, so the age is a natural number; then unlikely it goes towards infinite, or any very huge value; hence we are restricting the range from 0 .. Integer'Last
to 0 .. 140
. If a woman reaches 141, our system fails…!
There are differences I haven't studied yet deeply. One of these is about the operations you can do on your type, what is inherited from the parent type e what's not.
There's another interesting fact I've stumbled upon when writing the example Thick_Hello
: operations on a type defined in a package should be “qualified”, unless you add use My_Pack
. This we've already seen, but now consider when your type inherit basic operations of another type.
package Counter is
type C_Type is mod 17;
-- ...
end Counter;
If then we have
T : Counter.C_Type := 0;
can we do inherited operations on T
? E.g., can we write
if T = Counter.Value then
Counter.Increment;
end if;
We want to compare T
with the value returned by Counter.Value
. We can't, because the operations allowed on C_Type
are in the package Counter
, and we didn't use Counter
.
So we should write:
if Counter."=" (T, Counter.Value) then
Counter.Increment;
end if;
But this is super ugly! What can we do if we don't want to write operators in that way, but we also don't want to import all the symbols in Counter
?
This is what the following line is for:
use type Counter.C_Type;
The type C_Type
is a “modulus” type; we know better other types of this kind, e.g.
type Byte is mod 2**8;
For such a type a “wrap-around” arithmetic is at work; in fact if we have the byte, here written as a binary number,
11111111
and we add 1, then we obtain 0 (no overflow, since it is how it is meant to work!) In Ada a binary number can be written as 2#10101010#
, and if we want to visually separate nibbles, we can write 2#1010_1010#
, and this can be done with any number in all accepted bases: a billion can be written as 1_000_000_000
!
This “wrap-around arithmetic” maybe it isn't exactly what math guys would call modular arithmetic, but at some extent it is, and I think the keyword mod
tells that.
Now, let's see again the Father.Son
example. The package Father
defines the type Father_Type
(and a private procedure on it). The child package Father.Son
defines a subtype of Father_Type, Son_Type
, and it also pretends to provide for the operator +
when the left operand is Son_Type
and the right operand is Father_Type
(and the result is a Father_Type
, too).
If you compile and run the example test_children.adb
the output will be:
using this
116
105
Despite the fact that we did use type Father.Son.Son_Type
, the +
operator is given by the one for the base type of Father_Type
, that is, Integer
.
On the other hand, Father.Son
has also the type Sonic_Type
; it can hold the same values as Son_Type
, but it's a type. The operation +
for Sonic_Type
in Father.Son
can be used, but we need
use type Father.Son.Sonic_Type;
to avoid the ugly syntax. We also need, for the same reason
use type Father.Father_Type;
What does it happen if we also say
use type Father.Son.Son_Type;
? We obtain a warning which explains us again an important difference between type (deriving) and subtype (subtyping).
warning: "Father_Type" is already use-visible through previous use type clause
Both use type Father.Son.Son_Type
and use type Father.Father_Type
make visible the operations for the same type.
Sonic_Type
isn't a subtype, hence it doesn't inherit from its base; instead you can provide your own operation, and it gets called as expected:
-- ...
B : Father.Father_Type := 100;
C : constant Father.Son.Sonic_Type := 20;
-- ...
R : Integer;
-- ...
R := C + B;
This calls Father.Son."+" (Left : Sonic_Type; Right : Father_Type)
. But B + C
(Father_Type
+ Sonic_Type
) won't work, because it's an operation on two types, and neither Father
nor Father.Son
defines it — by the way, only Father.Son
can define it: a child package can see the parent, but the parent can't see the child, therefore there's no way it can declare and implement any subprogram which needs to see Father.Son.Sonic_Type
!
Incidentally, we've also seen that overloading works. Overloading is another feature typical of OO languages.
As said, the type system is huge, and also powerful and a great tool for safer software, provided you use it correctly: e.g., if you are writing a simulation of a car and you use the Float
type for the speed, that's fine, but you are not taking advantage of the Ada's typing system. I bet your speed absolute value will be less than the speed of light, to start with!
The topic is large and will be explored in future articles.
Initialization of a package
A package can have a initialization part:
package body My_Pack is
Internal_Value : Integer;
-- ...
begin
Internal_Value := 25;
end My_Pack;
Don't forget that when with
-ed in two places, you haven't a copy: it's the same package, the same variables: we call Suppack.Print
, which calls My_Pack.Get
and prints the returned value, and this is 100 after My_Pack.Set (100)
. It works like a singleton. We also see that the initialization is called once.
Separate!
Sometimes you have carved your nice package, you are implementing its body, but you don't want to stuff everything into it, maybe some procedure is really long and complex, or you would like to put it in a separate file just to better keep an eye on it.
You can.
In your body, you'll say that a certain procedure or function is separate.
package body Separated is
procedure Watch_Me is separate;
end Separated;
The file where you put the actual implementation must be named like package-procedure.adb
, the obvious way, so to say, since you would call Watch_Me
like this: Separated.Watch_Me
, and I've already talked about naming convention: dots replaced by -
, and lowercase (important on systems which distinguish lowercase and uppercase in file names).
Thus in separated-watch_me.adb
you put the implementation:
with Ada.Text_IO; use Ada.Text_IO;
separate (Separated)
procedure Watch_Me is
begin
Put_Line ("Better keep me separated");
end Watch_Me;
See separated.ads, separated.adb, and separated-watch_me.adb.
Conclusion
Having the code pasted into pastebin is good, except that I've read that they can delete the post if it hasn't accesses for a while (unless you pay for it). And the same is true for hastebin. This is why I've opened a github account and created the repository learning-ada.
Sources for this article are here.