Version 1.1 of acs/ac-00049.txt

Unformatted version of acs/ac-00049.txt version 1.1
Other versions for file 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.

1. General.

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
     child packages

  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
     primitive operation

  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
primitive operation.

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.

4.1. Syntax.

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
emerges here).

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.

4.2. Semantics.

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"
in Delphi/VB/C#.

6.1. Let's consider a well-known case of Unbounded_Strings. We may write:

  with Ada.Strings.Unbounded;

  package Popular_Strings is

    type Easy_String is private envelope of String;

    To_Easy_String(Source : String)
      return Easy_String;
    To_String(Source : Easy_String)
      return String;

    "^&" function(S1, S2 : Easy_String)
      return Easy_String;


    type Easy_String is Ada.Strings.Unbounded.Unbounded_String;

    function Element_Of_Easy_String(Source : Easy_String;
                                    Index : Integer)
      return Character;
    procedure Element_Of_Easy_String(Target : in out Easy_String;
                                     Index : Integer;
                                     Source : Character);

  end Popular_Strings;

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
cylindrical coordinates:

  package Space_Vectors is

    type Sample_Real digits 10;

    type Cartesian_Vector is record of
      X : Sample_Real;
      Y : Sample_Real;
      Z : Sample_Real;
      end record;

    type Spherical_Vector is private envelope of Cartesian_Vector;

    function To_Cartesian_Vector(Source : Spherical_Vector)
      return Cartesian_Vector;

    function To_Spherical_Vector(Source : Cartesian_Vector)
      return Spherical_Vector;

    function "^="(V1, V2 : Spherical_Vector)
      return Boolean;
    function "^+"(V1, V2 : Spherical_Vector)
      return Spherical_Vector;
    function "^-"(V1, V2 : Spherical_Vector)
      return Spherical_Vector;
    function "^*"(C : Sample_Real; V : Spherical_Vector)
      return Spherical_Vector;

    type Cylindrical_Vector is private envelope of Cartesian_Vector;

    function To_Cartesian_Vector(Source : Cylindrical_Vector)
      return Cartesian_Vector;

    function To_Cylindrical_Vector(Source : Cartesian_Vector)
      return Cylindrical_Vector;

    function "^="(V1, V2 : Cylindrical_Vector)
      return Boolean;
    function "^+"(V1, V2 : Cylindrical_Vector)
      return Cylindrical_Vector;
    function "^-"(V1, V2 : Cylindrical_Vector)
      return Cylindrical_Vector;
    function "^*"(C : Sample_Real; V : Cylindrical_Vector)
      return Cylindrical_Vector;


    type Spherical_Vector is record of
      Rho   : Sample_Real;
      Phi   : Sample_Real;
      Theta : Sample_Real;
      end record;

    function X(Source : Spherical_Vector)
      return Sample_Real;
    procedure X(Target : in out Spherical_Vector; Source : Sample_Real);

    function Y(Source : Spherical_Vector)
      return Sample_Real;
    procedure Y(Target : in out Spherical_Vector; Source : Sample_Real);

    function Z(Source : Spherical_Vector)
      return Sample_Real;
    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;
      end record;

    function X(Source : Cylindrical_Vector)
      return Sample_Real;
    procedure X(Target : in out Cylindrical_Vector; Source : Sample_Real);

    function Y(Source : Cylindrical_Vector)
      return Sample_Real;
    procedure Y(Target : in out Cylindrical_Vector; Source : Sample_Real);

    -- Z component has direct representative in the Cylindrical_Vector record

  end Space_Vectors;

6.3. For a GUI button control we may write something like the following:

  with Comprehensive_Windows_Bindings;

  package Elementary_Windows_Controls is

  type GUI_Button;

  type Button_Click is access procedure(Source : GUI_Button);

  type Button is record
    Enabled : Boolean;
    Caption : Unbounded_String;
    Action  : Button_Click;
  end record;

  type GUI_Button is private envelope of Button;

  To_Button(Source : GUI_Button)
    return Button;
  To_GUI_Button(Source : Button)
    return GUI_Button;


  type GUI_Button is new Comprehensive_Windows_Bindings.Windows_Button;

  function Enabled(Source : GUI_Button)
    return Boolean;
  procedure Enabled(Target : in out GUI_Button; Source : Boolean);

  function Caption(Source : GUI_Button)
    return Unbounded_String;
  procedure Caption(Target : in out GUI_Button; Source : Unbounded_String);

  function Action(Source : GUI_Button)
    return Button_Click;
  procedure Action(Target : in out GUI_Button; Source : Button_Click);

  end Elementary_Windows_Controls;

A client code for the above package might be like this:

  with Elementary_Windows_Controls;
  use Elementary_Windows_Controls;

  package body Test_Client is

  Test_Button : GUI_Button;

  procedure Note_Click(Source : GUI_Button) is
    Put_Line("Button clicked: " & To_String(Source.Caption));
  end Note_Click;

  procedure Test_Init is
    Test_Button.Action := Note_Click'Access;
  end Test_Init;


  end Test_Cient;

--------------------------------- THE END -------------------------------------

Questions? Ask the ACAA Technical Agent