Ada-Rapporteur-Group / User-Community-Input

Ada User Community Input Working Group - Github Mirror Prototype
27 stars 1 forks source link

Attribute to check subtype equivalence (or assignability) in a generic #15

Open sttaft opened 2 years ago

sttaft commented 2 years ago

In a generic that takes an indefinite private type as a formal parameter, there is no clean (exception free) way to tell if two instances are of the same definite subtype.

If you know you have an indefinite array type, you can compare the length - if the length is the same, they're the same subtype, and you can assign without an exception.

If you have a variant record with known discriminants, you can check if the discriminants match, and it is safe to assign one to the other.

If you know you have a class-wide tagged type, you can compare the tags of two instances, and if they match, it is safe to assign.

...but if you just have a private indefinite type, and you know both values are of the same private indefinite type, there's no attribute to check if they are of the same subtype or not (even though there are ways to check for each individual kind of indefinite type). If they are of the same subtype, assignment will work. If they are not, assignment is guaranteed to raise a constraint_error.

That leaves two options: 1) always dynamically allocate for indefinite types 2) always attempt assignment, and dynamically allocate when it raises a constraint_error

If a program uses an indefinite container that most often uses the same definite subtypes when replacing elements, and does so a lot, option 2 could be more efficient than option 1 for those cases, but right now there's no way to logically check if that assignment will work.

If it were possible to check, this would be very useful in the implementation of indefinite container packages, for example - both those defined by the language, and those implemented by developers). These typically have to free and reallocate any time an indefinite instance is replaced by another. If it were possible to check if the old and new values shared the same definite subtype, there would be no need to free and reallocate when true.

Surely there can be many times when an indefinite container is needed, but once in use, certain values would be updated with values of the same definite subtype. If so, this check should provide an improvement in performance.

This also seems like a gap in the language, given that this can be checked for indefinite arrays, variant records, and tagged types if you know about them, but not for indefinite private types.


An attribute that could be used on subtypes might look like this:

   S'Compatible_Subtypes (Arg_1, Arg_2)
      or
   S'Subtype_Match (Arg_1, Arg_2)
      or
   S'Same_Subtype (Arg_1, Arg_2)

Where S is an indefinite subtype, and Var_1 and Var_2 are instances of that indefinite subtype.

An object-based version could potentially be done like this:

   X'Same_Subtype (Arg_2)
   etc.

The object version seems a little broader. It's less clear how that version enforces that both objects are have the same indefinite subtype before doing the check.

There's room for discussion on the name of the attribute, and whether it is an object or a subtype attribute, or both, but I think it is an important feature that could immediately enable an improvement to the implementation of existing language defined packages (namely indefinite containers), as well as improvements to user-defined code that could take advantage of such an attribute.

Joshua Fletcher, fletcher.ua@gmail.com, 6/30/2022 10:43:49

ARG-Editor commented 1 year ago

I think this question is mis-described, in that this issue isn't with the subtype of the object (which may be unconstrained) but rather with the constraint. Moreover, the question of whether one object can be assigned to another is different than just the constraint (since accessibility and tag checks also could be involved).

I do think that it would be useful if one could get an answer to the question of whether an assignment would succeed (especially for any kind of private type), so I would suggest an attribute on the line of:

                  <LHS>'Is_Assignable(<RHS>)

which would return True if the current value of the can be assigned into the without raising Constraint_Error or Program_Error. (Storage_Error and user-defined exceptions would remain possible, of course, they couldn't be detected this way). The types of and would have to be the same. I put the as the object since that has to be the name of a variable anyway, so it works well as an attribute prefix. The could be any expression, so it would be better was an attribute parameter.

Is this roughly what you are looking for?

                 Randy.
joshua-c-fletcher commented 1 year ago

The Is_Assignable might meet the needs of what I'm looking for, but it is doing more than I had in mind, and isn't quite the same thing.

I'd want to use this to determine whether I need to dynamically allocate, or can just assign directly when replacing a previously allocated value. If the direct assignment is going to raise an exception for being out-of-range, or for an accessibility check failure, dynamically allocating it isn't going to work any better than just doing an assignment; it might as well just raise the exception.

I'm not so much concerned about the constraint of a subtype, as with the definite subtype of an instance when the generic formal parameter is an indefinite type.

You can have a named subtype that is indefinite, but as soon as you declare an instance of that subtype, the actual instance is considered to have an anonymous definite subtype. Therefore, no actual instance exists without belonging to a definite subtype - it might be anonymous, but it's still a definite subtype. Whether that subtype is constrained or not is different.

-

Suppose you have a generic that takes an indefinite type like this as a formal parameter:

type Element_Type (<>) is private;

Inside the generic, some structure manages instances of these - maybe it's like the container packages; indefinite holders, indefinite doubly linked lists, indefinite ordered maps, or maybe it's some other user-defined structure; someone might want to make their own container-like generic package, or build one out of the other containers.

The first elements added to the containers have to be dynamically allocated because the type is indefinite - the size is not known at compile time, so some 'create' or 'add' call dynamically allocates the first instances of Element_Type in the container.

Then, later, you may want to re-assign a value in the generic that was previously allocated - something like the Replace_Element call for Indefinite_Doubly_Linked_Lists.

If there's going to be a constraint issue, I don't have a problem getting a constraint error. If there's going to be an accessibility check failure, I don't have a problem getting the accessibility check error.

... but if the generic was instantiated with an Element_Type of String, and I want to replace the element "Hello" with "Smile", the package shouldn't need to deallocate the first element and dynamically allocate a new one. The strings are the same length - they are the same definite anonymous subtype of String. The container package should be able to just do the assignment without freeing the original and dynamically allocating the new one; it should be able to assign the new value to the previously allocated element, if it knows they're arrays of the same type and length - the same definite subtype of the type that was passed in as the formal parameter.

... if the generic was instantiated with a class-wide type: Some_Type'Class... if I previously allocated an element with a type of a particular tag, and I want to replace it with another element that has the same tag, the container package shouldn't have to free and reallocate, if it can confirm the new value has the same tag.

... if the generic was instantiated with an Element_Type of some variant record type, if I previously allocated an element with one particular variant, and I want to replace that with another element of the same variant, the container package shouldn't have to free and reallocate if it can confirm that the new value has the same discriminants and is therefore the same definite subtype as the previous one.

All three of those things are checkable if I wrote my own container package that was a little less generic:

If the generic formal parameter specified an indefinite array type, I could compare the lengths. If the generic formal parameter specified the class-wide type, I could compare the tags. If the generic formal parameter specified the the discriminants, I could compare the discriminants.

... and based on those things, the code could decide whether to free the previous value and dynamically allocate the new one, or just skip those steps and assign directly.

When the generic formal parameter only specifies that it is an indefinite type, the code can't check any of those things without trying out the assignment, and seeing if it gets a Constraint_Error... if the Constraint_Error occurs due to one of those three scenarios, it can free the original and dynamically allocate the replacement... but it would be better to be able to check without getting the Constraint_Error.

What if such a generic was just instantiated with something like an Integer subtype with a range limit? Then the Replace_Element would take a value of that subtype as its in parameter. Even if I just passed in an unconstrained Integer, or an Integer with a different range, it would be the matching subtype inside the call to Replace_Element, and should be recognized as the same subtype as any element that was created by the original 'create' or 'add' call. If it is out-of-range, the Constraint_Error would be raised at the call to Replace_Element, rather than inside it. Inside the call, the parameter would have been effectively cast to the matching subtype, and the check would pass, and allow direct assignment without dynamic re-allocation

If I'm going to get a Constraint_Error due to a value being out of range of the type the generic was instantiated with, that's fine; I'm not looking for a check that would avoid that. Accessibility check exceptions could still be raised; that would indicate another sort of problem to be addressed in client code. The tag checks were accounted for in the original proposa.

I don't have a big problem with the Is_Assignable variant of the proposal, but I think it is more complex than what I had in mind, and it defeats the purpose a bit if the check is more expensive than it needs to be. The idea is for it to be a fairly inexpensive check so that we can avoid dynamically allocating when we don't need to.

ARG-Editor commented 1 year ago

Sorry about taking so long to get back to you. A number of other things have intruded. I want to come to some conclusion on this topic, as I have an action item to combine this issue with another topic and create an AI for immediate consideration.

Anyway, your discussion above has explained the disconnect between our understandings of the problem. Luckily, it's most one of terminology. When you say: "If there's going to be a constraint issue, I don't have a problem getting a constraint error." and then you say that you want to assign into an existing allocated object, I get confused, because these are exactly the same things.

The model for Ada is that an allocated object is "constrained by its initial value". That means that the constraints come from the initial value, not a declared subtype somewhere. But as far as checks go (both in the language and in implementations), there is no difference in these cases. They're all constraint checks. Therefore, what you are interested in is knowing the the result of a constraint check before it is actually made. There will be, admittedly, some cases where that information does not help,

You also mentioned a case of a tag check. Again, you are interested in knowing the result of that tag check (which I believe will raise Program_Error if it fails) before you make it.

The Is_Assignable attribute I suggested captures both of these cases. It does capture some additional cases, but those probably would not actually be relevant in your usage (you're not going to get an accessibility check unless you have co-extensions or are using 'Access unsafely).

Let me back out a bit. One of the principles of the Ada design is that we try to provide building blocks, rather than direct solutions to individual problems. So we try to find general solutions to a class of problems, rather than single solutions to a specific problem.

In this case, the general problem is being able to query before an operation whether that operation will raise an exception. For instance, one often checks an access value for null before dereferencing it. Ada has various facilities for doing this, but they're not complete (as your example shows). Both assignments and return statements have a variety of checks, only a few of which can be tested before execution. And as your example shows, even more of them cannot be tested inside of a generic.

It seems that it might be generally useful to have attributes Is_Assignable and Is_Returnable without needing to worry about which specific checks are involved. (Or some other general solution for these cases.) It certainly seems more in the Ada spirit than having some specific check for the constraints of allocated objects.

It certainly is never a good idea to worry very much about the implementation of something. One thing I've definitely learned in the ARG is that one can only very loosely determine the implementation cost in other Ada people's implementations. It is not at all unusual for something that seems simple to be hard, or something that seems hard to be simple.

In this particular case, the expense for the Janus/Ada implementation would be the same and distributed regardless of what solution is chosen. Janus/Ada uses generic code sharing for all generics (as it is much easier to in-line shared code than it is to shared code that was generated multiple times) Every operation available in a generic body for a generic formal type has to be converted into a subprogram and the address of that subprogram stored in the instance date regardless of whether or not that operation is used in the generic body. (This is more expensive at compile-time than runtime, but there is a cost at runtime). As such, the fewer such operations that exist, the better. (The original reason we adopted this model is that in Ada 83, there was very few operations that needed to be passed. Every version of Ada since has expanded the number.)

My guess is that such an operation would be expanded in-line in a template implementation (such as GNAT). Thus the more complex checks like accessibility would normally end up comparing two identical static values, and thus would ultimately generate no code (no compiler is generating code to compare 1 to 1 after optimization, and likely not before it). Ada compilers are very good at eliminating unnecessary checks, and I would expect that to apply here, too.

So I doubt that the exact definition of the operation would make much difference in the actual performance. (Of course, it would be pretty slow regardless in Janus/Ada, since indirect calls would be needed unless the subprogram was inlined.)

Also note that any such operation (either your Same_Subtype or my Is_Assignable) is going to end up including predicate checks. Since those are user-defined code, they could swamp out the performance of all else regardless of the definition. (Of course, like accessibility checks, they can be avoided by construction.) My main point here is that this is never going to be a simple check (at least in general), it's going to drag in lots of complicated stuff. Only a problem-specific hack could be simple, and I don't think anyone wants that (at least in the Ada language).

Moral: premature optimization exists in language design, just as it does in programming. We'll need to let the full ARG consider the problem in order to get a picture of whether or not there is any performance concern with possible solutions. It's not something that you or I can determine here (or in the AI I'm going to create for that matter).

I don't like your suggestion of "Same_Subtype" for a number of reasons: (1) Same_Subtype implies to me same nominal subtype, which is a compile-time check (and definitely not what you want). While Ada has a notion of runtime subtype, I think of that as mainly the constraint (discriminants, bounds, range). In most cases, you can't change the constraint, which is where the checks come from. (2) The operation seems identical to a membership check; it would be confusing as to when you would need one or the other. Is_Assignable seems to be to be clear about its use. (3) Same_Subtype doesn't seem to include the case where the types are different (that is, have different tags).

(1) and (3) could be solved with a different name for the attribute, but even then (2) would remain.

Ergo, I believe more general attributes would make more sense. (It would make even more sense to let memberships make this change, but I haven't been able to figure out how that could work. Maybe someone else will have a better idea.)

                 Randy.
joshua-c-fletcher commented 1 year ago

Hi Randy,

First, I concede the following statement I made was confusing and not helpful: "If there's going to be a constraint issue, I don't have a problem getting a constraint error."

I wrote that after saying my proposal could be used (for example) in something like a Replace_Element call for Indefinite_Doubly_Linked_Lists, as an example, and I only meant that if existing implementations (that probably free and reallocate every for every call) would ever raise a Constraint_Error, I'm not looking to avoid such occurrences. That said, there shouldn't actually be any such occurrences of Constraint_Errors when freeing and reallocating every time (except perhaps at the calling code, if the caller attempts to pass in a value that is out-of-range of the formal type used for the Element_Type, but that's different).

More practically, what I was driving at was illustrated by a point you made, when you wrote:

One of the principles of the Ada design is that we try to provide building blocks, rather than direct solutions to individual problems. So we try to find general solutions to a class of problems, rather than single solutions to a specific problem.

Is_Assignable sounded to me more like a solution to a specific problem, whereas the subtype based names I had suggested were checking something more basic that happens to ultimately implies assignability.

The language would provide the building block; assignment is the most obvious, more specific, use case.

An Is_Assignable attribute like you described would meet the needs of what I'm looking for, but my main concern about it was that it might end up checking more than than it needed to, and become more of a bespoke solution rather than the 'building block' I had in mind. For example, the discussion was bringing accessibility checks in and I didn't think those needed to be checked by this call. We're not trying to avoid every conceivable exception.

You mentioned:

Ada has a notion of runtime subtype, I think of that as mainly the constraint (discriminants, bounds, range)

That "runtime subtype" that represents the constraint on an instance is exactly the kind of anonymous subtype to which I was referring - every instance of any type has one, and it is always definite, even when the named type maybe indefinite.

Inside a generic, for example, you might know that two instances are declared to be of the same type, but the formal type might be 'type T (<>) is private', so you can't currently tell if they are the same "runtime subtype".

I think we can get part way there with an existing attribute: S'Definite. If this attribute says that the formal indefinite subtype is actually a definite type, then we can go ahead and do that assignment. That attribute gives precedence to the idea of an attribute that acts on a formal indefinite subtype in a generic.

In a way my proposal is an extension of that idea: We can know that the formal type is indefinite, but when it is, we can't yet check if two instances are of the same definite "runtime subtype" (that is have the same constraints/tag) If we knew they were, we could assign one to the other, just like we could if the formal type was definite, itself.

Outside of a generic, we can check constraints like 'Length or discriminant values, or tags, so the check is less valuable outside a generic, but you mentioned predicate checks as well, which might provide a rational for having such an attribute outside a generic, too.

Your comment about membership checks is interesting, that my proposed operation seems like a membership check. It is like a membership check, but it is distinctly different than existing membership checks.

Here we're checking if two instances of one indefinite type share membership in common runtime subtype; i.e. they have the same constraints.

To express that as a membership check, it would probably still need a new attribute like this:

if RHS in LHS.all'Runtime_Subtype_For_Membership_Check then LHS.all := RHS; else Free (LHS); LHS := new T'(RHS); end if;

Where that condition is effectively equivalent to the variations we've been talking about:

if LHS'Is_Assignable (RHS) then

Or if T'Some_Attribute_Checking_For_Matching_Constraints (LHS, RHS) then

Bottom line, I agree with you that general attributes would make more sense; and that's what I thought I was arguing in favour of. I'm glad we've got the same goals. Any of the above three approaches, with a suitable name (not like those longer two), would be alright in my view; whichever seems best as a 'building block', as you said.

ARG-Editor commented 1 year ago

I wasn't expecting an answer quite this fast. :-)

I think I have enough information to write an AI, probably with a number of options, and the ARG can discuss them.

I'm writing this AI to cover two issues, the one you raised, and a rather similar one raised a few years ago. It would be bad to consider the questions separately, because then there would end up being a need to reconcile the solutions.

As I previously mentioned, the other question was that there are operations in Ada where one cannot make a pretest to avoid exceptions. In particular, both assignment and return statements have this property (in general). Should there be a way to test for problems before making an assignment or return?

So, when you say:

We're not trying to avoid every conceivable exception.

The other question is a more general question asking specifically this. I would like to answer both questions (your's and the older one). I think you've made an argument that there isn't an obvious workaround in the case of some exceptions, which seems reasonable. OTOH, programmers are clever and often find ways to use things that we language designers never anticipated. So making assumptions about what is useful tends to be not that compelling of an argument.

This brings us back to the building block question. When looking at the second question, the issue is being able to pre-test for problems before an operation; there one probably doesn't want that test to be complex. OTOH, one might instead want to be able to separate the tests in the categories as some are likely to be useful in a particular context, and others are not.

Thus, the answer seems to be clear as mud. :-) Luckily, I don't need a firm answer to write an AI.

... Later, you talk about membership checks.

It is like a membership check, but it is distinctly different than existing membership checks.

  • Usually a membership check is checking if an instance is a member of a named type or a named type's 'Class,
  • Or if an instance is a member of a range.

This is a very narrow view of membership checks. First of all, there are no named types in Ada, you can only name subtypes. And membership in a subtype includes all of the constraints, predicates, and exclusions that apply to the subtype. So this is a far more powerful statement than just checking a range.

Moreover, Ada 2005 extended this to do accessibility checks against named types, and Ada 2012 extended memberships to do set memberships: Color in Red | Green | Blue

Indeed, that last capability is a weird way to write equalities. Indeed, it calls the user-defined "=" if there is one.

So a membership is more like an equality than checking if the constraints of an object are compatible with that of another object (which is only equality of part of the object).

In particular, you can write:

Object_A in Object_B

and that means exactly:

Object_A = Object_B

It would make more sense (based on the uses for subtypes) for this to make the compatibility check that you need (which only makes sense for composite types), but it's too late for that.

So we'd have to use an extra keyword for this non-exact matching:

Object_A some in Object_B

but I don't think we have a keyword with the right meaning. And this doesn't seem important enough to introduce a new reserved word.

Bottom line, I agree with you that general attributes would make more sense; and that's what I thought I was arguing in favour of. I'm glad we've got the same goals. Any of the above three approaches, with a suitable name (not like those longer two), would be alright in my view; whichever seems best as a 'building block', as you said.

Agreed. Thanks for your help so far. You can comment on the AI once I get it written up (I will note that here).

          Randy.
sttaft commented 1 year ago

Several languages permit one object to be declared to be "like" another object. An attribute which supported that might accomplish what we want. If we use the obvious attribute name "Subtype" then we could write:

A : B'Subtype := B; -- should not raise Constraint_Error

or:

   if A in B'Subtype then
        B := A;  -- should not raise a Constraint_Error
   end if;
joshua-c-fletcher commented 1 year ago

I really like this option, and with that approach the attribute could be used for declarations as well as the check I had in mind.

A : B'Subtype := B; -- should not raise Constraint_Error

and

   if A in B'Subtype then
        B := A;  -- should not raise a Constraint_Error
   end if;

For the comparison to work correctly, though, B'Subtype would need to return what Randy called the "runtime subtype" of B, and what I called the "definite subtype". A and B could have been declared or passed in as a parameter of an indefinite subtype, and both could then be members of that subtype, but if B'Subtype clearly returned the definite subtype or the runtime subtype of the instance (rather than what it might have been declared as), then I think this would be a perfect solution to problem that prompted this discussion.

sttaft commented 1 year ago

For the comparison to work correctly, though, B'Subtype would need to return what Randy called the "runtime subtype" of B, and what I called the "definite subtype". ...

Yes, that was certainly the intent of what I was suggesting. We sometimes call it the "actual subtype" as opposed to the "nominal subtype". In any case, it is helpful to hear that you think such an attribute might provide the desired functionality.

ARG-Editor commented 1 year ago

There is a minor issue with B'Subtype that doesn't occur with Is_Assignable, however. If the actual type to your generic formal type is a type that does not constrain objects by their initial value (one that has a constrained partial view), then using B'Subtype alone would require making new objects even when that isn't needed by the underlying type (which is mutable in this case). If the formal type has discriminants, then Obj'Constrained could be used to avoid this problem, but for a type without discriminants (as in your container example), there doesn't seem to be any existing way to avoid the problem. And it seems annoying to go through the work of trying to avoid making a new object, and ending up doing so anyway when it isn't needed.

One could imagine extending Obj'Constrained to deal with this case, but that certainly adds complication to the use of 'Subtype and to the solution of the problem.

joshua-c-fletcher commented 1 year ago

Yes, I see. if A in B'Subtype then B := A; -- should not raise a Constraint_Error end if; If B's subtype were a mutable variant record (with defaulted discriminants), and A had been declared of the same type, but with discriminants expressed in the declaration, such that A itself is not actually mutable, we should still be able to assign A to B, even though A is more constrained than B is.

Also, if A's subtype is a mutable variant record, and B was declared in a way that made it more constrained, it should still be possible to assign the value of A to B, if A happened to be of the right variant.

I think there's still room for this to work, though, and that the membership check can account for this.

A in B'Subtype doesn't have to mean that the subtype and constraints of A has to match the subtype and constrains of B exactly (although generally it would). In the case where A and B are variant records of the same nominal, mutable type and one may be more constrained than the other, the membership check would return True, effectively, if A could be assigned to B. So if B was mutable, it would return true, even if A is more constrained, and if B was not mutable, it would only return true if A's mutation (variant) matched the constraints on B. I think that's a reasonable interpretation of the membership check, and it is consistent with how the existing membership checks work (see below)

This would be different than checking: A'Subtype = B'Subtype, and I think that's OK as long as it is documented clearly. Of the various options, I think the membership check approach is the nicest, if there were concerns about making a membership check accommodate this case, then an 'Is_Assignable attribute could do it.

That said, I did an experiment with GNAT, and the membership checks with nominal subtypes appear to have the desired behaviour already... if I declare a mutable variant record and make an instance where it is mutable, and an instance where the discriminants are declared with the type such that the instance is not mutable:

So I think the membership check approach is fine.

sttaft commented 1 year ago

On Sat, Feb 11, 2023 at 3:08 AM ARG-Editor @.***> wrote:

There is a minor issue with B'Subtype that doesn't occur with Is_Assignable, however. If the actual type to your generic formal type is a type that does not constrain objects by their initial value (one that has a constrained partial view), then using B'Subtype alone would require making new objects even when that isn't needed by the underlying type (which is mutable in this case). If the formal type has discriminants, then Obj'Constrained could be used to avoid this problem, but for a type without discriminants (as in your container example), there doesn't seem to be any existing way to avoid the problem. And it seems annoying to go through the work of trying to avoid making a new object, and ending up doing so anyway when it isn't needed.

I don't completely follow this. Could you give a more concrete example?

One could imagine extending Obj'Constrained to deal with this case, but that certainly adds complication to the use of 'Subtype and to the solution of the problem.

Can you explain what sort of extension to Obj'Constrained you could imagine?

Thanks, -Tuck

Message ID: @.*** com>

ARG-Editor commented 1 year ago

Tucker write:

I don't completely follow this. Could you give a more concrete example? ... Can you explain what sort of extension to Obj'Constrained you could imagine?

Sure. Better start at the beginning. The OP in considering the implementation of an indefinite container. It would have a spec something like:

generic type Element_Type (<>) is private; package Indef_Cont is ... procedure Replace (Cont : in out Container; Position : in Cursor; New_Item : in Element_Type); ... end Indef_Cont;

The body would have the container data structure, and an access types defined to contain each of the elements:

type Elem_Ptr is access Element_Type;

The body of Replace would look something like:

  procedure Replace (Cont : in out Container;
                     Position : in Cursor;
                     New_Item : in Element_Type) is
  begin
      declare
          The_Elem : Elem_Ptr renames ... -- The element to replace.
      begin
          Free (The_Elem);
          The_Elem := new Element_Type'(New_Item);
      end;
  end Replace;

The OP is concerned about all of this freeing and reallocated churn. He'd like to avoid it whenever the New_Item can be assigned into the existing element. The problem is that one does not know (a) whether the type is discriminated (or has bounds, for that matter), and (b) if the type is one that is constrained-by-its-initial-value (or not).

I suggested an Is_Assignable attribute for the purpose, which would replace the body of the declare block by:

         if The_Elem'Is_Assignable(New_Item) then
             The_Elem.all := New_Item;
         else
             Free (The_Elem);
             The_Elem := new Element_Type'(New_Item);
         end if;

You suggested 'Subtype instead, which would look like:

         if New_Item in The_Elem'Subtype then
             The_Elem.all := New_Item;
         else
             Free (The_Elem);
             The_Elem := new Element_Type'(New_Item);
         end if;

This checks whether the constraints match, but it doesn't check whether The_Elem is constrained by its initial value. (If it is not, then the assignment can be done even when the constraints don't match.) So this still is allocating and freeing more than necessary, it really doesn't solve the OP's problem.

Remember that types with constrained partial views are not constrained by their initial values. So in the following:

 package P is
     type Priv is private;
 private
     type Priv (Cap : Natural := 0) is record ...
 end P;

when one allocates an object of type Priv, one still can change the discriminants of an allocated object. OTOH, if the same type is declared without the private type, then one cannot change those.

So two rather similar instantiations (with these two type) of our Indef_Cont package would have different behavior internally.

I was thinking that an extension of the 'Constrained attribute could be used to deal with this part of the problem. Specifically, we could allow applying the attribute to objects of indefinite formal types (or indefinite private types or even any private type), and it would return True if the object is constrained (including by its initial value). That would allow writing:

         if The_Elem'Constrained and then
            New_Item not in The_Elem'Subtype then
             Free (The_Elem);
             The_Elem := new Element_Type'(New_Item);
         else
             The_Elem.all := New_Item;
         end if;

Joshua has suggested instead having The_Elem'Subtype always return True if the object is mutable. That would be easier to use, but also would make 'Subtype only usable in this context (if you want to know if the subtype [constraint] matches for a mutable object, that wouldn't be possible, and that seems unfortunate and somewhat inconsistent).

Hope this clears things up for you.

                 Randy.
sttaft commented 1 year ago

Unless I am still confused, you seem to be presuming that X in Y'Subtype will return False if Y is unconstrained while X is constrained by its initial value. To me that is inconsistent with how things work now. If I substituted Y's actual subtype into the membership test, the test would return True even if Y is unconstrained and X is constrained by its initial value. So personally I prefer the "X in Y'Subtype" approach to the "Y'Is_Assignable(X)" approach, and I think it is quite consistent with how other membership tests work.

It would not seem to be much of a stretch to allow the Constrained attribute to be usable with objects of a formal private type, if you want to answer the question of whether a given object is or is not constrained. Presumably for objects of an elementary type, or of a non-discriminated composite type, it would be defined to return True.

-Tuck

ARG-Editor commented 1 year ago

I don't understand your point, since your assumption about my thinking doesn't have the effect I pointed out.

Anyway, I'm assuming the X in Y'Subtype checks that the runtime subtype of X is compatible with the runtime subtype of Y. The nominal subtype of neither object comes into play, and in particular whether or not the nominal subtype is constrained or unconstrained does not come into play (there is no such notion at runtime - every object has a known, specific runtime subtype). Since the ability to compare the runtime subtypes of two objects is not available in Ada, and it is a useful capability regardless of the nominal subtypes involved, I've been presuming the proposed membership did that.

Assignments, of course, are more liberal than a straight comparison of constraints, and thus I thought another attribute would be required to get the entire functionality. (Memberships generally test convertability, not assignability.) Which makes it less clear whether Is_Assignable or Subtype+Constrained is the better idea.

In any case, we don't need to decide this now. A full ARG discussion should help clarify, and it might help to have additional use cases beyond the one Joshua identified.

                             Randy.
ARG-Editor commented 1 year ago

This topic is included in AI22-0071-1.

sttaft commented 1 year ago

The draft AI22-0071-1 can be found here: https://docs.google.com/document/d/1ZWEuOyd37DX0avaLKD3m7njtFp6wshoxJzccWM752c4