Packages

Pure program and library units

Todo

Complete section!

Package renaming

We've seen in the Introduction to Ada course that we can rename packages.

Grouping packages

A use-case that we haven't mentioned in that course is that we can apply package renaming to group individual packages into a common hierarchy. For example:

package Driver_M1 is end Driver_M1;
package Driver_M2 is end Driver_M2;
package Drivers with Pure is end Drivers;
with Driver_M1; package Drivers.M1 renames Driver_M1;
with Driver_M2; package Drivers.M2 renames Driver_M2;

Here, we're renaming the Driver_M1 and Driver_M2 packages as child packages of the Drivers package, which is a pure package.

Important

Note that a package that is renamed as a child package cannot refer to information from its (non-renamed) parent. In other words, Driver_M1 (renamed as Drivers.M1) cannot refer to information from the Drivers package. For example:

package Driver_M1 is Counter_2 : Integer := Drivers.Counter; end Driver_M1;
package Drivers is Counter : Integer := 0; end Drivers;
with Driver_M1; package Drivers.M1 renames Driver_M1;

As expected, compilation fails here because Drivers.Counter isn't visible in Driver_M1, even though the renaming (Drivers.M1) creates a virtual hierarchy.

Child of renamed package

Note that we cannot create a child package using a parent package name that was introduced by a renaming. For example, let's say we want to create a child package Ext for the Drivers.M1 package we've seen earlier. We cannot just declare a Drivers.M1.Ext package like this:

package Drivers.M1.Ext is

end Drivers.M1.Ext;

because the parent unit cannot be a renaming. The solution is to actually extend the original (non-renamed) package:

package Driver_M1.Ext is end Driver_M1.Ext;
-- A package called Drivers.M1.Ext is -- automatically available! with Drivers.M1.Ext; procedure Dummy is begin null; end Dummy;

This works fine because any child package of a package P is also a child package of a renamed version of P. (Therefore, because Ext is a child package of Driver_M1, it is also a child package of the renamed Drivers.M1 package.)

Backwards-compatibility via renaming

We can also use renaming to ensure backwards-compatibility when changing the package hierarchy. For example, we could adapt the previous source-code by:

  • converting Driver_M1 and Driver_M2 to child packages of Drivers, and

  • using package renaming to mimic the original names (Driver_M1 and Driver_M2).

This is the adapted code:

package Drivers with Pure is end Drivers;
-- We've converted Driver_M1 to -- Drivers.M1: package Drivers.M1 is end Drivers.M1;
-- We've converted Driver_M2 to -- Drivers.M2: package Drivers.M2 is end Drivers.M2;
-- Original Driver_M1 package still -- available via package renaming: with Drivers.M1; package Driver_M1 renames Drivers.M1;
-- Original Driver_M2 package still -- available via package renaming: with Drivers.M2; package Driver_M2 renames Drivers.M2;

Now, M1 and M2 are actual child packages of Drivers, but their original names are still available. By doing so, we ensure that existing software that makes use of the original packages doesn't break.

Private packages

In this section, we discuss the concept of private packages. However, before we proceed with the discussion, let's recapitulate some important ideas that we've seen earlier.

In the Introduction to Ada course, we've seen that encapsulation plays an important role in modular programming. By using the private part of a package specification, we can disclose some information, but, at the same time, prevent that this information gets accessed where it shouldn't be used directly. Similarly, we've seen that we can use the private part of a package to distinguish between the partial and full view of a data type.

The main application of private packages is to create private child packages, whose purpose is to serve as internal implementation packages within a package hierarchy. By doing so, we can expose the internals to other public child packages, but prevent that external clients can directly access them.

As we'll see next, there are many rules that ensure that internal visibility is enforced for those private child packages. At the same time, the same rules ensure that private packages aren't visible outside of the package hierarchy.

Declaration and usage

We declare private packages by using the private keyword. For example, let's say we have a package named Data_Processing:

package Data_Processing is -- ... end Data_Processing;

We simply write private package to declare a private child package named Calculations:

private package Data_Processing.Calculations is -- ... end Data_Processing.Calculations;

Let's see a complete example:

package Data_Processing is type Data is private; procedure Process (D : in out Data); private type Data is null record; end Data_Processing;
private package Data_Processing.Calculations is procedure Calculate (D : in out Data); end Data_Processing.Calculations;
with Data_Processing.Calculations; use Data_Processing.Calculations; package body Data_Processing is procedure Process (D : in out Data) is begin Calculate (D); end Process; end Data_Processing;
package body Data_Processing.Calculations is procedure Calculate (D : in out Data) is begin -- Dummy implementation... null; end Calculate; end Data_Processing.Calculations;
with Data_Processing; use Data_Processing; procedure Test_Data_Processing is D : Data; begin Process (D); end Test_Data_Processing;

In this example, we refer to the private child package Calculations in the body of the Data_Processing package — by simply writing with Data_Processing.Calculations. After that, we can call the Calculate procedure normally in the Process procedure.

Private sibling packages

We can introduce another private package Advanced_Calculations as a child of Data_Processing and refer to the Calculations package in its specification:

package Data_Processing is type Data is private; procedure Process (D : in out Data); private type Data is null record; end Data_Processing;
private package Data_Processing.Calculations is procedure Calculate (D : in out Data); end Data_Processing.Calculations;
with Data_Processing.Calculations; use Data_Processing.Calculations; private package Data_Processing.Advanced_Calculations is procedure Advanced_Calculate (D : in out Data) renames Calculate; end Data_Processing.Advanced_Calculations;
with Data_Processing.Advanced_Calculations; use Data_Processing.Advanced_Calculations; package body Data_Processing is procedure Process (D : in out Data) is begin Advanced_Calculate (D); end Process; end Data_Processing;
package body Data_Processing.Calculations is procedure Calculate (D : in out Data) is begin -- Dummy implementation... null; end Calculate; end Data_Processing.Calculations;
with Data_Processing; use Data_Processing; procedure Test_Data_Processing is D : Data; begin Process (D); end Test_Data_Processing;

Note that, in the body of the Data_Processing package, we're now referring to the new Advanced_Calculations package instead of the Calculations package.

Referring to a private child package in the specification of another private child package is OK, but we cannot do the same in the specification of a non-private package. For example, let's change the specification of the Advanced_Calculations and make it non-private:

with Data_Processing.Calculations; use Data_Processing.Calculations; package Data_Processing.Advanced_Calculations is procedure Advanced_Calculate (D : in out Data) renames Calculate; end Data_Processing.Advanced_Calculations;

Now, the compilation doesn't work anymore. However, we could still refer to Calculations packages in the body of the Advanced_Calculations package:

package Data_Processing.Advanced_Calculations is procedure Advanced_Calculate (D : in out Data); end Data_Processing.Advanced_Calculations;
with Data_Processing.Calculations; use Data_Processing.Calculations; package body Data_Processing.Advanced_Calculations is procedure Advanced_Calculate (D : in out Data) is begin Calculate (D); end Advanced_Calculate; end Data_Processing.Advanced_Calculations;

This works fine as expected: we can refer to private child packages in the body of another package — as long as both packages belong to the same package tree.

Outside the package tree

While we can use a with-clause of a private child package in the body of the Data_Processing package, we cannot do the same outside the package tree. For example, we cannot refer to it in the Test_Data_Processing procedure:

with Data_Processing; use Data_Processing; with Data_Processing.Calculations; use Data_Processing.Calculations; procedure Test_Data_Processing is D : Data; begin Calculate (D); end Test_Data_Processing;

As expected, we get a compilation error because Calculations is only accessible within the Data_Processing, but not in the Test_Data_Processing procedure.

The same restrictions apply to child packages of private packages. For example, if we implement a child package of the Calculations package — let's name it Calculations.Child —, we cannot refer to it in the Test_Data_Processing procedure:

package Data_Processing.Calculations.Child is procedure Process (D : in out Data); end Data_Processing.Calculations.Child;
package body Data_Processing.Calculations.Child is procedure Process (D : in out Data) is begin Calculate (D); end Process; end Data_Processing.Calculations.Child;
with Data_Processing; use Data_Processing; with Data_Processing.Calculations.Child; use Data_Processing.Calculations.Child; procedure Test_Data_Processing is D : Data; begin Calculate (D); end Test_Data_Processing;

Again, as expected, we get an error because Calculations.Child — being a child of a private package — has the same restricted view as its parent package. Therefore, it cannot be visible in the Test_Data_Processing procedure as well. We'll discuss more about visibility later.

Note that subprograms can also be declared private. We'll see this in another section.

Important

We've discussed package renaming in a previous section. We can rename a package as a private package, too. For example:

package Driver_M1 is end Driver_M1;
package Drivers with Pure is end Drivers;
with Driver_M1; private package Drivers.M1 renames Driver_M1;

Obviously, Drivers.M1 has the same restrictions as any private package:

with Driver_M1; with Drivers.M1; procedure Test_Driver is begin null; end Test_Driver;

As expected, although we can have the Driver_M1 package in a with clause of the Test_Driver procedure, we cannot do the same in the case of the Drivers.M1 package because it is private.

In the Ada Reference Manual

Private with clauses

Definition and usage

A private with clause allows us to refer to a package in the private part of another package. For example, if we want to refer to package P in the private part of Data, we can write private with P:

package P is type T is null record; end P;
private with P; package Data is type T2 is private; private -- Information from P is -- visible here type T2 is new P.T; end Data;
with Data; use Data; procedure Main is A : T2; begin null; end Main;

As you can see in the example, as the information from P is available in the private part of Data, we can derive a new type T2 based on T from P. However, we cannot do the same in the visible part of Data:

private with P; package Data is -- ERROR: information from P -- isn't visible here type T2 is new P.T; end Data;

Also, the information from P is available in the package body. For example, let's declare a Process procedure in the P package and use it in the body of the Data package:

package P is type T is null record; procedure Process (A : T) is null; end P;
private with P; package Data is type T2 is private; procedure Process (A : T2); private -- Information from P is -- visible here type T2 is new P.T; end Data;
package body Data is procedure Process (A : T2) is begin P.Process (P.T (A)); end Process; end Data;
with Data; use Data; procedure Main is A : T2; begin null; end Main;

In the body of the Data, we can access information from the P package — as we do in the P.Process (P.T (A)) statement of the Process procedure.

Referring to private child package

There's one case where using a private with clause is the only way to refer to a package: when we want to refer to a private child package in another child package. For example, here we have a package P and its two child packages: Private_Child and Public_Child:

package P is end P;
private package P.Private_Child is type T is null record; end P.Private_Child;
private with P.Private_Child; package P.Public_Child is type T2 is private; private type T2 is new P.Private_Child.T; end P.Public_Child;
with P.Public_Child; use P.Public_Child; procedure Test_Parent_Child is A : T2; begin null; end Test_Parent_Child;

In this example, we're referring to the P.Private_Child package in the P.Public_Child package. As expected, this works fine. However, using a normal with clause doesn't work in this case:

with P.Private_Child; package P.Public_Child is type T2 is private; private type T2 is new P.Private_Child.T; end P.Public_Child;

This gives an error because the information from the P.Private_Child, being a private child package, cannot be accessed in the public part of another child package. In summary, unless both packages are private packages, it's only possible to access the information from a private package in the private part of a non-private child package.

In the Ada Reference Manual

Limited Visibility

Sometimes, we might face the situation where two packages depend on information from each other. Let's consider a package A that depends on a package B, and vice-versa:

with B; use B; package A is type T1 is record Value : T2; end record; end A;
with A; use A; package B is type T2 is record Value : T1; end record; end B;

Here, we have two mutually dependent types T1 and T2, which are declared in two packages A and B that refer to each other. These with clauses constitute a circular dependency, so the compiler cannot compile either of those packages.

One way to solve this problem is by transforming this circular dependency into a partial dependency. We do this by limiting the visibility — using a limited with clause. To use a limited with clause for a package P, we simply write limited with P.

If a package A has limited visibility of a package B, then all types from package B are visible as if they had been declared as incomplete types. For the specific case of the previous source-code example, this would be the limited visibility of package B from package A's perspective:

package B is

   --  Incomplete type
   type T2;

end B;

As we've seen previously,

  • we cannot declare objects of incomplete types, but we can declare access types and anonymous access objects of incomplete types. Also,

  • we can use anonymous access types to declare mutually dependent types.

Keeping this information in mind, we can now correct the previous code by using limited with clauses for package A and declaring the component of the T1 record using an anonymous access type:

limited with B; package A is type T1 is record Ref : access B.T2; end record; end A;
with A; use A; package B is type T2 is record Value : T1; end record; end B;

As expected, we can now compile the code without issues.

Note that we can also use limited with clauses for both packages. If we do that, we must declare all components using anonymous access types:

limited with B; package A is type T1 is record Ref : access B.T2; end record; end A;
limited with A; package B is type T2 is record Ref : access A.T1; end record; end B;

Now, both packages A and B have limited visibility of each other.

In the Ada Reference Manual

Limited visibility and private with clauses

We can limit the visibility and use private with clauses at the same time. For a package P, we do this by simply writing limited private with P.

Let's reuse the previous source-code example and convert types T1 and T2 to private types:

limited private with B; package A is type T1 is private; private -- Here, we have limited visibility -- of package B type T1 is record Ref : access B.T2; end record; end A;
private with A; package B is type T2 is private; private use A; -- Here, we have full visibility -- of package A type T2 is record Value : T1; end record; end B;

In this updated version of the source-code example, we have not only limited visibility of package B, but also, each package is just visible in the private part of the other package.

Limited visibility and other elements

It's important to mention that the limited visibility we've been discussing so far is restricted to type declarations — which are seen as incomplete types. In fact, when we use a limited with clause, all other declarations have no visibility at all! For example, let's say we have a package Info that declares a constant Zero_Const and a function Zero_Func:

package Info is function Zero_Func return Integer is (0); Zero_Const : constant := 0; end Info;

Also, let's say we want to use the information (from package Info) in package A. If we have limited visibility of package Info, however, this information won't be visible. For example:

limited private with Info; package A is type T1 is private; private type T1 is record V : Integer := Info.Zero_Const; W : Integer := Info.Zero_Func; end record; end A;

As expected, compilation fails because of the limited visibility — as Zero_Const and Zero_Func from the Info package are not visible in the private part of A. (Of course, if we revert to full visibility by simply removing the limited keyword from the example, the code compiles just fine.)

Visibility

Todo

Complete section!

Use type clause

Relevant topics

Todo

Complete section!