Version 1.1 of acs/ac-00049.txt
!standard 7.4(00) 02-09-30 AC95-00049/01
!class amendment 02-09-30
!status received no action 02-09-30
!subject Private envelope types
From: Alexander Kopilovitch
Sent: Monday, September 30, 2002 4:11 PM
Here is substantially expanded and reworked (comparing with the initial version
posted about a month ago) concept of private envelope type.
A private envelope type is a custom implementation of its core type.
Conversely, a type is the canonical abstraction for each of its private envelope
Declaration of a private envelope type resembles declaration of a conventional
private type, with the addition of the new keyword "envelope":
type Envelope_Type is private envelope of Core_Type;
type Tagged_Envelope_Type is tagged private envelope of Core_Type;
By defining a type as a private envelope type, the package separates the
"abstract state" (which may represent ontological features) for an object of
the type from its custom features.
That separation can be maintained, largely transparently, throughout a client
code. The transparency is achieved by means of canonical implicit conversions
and a special semantical interpretation for pseudo-components of PET objects.
In presence of implicit conversions, some measures against ambiguity are
necessary; therefore polymorphism over PETs is restricted: a PET's primitive
operation cannot overload primitive operation of its core type. Instead,
a PET may define "embedded" primitive operations with derived names.
Private envelope types may constitute a hierarchy: the core of an "outer" PET
may be, in turn, an "inner" PET. But that is permitted only if the package that
defines "outer" PET is a child of the package that defines "inner" PET.
2. Canonical implicit conversions.
Every private envelope type is equipped with two canonical conversion functions:
from the PET to its core type and vice versa. The name for each of these
functions is a concatenation of the string "To_" and the name of the return type
of the function.
Those functions should be used for implicit conversions when appropriate
conditions are met.
The following general conditions are necessary for applying the implicit
a) the PET's core type must be untagged (more on that in the section
"Relations between private envelope types and tagged types" below)
b) the place must be outside the package, which defines the PET, and its
c) neither record component selector nor indexing or attribute defined for
the current type immediately follow the source object
d) the source object is neither an operand nor the result of an embedded
e) the source object is not a subprogram argument that should be passed by
Provided that all above conditions are met, a compiler should:
a) always apply canonical conversion for a PET expression (thus landing
the latter to the core type) unless the PET is explicitly expected in the
context (e.g. actual argument for the PET formal parameter or the source
of an assignment to the PET variable)
b) apply canonical conversion to the PET from its core type if the PET is
explicitly expected in the context
(that is like there is a force directed from a PET to its core, and another,
stronger force directed to the type explicitly expected in the context).
3. Embedded primitive operations.
A PET's embedded primitive operation is distinguished by its name, in which
the leading character is a circumflex ('^'), and the rest is identical to the
name of some primitive operation of the core type.
PETs cannot be intermixed with their core types inside a specification of an
embedded primitive operation. That is, any PET cannot participate simultaneously
with its core in formal parameters and the result of an embedded primitive
Within an expression, an embedded primitive operation always must be
In all other respects, an embedded primitive operation is like a normal
4. Pseudo-components and insider subroutines.
A PET, whose core is an aggregate type, simulates the components of the latter.
For this purpose, the PET provides either a direct representative (which is the
matching record field within the PET's full definition) or a pair of insiders
(a private function, which generates simulated value from a PET object, and
a private procedure, which affects a PET object according to its argument) for
each core's component. In a client code those simulation facilities are
represented by the PET's pseudo-components.
Note, that PET simulates the aggregate structure explicitly presented in the
core type declaration, and does not go deep into the possible aggregate
structure of the core component types.
A pseudo-component of a PET object syntactically resembles a proper component
of a core's object. In other words, a substitution of the PET expression
(which is the left side of a pseudo-component) by identifier of PET's core
object produces a conventional reference to a component of the latter.
A pseudo-component cannot be passed by reference, and the pointer attributes
(Access, Address, etc.) cannot be applied to it.
A direct representative is possible for the PET's core type record fields.
For that, the PET's full definition must be a record, and that record must
contain a field that matches the simulated field both in the name and the type.
Conversely, if PET's full definition is a record, and a field of that record
matches the PET's core type record field then the former field is regarded as
direct representative for the latter.
For each compoment of the PET's core that has no direct representative within
the PET full definition, there must be a corresponding pair of insiders. Note,
that if a record field is an array then two pairs of insiders are needed: one
for the whole field, and another for the array's element (the latter is always
applied when indexing is supplied for a pseudo-component, therefore no ambiguity
Generally, the insiders must be named identically to the component they
simulate. The single exception is the case when the PET's whole core is an
array: in this case the insider's name must be a concatenation of the string
"Element_Of_" and the PET's name.
The first parameter for all insiders is the PET object to deal with; it is IN
parameter for an insider function, and IN OUT parameter for an insider procedure
(the other parameters of an insider, if any, are all IN parameters). If the
relevant core component is an array element then the appropriate number of
parameters for the indices (of an appropriate type each) follow. An insider
function returns an object of relevant core component type, while an insider
procedure has an additional (the last) parameter of that type.
The compiler substitutes pseudo-components by their direct representatives or
by calls of appropriate insiders. Dealing with a direct representative, the
compiler simply uses it literally, "tunneling under" the private part barrier;
but the employment of insiders requires some actual transformations.
First of all, in a call of an insider subroutine, the PET expression (the left
side of the pseudo-component involved) always becomes the first argument for the
insider, and the following indices (if any) become the next arguments. For a call
of an insider function that's all, but an insider procedure needs more.
An insider procedure is needed in two cases: for a pseudo-component that is
the target of an assignment, and for pseudo-component that is an actual argument
for an OUT (or IN OUT) parameter in a procedure call.
The value to be received by the pseudo-component becomes the last argument
of the insider procedure. So, the whole assignment to a pseudo-component should
be transformed into an insider procedure call, in which the left side of the
assignment determines the insider's name and supplies all arguments except the
last one, while the right side supplies the last argument.
For a pseudo-component that is an argument for an OUT parameter, the compiler
should create a temporary variable, use it instead of the pseudo-component in
the original procedure call, then insert after the latter a separate assignment
of the emerged value to the pseudo-component, and then process that assignmemnt
as it was said above. For the case of an IN OUT parameter, the second assignment,
which initializes the temporary variable with the pseudo-component's value
should be inserted before the original call.
5. Relations between private envelope types and tagged types.
A private envelope type and its core type may be either tagged or untagged
independently of each other. That is, there may be a tagged private envelope
type with an untagged core type, and vice versa.
The rules for PET do not depend on whether it is tagged or untagged.
But it is not true for its core type: if the latter is tagged, then the implicit
conversions (to and from its PETs) are never applied.
This restriction is needed for suppressing a major source of ambiguity:
obviously, if both PET and its core type are extensions of some tagged types
then there will be multiple paths for implicit conversions. Since an
opportunity to extend a type is much more important for a PET than for its core,
the restriction pertains to the latter.
6. About examples.
I imagine 3 kinds of examples, which illustrate an appearance of PETs
in different circumstances:
a) circumventing language restrictions
b) dealing with different representations, one of which may be regarded as
primary, and others as secondary
c) providing ontological abstractions for custom components, that operate
inside complicated enviroments.
The latter seems to be a natural Ada approach for what is called "properties"
6.1. Let's consider a well-known case of Unbounded_Strings. We may write:
package Popular_Strings is
type Easy_String is private envelope of String;
To_Easy_String(Source : String)
To_String(Source : Easy_String)
"^&" function(S1, S2 : Easy_String)
type Easy_String is Ada.Strings.Unbounded.Unbounded_String;
function Element_Of_Easy_String(Source : Easy_String;
Index : Integer)
procedure Element_Of_Easy_String(Target : in out Easy_String;
Index : Integer;
Source : Character);
Then, client code might look like:
A : Easy_String = "aaa";
B : Easy_String = "bbb";
U : Easy_String;
S : String := A & B; -- <==> S : String := To_String(A) & To_String(B);
U := A & B; -- <==> U := To_Easy_String(To_String(A) & To_String(B));
S : String := (A ^& B); -- <==> S : String := To_String(A ^& B);
U := (A ^& B); -- no implicit conversion is needed here
and the following is illegal:
S : String := A ^& B; -- illegal, parenthesis are missing
6.2. Let's consider 3-dimensional vectors in Cartesian, spherical and
package Space_Vectors is
type Sample_Real digits 10;
type Cartesian_Vector is record of
X : Sample_Real;
Y : Sample_Real;
Z : Sample_Real;
type Spherical_Vector is private envelope of Cartesian_Vector;
function To_Cartesian_Vector(Source : Spherical_Vector)
function To_Spherical_Vector(Source : Cartesian_Vector)
function "^="(V1, V2 : Spherical_Vector)
function "^+"(V1, V2 : Spherical_Vector)
function "^-"(V1, V2 : Spherical_Vector)
function "^*"(C : Sample_Real; V : Spherical_Vector)
type Cylindrical_Vector is private envelope of Cartesian_Vector;
function To_Cartesian_Vector(Source : Cylindrical_Vector)
function To_Cylindrical_Vector(Source : Cartesian_Vector)
function "^="(V1, V2 : Cylindrical_Vector)
function "^+"(V1, V2 : Cylindrical_Vector)
function "^-"(V1, V2 : Cylindrical_Vector)
function "^*"(C : Sample_Real; V : Cylindrical_Vector)
type Spherical_Vector is record of
Rho : Sample_Real;
Phi : Sample_Real;
Theta : Sample_Real;
function X(Source : Spherical_Vector)
procedure X(Target : in out Spherical_Vector; Source : Sample_Real);
function Y(Source : Spherical_Vector)
procedure Y(Target : in out Spherical_Vector; Source : Sample_Real);
function Z(Source : Spherical_Vector)
procedure Z(Target : in out Spherical_Vector; Source : Sample_Real);
type Cylindrical_Vector is record of
Rho : Sample_Real;
Phi : Sample_Real;
Z : Sample_Real;
function X(Source : Cylindrical_Vector)
procedure X(Target : in out Cylindrical_Vector; Source : Sample_Real);
function Y(Source : Cylindrical_Vector)
procedure Y(Target : in out Cylindrical_Vector; Source : Sample_Real);
-- Z component has direct representative in the Cylindrical_Vector record
6.3. For a GUI button control we may write something like the following:
package Elementary_Windows_Controls is
type Button_Click is access procedure(Source : GUI_Button);
type Button is record
Enabled : Boolean;
Caption : Unbounded_String;
Action : Button_Click;
type GUI_Button is private envelope of Button;
To_Button(Source : GUI_Button)
To_GUI_Button(Source : Button)
type GUI_Button is new Comprehensive_Windows_Bindings.Windows_Button;
function Enabled(Source : GUI_Button)
procedure Enabled(Target : in out GUI_Button; Source : Boolean);
function Caption(Source : GUI_Button)
procedure Caption(Target : in out GUI_Button; Source : Unbounded_String);
function Action(Source : GUI_Button)
procedure Action(Target : in out GUI_Button; Source : Button_Click);
A client code for the above package might be like this:
package body Test_Client is
Test_Button : GUI_Button;
procedure Note_Click(Source : GUI_Button) is
Put_Line("Button clicked: " & To_String(Source.Caption));
procedure Test_Init is
Test_Button.Action := Note_Click'Access;
--------------------------------- THE END -------------------------------------
Questions? Ask the ACAA Technical Agent