Version 1.3 of ais/ai-00252.txt

Unformatted version of ais/ai-00252.txt version 1.3
Other versions for file ais/ai-00252.txt

!standard 04.01.03 (05)          01-10-05 AI95-00252/02
!class amendment 00-12-04
!status work item 00-12-04
!status received 00-12-04
!priority High
!difficulty Hard
!subject Object.Operation Notation
!summary
An equivalence is proposed between Object.Operation(...) and Package.Operation(Object,...) to allow an object-oriented programming model that is based on applying operations to objects, rather than selecting operations from a package and then applying them to parameters.
!problem
In Ada 95, one must identify the particular package in which an operation is defined, in addition to the primary "controlling" object to which the operation is to be applied. Identifying both the package and the object is to some extent redundant, and makes object-oriented programming in Ada 95 wordier than necessary, and/or encourages heavy use of potentially confusing "use" clauses. Eliminating this redundancy would make object-oriented programming less-error prone, while also clarifying what object is the controlling object.
!proposal
We propose an equivalence between Object.Operation(...) and Package.Operation(Object, ...) where "Package" is the package in which Object's type (or covering thereof) is declared. The equivalence would be based on a "use"-like visibility model, where after the ".", operations from the package in which an object's type is declared, as well as packages in which types that "cover" the object's type are declared, would be effectively "use" visible. Further, the first parameter to the operations made "use" visible would (implicitly) be the object preceding the ".".
The "use"-like visibility model ensures that the operations made visible would be hidden by any components or protected operations with the same identifier as the operation (since they are "directly" visible after the ".", rather than "use" visible). In addition, no "beaujolais" effects would be produced, because the operations made visible would all overload one another, rather than having operations from one package hide another.
To allow operations that are declared in the current package body to be visible, the directly visible declarations are considered prior to the (implicitly) use-visible declarations. As usual, the directly visible operations can hide the use-visible declarations, but the use-visible declarations cannot hide each other (they only overload each other).
Because of access parameters, if the type of the prefix is an access type, the package(s) in which the designated type and types that cover the designated type would be "use"d, in addition to the package in which the access type itself is declared. Furthermore, to preserve the model that "." can result in an implicit dereference, an interpretation of the prefix as an implicit dereference is coupled with a "use" of the packages in which the designated type and its coverings are declared. In this implicit dereference case, the package where the access type itself is declared is irrelevant.
As a final potentially useful addition, if the object is aliased, interpretations where 'Access would be needed after the object name are considered. That is, even if the prefix is not of an access type, operations with the first parameter being an access parameter designating the type of the prefix are considered. This would mean that access parameters can be used in general in primitives, without penalizing the person who actually declares an (aliased) object on the stack rather than using the heap.
One way of describing what is happening is that operations declared in the package immediately enclosing a type declaration that have the type (or "access" type or covering-type'Class) as their first parameter, are implicitly declared as components of a type, but with the first parameter removed. These implicitly declared components are then inherited when the type is extended. Use-visibility rules are used for these implicitly-declared components to avoid "beaujolais" effects.
Note that if the prefix type is not tagged and not access, this only makes available primitive operations in the package where the type is declared. If the type is tagged, it makes the classwide operations from the packages where "coverings" are declared, plus the primitives from the package where the type itself is declared. If the type is an access type, it makes primitives available from the package where the access type is declared, plus operations that would have been made available for the designated type.
!discussion
This AI grew out of an issue identified by Erhard Ploedereder and his graduate students, where it made OOP awkward to always identify (or "use") the specific package in which a dispatching operation was declared, particularly when the operation was inherited, and hence only implicitly declared in that package. Furthermore, the rules for calling classwide operations and primitive operations were significantly different, where the classwide operation was not inherited, and hence "remains" in the original package where it was declared, whereas the primitive operations were inherited, and hence got carried along into the package where the type was declared. The tendency was to "use" every package that might conceivably have an operation of interest, which can significantly add to the confusion.
We considered an Object'Operation(...) syntax, but that was felt to introduce possible conflicts with implementation-dependent attributes. Also, the "." notation had the additional nice feature that a primitive function could be used to effectively provide a "read only" component, with the familiar "." syntax. Also, using the "." notation allows primitives defined outside a protected or a task type to be called in the same "obj.operation" notation used for entries and protected subprograms. This unifies these two kinds of operations, which from a user perspective are both "fundamental" operations of the synchronizing types.
We considered only making primitive operations visible, but there are situations where an abstraction uses a classwide operation very much like a primitive operation. For example in Claw, whether a given operation is classwide or primitive is not particularly relevant to how it is used (though of course it is relevant to how it is handled in type extensions). The "package"-oriented approach, as opposed to the "primitive"-oriented approach, may also fit more cleanly into existing overload resolution algorithms, since it has more in common with the way "use" visibility works currently. Of course, any guess of implementation burden is hard to make, since compilers have so many different strategies.
The rules for access types are a bit convoluted, but they seem necessary given the importance of access parameters. Adding the implicit "'Access" seems like a useful capability, without significantly complicating the proposal.
We talk about "covering" types rather than "ancestor" types for two reasons. One is that it is only operations on class-wide types that are being imported from packages other than the one in which the type itself is declared. Second, the notion of "covers" will presumably generalize better if we adopt the notion of abstract interfaces (see the multiple inheritance Amendment AI).
Note that operations on class-wide types can be used even if the package in which they are declared is not "with"ed. This is part of the whole point. A class-wide operation may be introduced anywhere in the type hierarchy, and it will be available via the object.operation(...) notation without having to identify the specific package. Since only packages where the "coverings" of the type are declared are considered, this "implicit with" is not actually creating any new semantic dependences. Essentially it is as though these class-wide operations are being inherited, but with no possibility of overriding. They are like "final" operations in Java.
!example
Here is an example of use of the "object.operation" syntax:
package P is type T is tagged ... -- implicit declaration of T'Class procedure Prim(X : in out T); procedure Classwide(X : in out T'Class; Y : Integer); end P;
with P; package P2 is type T2 is new P.T with ... -- implicit declaration of T2'Class -- implicit declaration of Prim(X : in out T2); procedure Prim2(X : in out T2; B : Boolean); function Prim3(X : T2) return Float; end P2;
with P2; procedure Main is Obj : P2.T2; CObj : P2.T2'Class := ... begin Obj.Prim; -- call on inherited primitive Obj.Prim2(True); -- call on primitive CObj.Prim; -- dispatching call Obj.Classwide(Y => 77); -- call on classwide op if CObj.Prim3 > 33.5 then -- dispatching call on primitive function ... end if; end Main;
Here is an example using a prefix that is of an access type.
with P2; package P3 is type T3 is new P2.T2 with ... procedure Prim4(A : access T3; C : Character); -- a primitive of T3 using an access param end P3;
with P3; package P4 is type AT3 is access all T3'Class; procedure APrim5(Q : AT3; R : Integer); -- a primitive of AT3 (not of T3) end P4;
with P4; procedure AccMain is Ptr : P4.AT4 := new ...; begin Ptr.Prim; -- Implicit dereference, equivalent to Ptr.all.Prim Ptr.Prim4(C => 'x'); -- No implicit dereference; "P3" package "use"ed -- because desig type declared there Ptr.Aprim5(R => 13); -- No implicit dereference; "P4" package "use"ed -- because AT4 declared there if Ptr."="(null) then -- "=" declared in P4 so can be called this way also ... end if; end AccMain;
!ACATS test
!appendix

From: Tucker Taft
Sent: Sunday, November 26, 2000 10:49 AM
Subject: Object.Operation amendment AI

Here is an amendment AI that might be considered part of
the series of AIs designed to "round out" the OOP features.
This one was prompted in part by the reactions of Erhard's
grad students to the difficulty of having to both identify
the package containing an operation and the object on which
the operation is to be performed.  With both classwide
and primitive operations being relevant, and these operations
having essentially opposite rules about which package the
operation resides in (the ultimate ancestor for classwide,
and the ultimate descendant for primitive), some way to
eliminate the package from the syntax seemed useful.

The other prompting factor is the continual whining that Ada 95
is out of the mainstream of OOP languages because it lacks the
object.operation syntax.  This proposal defines the "object.op"
syntax as essentially a syntactic sugar on the pkg.op(object,...)
syntax.  This approach is pretty much what Modula-3 did.  It
provides for a "symmetric" notation when dealing with binary
operators, while also providing an "asymmetric" (object-oriented)
syntax when using operations that have a single controlling
operand.

As usual, any and all comments highly encouraged.
-Tuck
-------------
!standard 04.01.03 (05)                               00-11-25  AI95-xxx/01
!class amendment 00-11-25
!priority High
!difficulty Hard
!subject Tagged Types, Object.Operation Notation, Object-Oriented Programming

!summary

An equivalence is proposed between Object.Operation(...) and
Package.Operation(Object,...) to allow an object-oriented programming model
that is based on applying operations to objects, rather than selecting
operations from a package and then applying them to parameters.

!question

[Note: I am interpreting this "question" section as a statement of the problem
that might deserve an amendment.]

In Ada 95, one must identify the particular package in which an
operation is
defined, in addition to the primary "controlling" object to which the operation
is to be applied. Identifying both the package and the object is to some extent
redundant, and makes object-oriented programming in Ada 95 wordier than
necessary, and/or encourages heavy use of potentially confusing "use" clauses.
Would it be possible to eliminate this redundancy, while also clarifying what
object is the controlling object.

!recommendation

We propose an equivalence between Object.Operation(...) and
Package.Operation(Object, ...) where "Package" is the package in which Object's
type (or covering thereof) is declared. The equivalence would be based on a
"use"-like visibility model, where after the ".", operations from the package in
which an object's type is declared, as well as packages in which types that
"cover" the object's type are declared, would be effectively "use" visible.
Further, the first parameter to the operations made "use" visible would
(implicitly) be the object preceding the ".".

The "use"-like visibility model ensures that the operations made visible would be
hidden by any components or protected operations with the same identifier as the
operation (since they are "directly" visible after the ".", rather than "use"
visible). In addition, no "beaujolais" effects would be produced, because the
operations made visible would all overload one another, rather than having
operations from one package hide another.

Because of access parameters, if the type of the prefix is an access type, the
package(s) in which the designated type and types that cover the designated type
would be "use"d, in addition to the package in which the access type itself is
declared. Furthermore, to preserve the model that "." can result in an implicit
dereference, an interpretation of the prefix as an implicit dereference is
coupled with a "use" of the packages in which the designated type and its
coverings are declared. In this implicit dereference case, the package where the
access type itself is declared is irrelevant.

!example

Here is an example of use of the "object.operation" syntax:

package P is
    type T is tagged ...
    -- implicit declaration of T'Class
    procedure Prim(X : in out T);
    procedure Classwide(X : in out T'Class; Y : Integer);
end P;

with P;
package P2 is
    type T2 is new P.T with ...
    -- implicit declaration of T2'Class
    -- implicit declaration of Prim(X : in out T2);
    procedure Prim2(X : in out T2; B : Boolean);
    function Prim3(X : T2) return Float;
end P2;

with P2;
procedure Main is
    Obj : P2.T2;
    CObj : P2.T2'Class := ...
begin
    Obj.Prim;         -- call on inherited primitive
    Obj.Prim2(True);  -- call on primitive
    CObj.Prim;        -- dispatching call
    Obj.Classwide(Y => 77);  -- call on classwide op
    if CObj.Prim3 > 33.5 then  -- dispatching call on primitive function
        ...
    end if;
end Main;

Here is an example using a prefix that is of an access type.

with P2;
package P3 is
    type T3 is new P2.T2 with ...
    procedure Prim4(A : access T3; C : Character);
      -- a primitive of T3 using an access param
end P3;

with P3;
package P4 is
    type AT3 is access all T3'Class;
    procedure APrim5(Q : AT3; R : Integer);
      -- a primitive of AT3 (not of T3)
end P4;

with P4;
procedure AccMain is
    Ptr : P4.AT4 := new ...;
begin
    Ptr.Prim;             -- Implicit dereference, equivalent to Ptr.all.Prim
    Ptr.Prim4(C => 'x');  -- No implicit dereference; "P3" package
"use"ed
                          --  because desig type declared there
    Ptr.Aprim5(R => 13);  -- No implicit dereference; "P4" package
"use"ed
                          --  because AT4 declared there
    if Ptr."="(null) then -- "=" declared in P4 so can be called this
way also
        ...
    end if;
end AccMain;


!discussion

This AI grew out of an issue identified by Erhard Ploedereder and his graduate
students, where it made OOP awkward to always identify (or "use") the specific
package in which a dispatching operation was declared, particularly when the
operation was inherited, and hence only implicitly declared in that package.
Furthermore, the rules for calling classwide operations and primitive operations
were significantly different, where the classwide operation was not inherited,
and hence "remains" in the original package where it was declared, whereas
the primitive operations were inherited, and hence got carried along into
the package where the type was declared.  The tendency was to "use" every
package that might conceivably have an operation of interest, which can
significantly add to the confusion.

We considered an Object'Operation(...) syntax, but that was felt to introduce
possible conflicts with implementation-dependent attributes.  Also, the "."
notation had the additional nice feature that a primitive function could be used
to  effectively provide a "read only" component, with the familiar "." syntax.
Also, using the "." notation allows primitives defined outside a protected or a
task type to be called in the same "obj.operation" notation used for entries and
protected subprograms.  This unifies these two kinds of operations, which from a
user perspective are both "fundamental" operations of the synchronizing types.

We considered only making primitive operations visible, but there are situations
where an abstraction uses a classwide operation very much like a primitive
operation. For example in Claw, whether a given operation is classwide or
primitive is not particularly relevant to how it is used (though of course it is
relevant to how it is handled in type extensions).  The "package"-oriented
approach, as opposed to the "primitive"-oriented approach, may also fit more
cleanly into existing overload resolution algorithms, since it has more in common
with the way "use" visibility works currently.  Of course, any guess of
implementation burden is hard to make, since compilers have so many different
strategies.

The rules for access types are a bit convoluted, but they seem necessary given
the importance of access parameters.

We talk about "covering" types rather than "ancestor" types for two reasons.
One is that it is only operations on class-wide types that are being imported from
packages other than the one in which the type itself is declared.  Second,
the notion of "covers" will presumably generalize better if we adopt the notion
of abstract interfaces (see the multiple inheritance Amendment AI).

****************************************************************

Questions? Ask the ACAA Technical Agent