Completing private types with access types considered harmful
September 25, 2025
by Randy Brukardt
This topic doesn't have much to do with Janus/Ada specifically; it has
come up in a recent ARG discussion. I needed a place to write down my thoughts
that isn't associated with an AI.
We've been discussing the importance of supporting capabilities on private
types completed by a full type that cannot support that capability (AI22-0141-1).
This is in particular interesting for access types. Tucker Taft says that 25%
of the private types in the major projects he works on (Codepeer, Parasail, and
the GNAT runtime). That's an amazing number for a mechanism that doesn't work
very well.
Let's look at why this mechanism doesn't work well. First, we need to understand
that all private types are not considered equal. Any private type creates an
Abstract Data Type (ADT), as it hides details not relevant to the client
(the abstract part) and it surely is a data type. But there can be multiple
ADTs in a package, but only one can be the Primary Abstract Data Type (PADT).
A PADT is the data type for which a package exists. For instance, in Ada.Containers.Vectors,
type Vector is the PADT (the type for which the package was defined), and type
Cursor is an ADT but not a PADT. (A package might contain some private types,
but not contain any PADT. That's not an interesting case for this discussion.)
PADTs should be designed to have the most utility for their clients and the
least constraints on the clients as possible. This leads to an important principle
for memory management: The client should be able to choose how a PADT is managed
in their code. The PADT should work just as well in a container, declared in a
declare block or subprogram, declared as a component of another type (often another
PADT), allocated from an ownership-managed access type (hopefully,
Ada will get those soon; don't me started as to why Ada 2022 didn't have them),
or allocated for a traditional access type from some storage pool.
This principle leads to a number of corollaries:
- There should not be a clean-up routine that has to be called before an object
of a PADT ceases to exist (goes out of scope, is deallocated, etc.).
This is necessary because of the difficulty of knowing when a particular object
ceases to exist. For instance, for a stand-alone object, the scope can be closed
by exception propagation. It can be very difficult to ensure a closing routine
is called on every possible exceptional path as well as the primary path. (Ask
me for the war story about the searching on Ada-Auth.org if you need help
believing this.) Similarly, exactly when elements of a container cease to exist
is not defined, only a bound.
A controlled type can manage cleanup, or the PADT can be designed so no cleanup
is necessary. But note that a bare access type as the full definition of a PADT
can do neither.
- Any items that are needed internally by the PADT should be managed internally.
That means that they should be created and destroyed by the PADT, and not require
the involvement of the client in any way.
Just as we do not want to impose a memory management scheme on the client, nor
do we want to impose one on the PADT. It also should be able to use a container,
ownership access type, or legacy access type to manage any extra storage that it
needs.
A bare access type needs to use some sort of allocator to manage the designated
object. In the current absence of ownership access types, that leaves one with
no way to recover the memory; there is a guaranteed storage leak.
Note that a record type containing no access types necessarily meets all
of these requirements and corollaries trivially. From that I conclude that most
PADTs should avoid any use of access types anywhere. The client is the place to
manage the use of PADTs. If something has to be allocated dynamically, one
should try to use a container to hold the something when possible, as otherwise
controlled types are required to ensure a proper cleanup.
Other ADTs have less rigid requirements. One common use of a non-primary ADT
is as an abstract reference. However, completing such a reference with a bare
access type is not helpful: it makes it more complex to dereference the abstract
reference while adding nothing over directly using the access type. I believe
the use for abstract references (like the type Cursor in the containers) is to add
additional capabilities beyond that of a raw pointer (such as dangling reference
detection/elimination, persistence management, or reference counting); there's
no point in just hiding an access value since it has no interesting capabilities
of its own (and the use of bare access values should be clearly noted in the
specification of a PADT, not hidden behind some other ADT).
Why then, does Tucker report so many ADTs completed with bare access types?
One possible reason is that the ADTs are old, originally designed in Ada 83.
Back then, there was no way to automatically clean up types, so some manual
method was needed. This is likely to leak memory and other resources, but
depending upon the usage, it may not matter sufficiently to manage properly.
For example, Tucker mentioned that File_Type is often implemented as a bare
access type. That works because Ada 83 compilers had to invent a mechanism to
close any open file. Memory for the file objects might leak, and an external
file might be locked until the program exits, but these are mostly minor
annoyances and it may simply not be worth the effort to fix them (with all of
the other things that could be worked on). For instance, for Janus/Ada, File_Type
should really be a controlled type that closes the associated file, but given
the lack of complaints about the current mechanism, time is better spent elsewhere.
Another possibility is that all of these ADTs are designed to only work in
very limited environments. For instance, an ADT that only works with a particular
storage pool (perhaps one with subpools). These are ADTs in name only; the private
type is not serving any real purpose other than to make the author feel better
about their design. The few capabilities hidden (mostly dereferencing) have
to be provided in another way, which just makes them more expensive to use.
Needless to say, the existence of such things should not influence language
design.
A recent development in Ada 2022 to extend prefixed views to all types other
than access types will make it even less desirable to complete private types
with an access type, as the private view will allow prefixed views, while the
full (access) view will not. This is an unfortunate situation needed to keep
compatibility, but it will make such type annoying to use in the package body.
For instance:
package Foo is>
type T is private;
Glob : constant T;
function Cap (A : T) return Natural;
procedure Create (A : in out T; Capacity : Natural := Glob.Cap);
procedure Munge (A : T)
with Pre => A.Cap > 20;
private
type Desig is ...
type T is access Desig;
...
end Foo;
Here we have a couple of instances of using prefix notation in the
specification. But now see what happens in the body:
package body Foo is>
...
procedure Create (A : in out T; Capacity : Natural := Glob.Cap) is
-- Glob.Cap is illegal, and just changing it here causes conformance to fail.
...
procedure Munge (A : T) is
pragma Assert (A.Cap > 20); -- A.Cap is illegal here.
...
end Foo;
This is going to be annoying to those who write most of their calls in
prefix notation, and especially annoying as it will work in the specification
and in clients. Not the largest deal, but certainly another reason to avoid
using access type directly to complete a private type.
Thus I conclude that almost all private types should be completed by some
sort of record, and almost everything else will be completed by a protected
type (which really should be a form of record, but that's a topic for another
day). The issues of memory management (not to mention the likelihood of
capabilities needed outside of a single access value) make that a near
requirement.
If you have any comments on this piece, please send them to me at
randy@rrsoftware.com; if there are
enough interesting comments I'll create a companion piece to hold them.
|