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.
Here is an example of a package declaration in Ada:
And here is how you use it:
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.
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.
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
withconstruct is a semantic inclusion mechanism, not a text inclusion mechanism. Hence, when you
witha 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
withed from. Contrast this with C/C++, where the meaning of the included text depends on the context in which the
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
use clauses does not matter, and can be changed without side
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
(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
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
In fact, we have been using the
use clause since almost the beginning of
As you can see in the example above:
Put_Lineis a subprogram that comes from the
Ada.Text_IOpackage. We can reference it directly because we have
used the package at the top of the
useclause can be placed either in the prelude, or in any declarative region. In the latter case the
useclause will have an effect in its containing lexical scope.
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.
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:
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
package. Here, the parent package is called
Ada, while the child package
Text_IO. In the previous examples, we've been using the
Put_Line procedure from the
Text_IO child package.
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
If we want to create a child package for
Week, we may write:
Week is the parent package and
Child is the child
package. This is the corresponding package body of
In the implementation of the
Get_First_Of_Week function, we can use
Mon string directly, even though it was declared in the parent
Week. We don't write
with Week here because all
elements from the specification of the
Week package — such as
Tue and so on — are visible in the child package
Now that we've completed the implementation of the
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,
use Week.Child in addition. For example:
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,
be the parent of the
Grandchild package. Let's consider this
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
use, and the function call. This is the updated code:
Again, this isn't the limit for the package hierarchy. We could continue to
extend the hierarchy of the previous example by implementing a
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:
Week is still the parent package of the
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
This is the corresponding package body of
We can now reference both children in our test application:
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
This is the body of both packages:
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
package — isn't visible in the
package. Therefore, we cannot use it to implement the
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:
This is a simple test application for the packages above:
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.
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
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