Version 1.5 of ai12s/ai12-0223-1.txt

Unformatted version of ai12s/ai12-0223-1.txt version 1.5
Other versions for file ai12s/ai12-0223-1.txt

!standard 3.4(2/2)          17-04-14 AI12-0223-1/00
!class Amendment 17-04-14
!status Hold by Letter Ballot (9-1-1) - 18-05-07
!status work item 17-04-14
!status received 17-01-11
!priority Low
!difficulty Hard
!subject The co-derivation problem
!summary
** TBD.
!problem
The containers are tagged types, with the idea that they would support extension if needed. But it turns out that one can't really extend them sensibly. Consider what happens if you try.
The obvious way to create an extension of a container would be something like:
package P is package My_Map_Pkg is new Ada.Containers.Ordered_Maps (Positive, My_Rec);
type Map_Rec is new My_Map_Pkg.Map with private;
type Map_Rec_Cursor is new My_Map_Pkg.Cursor;
-- New and overridden operations here.
private ... end P;
The problem is that the inherited operations are NOT the ones we want. For the derivation making type Map_Rec, we have inherited primitive operations on Map_Rec and on My_Map_Pkg.Cursor (that is, the new map and the old cursor). For the derivation making type Map_Rec_Cursor, we have inherited primitive Operations on My_Map_Pkg.Vector and on Map_Rec_Cursor (that is, the old map and the new cursor). That is certainly not what we want, for either type.
The only workaround would be to declare all of the operations we want and then manually call the correct original operations, very tedious and error-prone. (Plus we still have all of those junk operations declared and visible, meaning lots of potential errors in use could go undetected.)
This problem comes up anytime you have an ADT and an associated handle, with operations on both. For tagged types, one can mitigate the problem if the "handle" is a raw access type, by using an access-to-classwide type for the handle. But that eliminates any compile-time checking, moving everything to runtime checks (with the usual resulting problems - insufficient testing could leave a time-bomb in the code).
Ada is all about compile-time checking, so there should be a way to do this that preserves it.
!proposal
The following does not work in all cases, but it is presented as a starting point for a solution:
When one has two types that are tied together, it doesn't make sense to derive them separately. Therefore, we provide co-derivation, where multiple types are derived at once.
The syntax would be:
type defining_identifier {, defining_identifier} is new
derived_type_definition {, derived_type_definition};
[Note: "and" probably would have been better syntax, but since interfaces use that, we're stuck with ','.]
For a co-derivation, the primitives inherited are those that are primitive on either type, and that both types appearing in the profile are replaced as described in 3.4. The bodies of the new routines are the original routines called with parameters of both types converted as needed.
For the example given in the problem, that would give:
type Map_Rec, Map_Rec_Cursor is new
My_Map_Pkg.Map with private, My_Map_Pkg.Cursor;
This is known not to work in various cases.
The most important is that it doesn't make sense for dispatching. If one of the types is tagged (both cannot be tagged, as that would imply a type primitive for two tagged types, which is not allowed in Ada), then one would get inherited dispatching routines.
For instance, with the above declaration, we'd inherit delete, which is defined as:
procedure Delete (Container : in out Map; Position : in out Cursor);
which would be inherited as:
procedure Delete (Container : in out Map_Rec; Position : in out Map_Rec_Cursor);
However, a dispatching call on My_Map_Pkg.Map'Class would call:
procedure Delete (Container : in out Map_Rec; Position : in out My_Map_Pkg.Cursor);
which doesn't exist.
One could imagine limiting this feature to untagged types, but that doesn't seem to be very helpful. One could also imagine somehow banning dispatching from the root type to make this work, but that also seems to be a weird (and potentially incompatible in the case of existing code like the containers) restriction.
Steve Baird notes that there might be issues with generic formal derived types causing emergence of routines that aren't declared. Probably generic matching could fix that. He also noted similar issues with deriving from abstract root types with abstract routines.
!wording
** TBD.
!discussion
An industrial Ada user reported the following:
We have a "design pattern" here of handles being class-wide, when a specific type would be much better. Procedure X has an object of a specific type, it's upward converted to a class-wide type for passing as a parameter to procedure Y. Y downward converts it back to the specific type - indeed can only handle that specific type. A hole has been created where there is the potential to call Y with an actual of the wrong specific type, which would fail the Tag Check and crash with a Constraint Error.
This is an example of how existing OOP practice forces users away from static strong typing into dynamic type checks. Better would be a "post-OOP" practice that uses strong static typing everywhere. (All we have to do is figure it out. ;-)
!example
(See Problem and Proposal.)
!ASIS
The new capabilities will need ASIS support.
!ACATS test
ACATS B-Test and C-Tests will be needed to check that the new capabilities are supported.
!appendix

From: Randy Brukardt
Sent: Wednesday, January 11, 2017  6:48 PM

I mentioned this problem in an aside to Tucker, but it probably deserves a
first-class examination.

The containers are tagged types, with the idea that they would support
extension if needed. But it turns out that one can't really extend them
sensibly. Consider what happens if you try.

The obvious way to create an extension of a container would be something
like:

    package P is
       package My_Map_Pkg is new Ada.Containers.Ordered_Maps (Positive, My_Rec);

       type Map_Rec is new My_Map_Pkg.Map with private;

       type Map_Rec_Cursor is new My_Map_Pkg.Cursor;

       -- New and overridden operations here.

    private
       ...
    end P;

The problem is that the inherited operations are NOT the ones we want. For the
derivation making type Map_Rec, we have inherited primitive operations on
Map_Rec and on My_Map_Pkg.Cursor (that is, the new map and the old cursor).
For the derivation making type Map_Rec_Cursor, we have inherited primitive
Operations on My_Map_Pkg.Vector and on Map_Rec_Cursor (that is, the old map
and the new cursor). That is certainly not what we want, for either type.

The only workaround would be to declare all of the operations we want and then
manually call the correct original operations, very tedious and error-prone.
(Plus we still have all of those junk operations declared and visible, meaning
lots of potential errors in use could go undetected.)

[Aside: I had a version of this problem in early Claw versions, when a "type
Handle is new Dword;" caused all kinds of routines that were accidentally
primitive on Dword to get derived. Which I found out when trying to debug some
unrelated problem; looking at a compiler symboltable dump I realized that
there were many unintended routines in it. I ended up moving Dword into a
subpackage to prevent any uses of it from being primitive.]

This problem comes up anytime you have an ADT and an associated handle, with
operations on both. That seems pretty common to me, especially as the "handle"
might just be an explicit access type that points at the original type.

[Yes, this exhibit N in the long list of reasons that we should have had all
of the container operations having a container parameter. If that had been the
case, we'd just be losing a bit of type checking by using the first derivation
alone, annoying but not necessarily catastrophic. It seems too late to fix
that problem, though. Unless we want to add overloaded versions of Next and
First and the like (we already have Reference and the like, and for Vectors,
we have all of the index versions) -- but that's not a general solution, it
would only help the containers.]

---

I once tried to work out a proposal for "co-derivation", with the intent of
handling this problem. It has issues with dispatching, sadly. I'll present the
idea here in the hopes that someone can think of something better.

The basic idea is that the two types above have to be derived at the same
time. It doesn't make sense to derive them separately because they're joined
at the hip. So I proposed a co-derivation, that would look something like:

    type Map_Rec, Map_Rec_Cursor is new
       My_Map_Pkg.Map with private, My_Map_Pkg.Cursor;

[Note: "and" probably would have been better syntax, but since interfaces use
that, we're stuck with ','.]

The basic idea is that the primitives inherited are those that are primitive
on either type, and that both types appearing in the profile are replaced as
described in 3.4. The bodies of the new routines are the original routines
called with parameters of both types converted as needed.

This works fine for untagged types, as far as I can tell (but Steve may be
able to disprove that optimistic assumption :-).

However, for tagged types, it's unclear how to deal with dispatching (since
one of the types necessarily has to be untagged, and we don't have any
"co-dispatching" anyway). Arguably, you don't want dispatching in this case
(the handle would almost always be the wrong type), but that seems
unfortunate on basic principles. (And you do want tagged types, so that you
can use the containers and especially prefix notation.)

Anyway, an idea to consider. Something seems to be needed to handle this sort
of problem in the general case, because it is frequent and the existing
solutions are lengthy and error-prone.

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

From: Jean-Pierre Rosen
Sent: Tuesday, May 22, 2018  9:16 AM

Taking up this AI from where it was left...

The killing problem noted by Randy is (from the AI):
-------------------------
For the example given in the problem, that would give:

    type Map_Rec, Map_Rec_Cursor is new
       My_Map_Pkg.Map with private, My_Map_Pkg.Cursor;

This is known not to work in various cases.

The most important is that it doesn't make sense for dispatching. If one of
the types is tagged (both cannot be tagged, as that would imply a type 
primitive for two tagged types, which is not allowed in Ada), then one would
get inherited dispatching routines.

For instance, with the above declaration, we'd inherit delete, which is 
defined as:

   procedure Delete (Container : in out Map;
                     Position  : in out Cursor);

which would be inherited as:

   procedure Delete (Container : in out Map_Rec;
                     Position  : in out Map_Rec_Cursor);

However, a dispatching call on My_Map_Pkg.Map'Class would call:

   procedure Delete (Container : in out Map_Rec;
                     Position  : in out My_Map_Pkg.Cursor);

which doesn't exist.
-----------------------------------------------
Is it really a problem? Important points to keep in mind:
1) Coderivation does not imply multi-dispatch, since at most one type is 
tagged.
2) Derived non-tagged types are equivalent (assuming no change in 
representation), and calls to primitive operations are always statically 
bound.

So it seems harmles for the co-derived subprogram to use the same slot in 
the dispatch table as the original one, i.e. a dispatching call on some 
Container will call the Delete on Map_Rec_Cursor if the dispatching call
is on Map_Rec, and the Delete on Cursor if the dispatching call is on Map.

Where could such a dispatching call happen? Presumably within a procedure 
like:

Do_Something (Container : in out Map'Class;
              Position  : in out Cursor);

Now, the user has declared:
My_Cont : Map_Rec;
My_Curs : Map_Rec_Cursor;

He can call:
Do_Something (My_Cont, Cursor (My_Curs));

(Annoying conversion, see later)

Inside Do_Something, there is a call:
Delete (Container, Position);  -- Dispatching

In this case, the dispatching table will point to the co-derived subprogram 
=> it works as expected.

1) Extra point 1:
This assumes that there is no change of representation for the derived type - 
i.e. changes of representation would not be allowed for types that are part of
a coderivation. Or alternatively, co-derivation would not be allowed if a non 
tagged type has a change of representation. This seems a small price to pay 
for the feature.

2) Extra point 2:
The extra conversion in the call:
 Do_Something (My_Cont, Cursor (My_Curs)); is annoying. It could be avoided by 
allowing T'Class on non-tagged types ONLY IN FORMAL PARAMETERS  (trying to 
quickly jump into my asbestos suit), with the obvious meaning: compatible with
any type derived from it, perhaps also under the condition that there is no 
change of representation.

Thoughts?

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

From: Randy Brukardt
Sent: Thursday, June 7, 2018  7:31 PM

...
> Where could such a dispatching call happen? Presumably within a 
> procedure like:
> 
> Do_Something (Container : in out Map'Class;
>               Position  : in out Cursor);
> 
> Now, the user has declared:
> My_Cont : Map_Rec;
> My_Curs : Map_Rec_Cursor;
> 
> He can call:
> Do_Something (My_Cont, Cursor (My_Curs));

True, but he's totally lost static type checking. The routine can no longer 
know whether it is getting the right type.

Nothing prevents calling Do_Something with mixed types, such as:

My_Other_Cont : Map_Rec;
My_Other_Curs : Cursor;

Do_Something (My_Other_Cont, Cursor (My_Other_Curs));

...and now you'll dispatch to the primitive routine Delete with the wrong kind
of cursor.

That is, it will work in the "correct" case, but it also will allow many 
incorrect calls.

If you're going to lose most of the type checking, you might as well not 
bother with the co-derivation.

...
> 1) Extra point 1:
> This assumes that there is no change of representation for the derived 
> type - i.e. changes of representation would not be allowed for types 
> that are part of a coderivation. Or alternatively, co-derivation would 
> not be allowed if a non tagged type has a change of representation. 
> This seems a small price to pay for the feature.

The tagged type is not the problem, and it should be allowed to have a change
of representation. Untagged types with primitive routines never allow change
of representation (except via a language bug) - see 13.1(10). So this doesn't
seem to be a problem unless we decide to remove the nasty (and incompletely
enforced) 13.1(10).

> 2) Extra point 2:
> The extra conversion in the call:
>  Do_Something (My_Cont, Cursor (My_Curs)); is annoying. It could be 
> avoided by allowing T'Class on non-tagged types ONLY IN FORMAL 
> PARAMETERS  (trying to quickly jump into my asbestos suit), with the 
> obvious meaning: compatible with any type derived from it, perhaps 
> also under the condition that there is no change of representation.

That would work, but you'd still have the typing problem noted above.

The way to fix the typing problem would be to require that both types be 
tagged (not currently allowed). We'd only allow co-derivation for such types,
so their tags would always be related. That would avoid the problems of 
multiple dispatch. (I don't think the tags could be the same, because 
predefined single type stuff like streaming and assignment has to be different
for each type. But the routines with multiple controlling parameters would 
have to be present in both tags, and designate the same subprogram.)

We already have a check for any subprogram that has multiple controlling 
parameters that the tags match. We could replace that by a rule that the tags
have to have the same relationship (come from the same co-derivation). In that
case, dispatching would always go to the routine with the correct parameter.

A dispatching routine would thus have to have dynamically tagged values (or
statically tagged) for both parameters. And 'Class is automatically available.
So Do_Something could be declared:

Do_Something (Container : in out Map'Class;
              Position  : in out Cursor'Class);

I think this would solve the typing problem, but I don't know if it works for
the generic derived type issues that Steve (who else?) raised privately with 
me. And I don't know if it would interfere with any other language mechanisms.
Someone more motivated than me needs to figure that out.

P.S. It also would be an issue with the existing containers. Not sure if 
making Cursors tagged would be compatible enough; it would make regular 
derivations of them (and containerss) illegal. I think the result would be 
much better (if the code was rewritten to use coderivation), but the code 
breakage might be too much to stomach.

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

From: Randy Brukardt
Sent: Wednesday, October 30, 2019  2:41 PM

[Split from another thread filed in AI12-0257-1 -- Editor]

Aside: Ada does not have good support for the case where an ADT has various 
ancillary private types, as such types need to be derived/extended as a group.
I've figured out a workable solution for that (after years of trying), but the
ARG decided not to consider the probem further this go-round. Too bad, but 
understandable.

If you care, the solution involves treating a pair (or more) of tagged types 
as a single entity for the purposes of dispatching and derivation. If anyone 
is interested, I can explain further.

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

From: Bob Duff
Sent: Wednesday, October 30, 2019  3:06 PM

...
> If you care, the solution involves treating a pair (or more) of tagged 
> types as a single entity for the purposes of dispatching and 
> derivation. If anyone is interested, I can explain further.

I had a similar idea some years ago.  I've never worked out the details.  
Didn't you write up an AI on that?  A reference to the AI would be good, or 
if that doesn't exist, I'd be interested in you explaining further.

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

From: Randy Brukardt
Sent: Wednesday, October 30, 2019  3:51 PM

There's an AI, but it was for a different idea that Steve proved wouldn't 
work. (That's AI12-0223-1.) The last e-mail in that AI outlines this idea,
but I never worked out any details beyond those associated with derivation 
and dispatching.

If you'd like me to write up something a bit more formal, I can put that 
on my TBD list and post it later as a new thread (it shouldn't get combined 
with this one).

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

From: Bob Duff
Sent: Wednesday, October 30, 2019  4:22 PM

I won't try to bully you into doing it, ;-) but if you feel like it, go ahead.
If you do, I'll read it.

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

Questions? Ask the ACAA Technical Agent