Modular programming
So far, our examples have been simple standalone subprograms. Ada is helpful in that regard, since it allows arbitrary declarations in a declarative part. We were thus able to declare our types and variables in the bodies of main procedures.
However, it is easy to see that this is not going to scale up for real-world applications. We need a better way to structure our programs into modular and distinct units.
Ada encourages the separation of programs into multiple packages and sub-packages, providing many tools to a programmer on a quest for a perfectly organized code-base.
Packages
Here is an example of a package declaration in Ada:
package Week is Mon : constant String := "Monday"; Tue : constant String := "Tuesday"; Wed : constant String := "Wednesday"; Thu : constant String := "Thursday"; Fri : constant String := "Friday"; Sat : constant String := "Saturday"; Sun : constant String := "Sunday"; end Week;
And here is how you use it:
with Ada.Text_IO; use Ada.Text_IO; with Week; -- References the Week package, and -- adds a dependency from Main to Week procedure Main is begin Put_Line ("First day of the week is " & Week.Mon); end Main;
Packages let you make your code modular, separating your programs into semantically significant units. Additionally the separation of a package's specification from its body (which we will see below) can reduce compilation time.
While the with
clause indicates a dependency, you can see in the example
above that you still need to prefix the referencing of entities from the Week
package by the name of the package. (If we had included a use Week
clause,
then such a prefix would not have been necessary.)
Accessing entities from a package uses the dot notation, A.B
, which is
the same notation as the one used to access record fields.
A with
clause can only appear in the prelude of a compilation unit
(i.e., before the reserved word, such as procedure
, that marks the
beginning of the unit). It is not allowed anywhere else. This rule is only
needed for methodological reasons: the person reading your code should be able
to see immediately which units the code depends on.
In other languages
Packages look similar to, but are semantically very different from, header files in C/C++.
The first and most important distinction is that packages are a language-level mechanism. This is in contrast to a
#include
'd header file, which is a functionality of the C preprocessor.An immediate consequence is that the
with
construct is a semantic inclusion mechanism, not a text inclusion mechanism. Hence, when youwith
a package, you are saying to the compiler "I'm depending on this semantic unit", and not "include this bunch of text in place here".The effect of a package thus does not vary depending on where it has been
with
ed from. Contrast this with C/C++, where the meaning of the included text depends on the context in which the#include
appears.This allows compilation/recompilation to be more efficient. It also allows tools like IDEs to have correct information about the semantics of a program. In turn, this allows better tooling in general, and code that is more analyzable, even by humans.
An important benefit of Ada with
clauses when compared to
#include
is that it is stateless. The order of with
and
use
clauses does not matter, and can be changed without side
effects.
In the GNAT toolchain
The Ada language standard does not mandate any particular relationship
between source files and packages; for example, in theory you can put all
your code in one file, or use your own file naming conventions. In
practice, however, an implementation will have specific rules. With GNAT,
each top-level compilation unit needs to go into a separate file. In the
example above, the Week
package will be in an .ads
file (for Ada
specification), and the Main
procedure will be in an .adb
file
(for Ada body).
Using a package
As we have seen above, the with
clause indicates a dependency on another
package. However, every reference to an entity coming from the Week
package had to be prefixed by the full name of the package. It is possible to
make every entity of a package visible directly in the current scope, using the
use
clause.
In fact, we have been using the use
clause since almost the beginning of
this tutorial.
with Ada.Text_IO; use Ada.Text_IO; -- ^ Make every entity of the -- Ada.Text_IO package -- directly visible. with Week; procedure Main is use Week; -- Make every entity of the Week -- package directly visible. begin Put_Line ("First day of the week is " & Mon); end Main;
As you can see in the example above:
Put_Line
is a subprogram that comes from theAda.Text_IO
package. We can reference it directly because we haveuse
d the package at the top of theMain
unit.Unlike
with
clauses, ause
clause can be placed either in the prelude, or in any declarative region. In the latter case theuse
clause will have an effect in its containing lexical scope.
Package body
In the simple example above, the Week
package only has
declarations and no body. That's not a mistake: in a package specification,
which is what is illustrated above, you cannot declare bodies. Those have to be
in the package body.
package Operations is -- Declaration function Increment_By (I : Integer; Incr : Integer := 0) return Integer; function Get_Increment_Value return Integer; end Operations;package body Operations is Last_Increment : Integer := 1; function Increment_By (I : Integer; Incr : Integer := 0) return Integer is begin if Incr /= 0 then Last_Increment := Incr; end if; return I + Last_Increment; end Increment_By; function Get_Increment_Value return Integer is begin return Last_Increment; end Get_Increment_Value; end Operations;
Here we can see that the body of the Increment_By
function has to be
declared in the body. Coincidentally, introducing a body allows us to put the
Last_Increment
variable in the body, and make them inaccessible to the
user of the Operations
package, providing a first form of encapsulation.
This works because entities declared in the body are only visible in the body.
This example shows how Last_Increment
is used indirectly:
with Ada.Text_IO; use Ada.Text_IO; with Operations; procedure Main is use Operations; I : Integer := 0; R : Integer; procedure Display_Update_Values is Incr : constant Integer := Get_Increment_Value; begin Put_Line (Integer'Image (I) & " incremented by " & Integer'Image (Incr) & " is " & Integer'Image (R)); I := R; end Display_Update_Values; begin R := Increment_By (I); Display_Update_Values; R := Increment_By (I); Display_Update_Values; R := Increment_By (I, 5); Display_Update_Values; R := Increment_By (I); Display_Update_Values; R := Increment_By (I, 10); Display_Update_Values; R := Increment_By (I); Display_Update_Values; end Main;
Child packages
Packages can be used to create hierarchies. We achieve this by using child
packages, which extend the functionality of their parent package. One example
of a child package that we've been using so far is the Ada.Text_IO
package. Here, the parent package is called Ada
, while the child package
is called Text_IO
. In the previous examples, we've been using the
Put_Line
procedure from the Text_IO
child package.
Important
Ada also supports nested packages. However, since they can be more complicated to use, the recommendation is to use child packages instead. Nested packages will be covered in the advanced course.
Let's begin our discussion on child packages by taking our previous
Week
package:
package Week is Mon : constant String := "Monday"; Tue : constant String := "Tuesday"; Wed : constant String := "Wednesday"; Thu : constant String := "Thursday"; Fri : constant String := "Friday"; Sat : constant String := "Saturday"; Sun : constant String := "Sunday"; end Week;
If we want to create a child package for Week
, we may write:
package Week.Child is function Get_First_Of_Week return String; end Week.Child;
Here, Week
is the parent package and Child
is the child
package. This is the corresponding package body of Week.Child
:
package body Week.Child is function Get_First_Of_Week return String is begin return Mon; end Get_First_Of_Week; end Week.Child;
In the implementation of the Get_First_Of_Week
function, we can use
the Mon
string directly, even though it was declared in the parent
package Week
. We don't write with Week
here because all
elements from the specification of the Week
package — such as
Mon
, Tue
and so on — are visible in the child package
Week.Child
.
Now that we've completed the implementation of the Week.Child
package,
we can use elements from this child package in a subprogram by simply writing
with Week.Child
. Similarly, if we want to use these elements directly,
we write use Week.Child
in addition. For example:
with Ada.Text_IO; use Ada.Text_IO; with Week.Child; use Week.Child; procedure Main is begin Put_Line ("First day of the week is " & Get_First_Of_Week); end Main;
Child of a child package
So far, we've seen a two-level package hierarchy. But the hierarchy that we
can potentially create isn't limited to that. For instance, we could extend
the hierarchy of the previous source code example by declaring a
Week.Child.Grandchild
package. In this case, Week.Child
would
be the parent of the Grandchild
package. Let's consider this
implementation:
package Week.Child.Grandchild is function Get_Second_Of_Week return String; end Week.Child.Grandchild;package body Week.Child.Grandchild is function Get_Second_Of_Week return String is begin return Tue; end Get_Second_Of_Week; end Week.Child.Grandchild;
We can use this new Grandchild
package in our test application in the
same way as before: we can reuse the previous test application and adapt the
with
and use
, and the function call. This is the updated code:
with Ada.Text_IO; use Ada.Text_IO; with Week.Child.Grandchild; use Week.Child.Grandchild; procedure Main is begin Put_Line ("Second day of the week is " & Get_Second_Of_Week); end Main;
Again, this isn't the limit for the package hierarchy. We could continue to
extend the hierarchy of the previous example by implementing a
Week.Child.Grandchild.Grand_grandchild
package.
Multiple children
So far, we've seen a single child package of a parent package. However, a
parent package can also have multiple children. We could extend the example
above and implement a Week.Child_2
package. For example:
package Week.Child_2 is function Get_Last_Of_Week return String; end Week.Child_2;
Here, Week
is still the parent package of the Child
package,
but it's also the parent of the Child_2
package. In the same way,
Child_2
is obviously one of the child packages of Week
.
This is the corresponding package body of Week.Child_2
:
package body Week.Child_2 is function Get_Last_Of_Week return String is begin return Sun; end Get_Last_Of_Week; end Week.Child_2;
We can now reference both children in our test application:
with Ada.Text_IO; use Ada.Text_IO; with Week.Child; use Week.Child; with Week.Child_2; use Week.Child_2; procedure Main is begin Put_Line ("First day of the week is " & Get_First_Of_Week); Put_Line ("Last day of the week is " & Get_Last_Of_Week); end Main;
Visibility
In the previous section, we've seen that elements declared in a parent package specification are visible in the child package. This is, however, not the case for elements declared in the package body of a parent package.
Let's consider the package Book
and its child
Additional_Operations
:
package Book is Title : constant String := "Visible for my children"; function Get_Title return String; function Get_Author return String; end Book;package Book.Additional_Operations is function Get_Extended_Title return String; function Get_Extended_Author return String; end Book.Additional_Operations;
This is the body of both packages:
package body Book is Author : constant String := "Author not visible for my children"; function Get_Title return String is begin return Title; end Get_Title; function Get_Author return String is begin return Author; end Get_Author; end Book;package body Book.Additional_Operations is function Get_Extended_Title return String is begin return "Book Title: " & Title; end Get_Extended_Title; function Get_Extended_Author return String is begin -- "Author" string declared in the body -- of the Book package is not visible -- here. Therefore, we cannot write: -- -- return "Book Author: " & Author; return "Book Author: Unknown"; end Get_Extended_Author; end Book.Additional_Operations;
In the implementation of the Get_Extended_Title
, we're using the
Title
constant from the parent package Book
. However, as
indicated in the comments of the Get_Extended_Author
function, the
Author
string — which we declared in the body of the Book
package — isn't visible in the Book.Additional_Operations
package. Therefore, we cannot use it to implement the
Get_Extended_Author
function.
We can, however, use the Get_Author
function from Book
in the
implementation of the Get_Extended_Author
function to retrieve this
string. Likewise, we can use this strategy to implement the
Get_Extended_Title
function. This is the adapted code:
package body Book.Additional_Operations is function Get_Extended_Title return String is begin return "Book Title: " & Get_Title; end Get_Extended_Title; function Get_Extended_Author return String is begin return "Book Author: " & Get_Author; end Get_Extended_Author; end Book.Additional_Operations;
This is a simple test application for the packages above:
with Ada.Text_IO; use Ada.Text_IO; with Book.Additional_Operations; use Book.Additional_Operations; procedure Main is begin Put_Line (Get_Extended_Title); Put_Line (Get_Extended_Author); end Main;
By declaring elements in the body of a package, we can implement encapsulation in Ada. Those elements will only be visible in the package body, but nowhere else. This isn't, however, the only way to achieve encapsulation in Ada: we'll discuss other approaches in the Privacy chapter.
Renaming
Previously, we've mentioned that
subprograms can be renamed. We can rename
packages, too. Again, we use the renames
keyword for that. The following
example renames the Ada.Text_IO
package as TIO
:
with Ada.Text_IO; procedure Main is package TIO renames Ada.Text_IO; begin TIO.Put_Line ("Hello"); end Main;
We can use renaming to improve the readability of our code by using shorter
package names. In the example above, we write TIO.Put_Line
instead of
the longer version (Ada.Text_IO.Put_Line
). This approach is especially
useful when we don't use
packages and want to avoid that the code
becomes too verbose.
Note we can also rename subprograms and objects inside packages. For instance,
we could have just renamed the Put_Line
procedure in the source code
example above:
with Ada.Text_IO; procedure Main is procedure Say (Something : String) renames Ada.Text_IO.Put_Line; begin Say ("Hello"); end Main;
In this example, we rename the Put_Line
procedure to Say
.