RR's Ramblings

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.

Copyright © 2023 RR Software, Inc.
Use of this site constitutes your acceptance of these terms of use.