Hi,
I do not understand what construction like these:
generic
type Item_Type;
with function "<" (Left, Right : Item_Type) return Boolean;
procedure sort;
it compiles with a fake body. So the only use it has in the procedure’s body, is to provide parameters to the operator, but then only by getting values through function calls I suppose, since you can’t declare variables nor be provided a formal object. So in this body, this operator can’t be called at all, right ?
Then why does it compile ?
I don’t understand what this means. My understanding and experience is that sort has a very real body, defined elsewhere.
But maybe you mean the body of <. In that case, I’d suggest that it the point is to check how you use it in sort until you instantiate the sort for some type.
Once you have something like
procedure Integer_Sorter is new sort(Item_Type => Integer, "<" => <>);
…then it compiles with a very concrete body, and you get working code.
By fake I mean a stub. My question was, while this is valid, I don’t understand what incomplete type with no complete definition does… It doesn’t seem to be usable at all. I don’t see that the formal function can do anything in this code, yet it compiles.
generic
type Item_Type;
with function "<" (Left, Right : Item_Type) return Boolean;
procedure sort;
procedure sort is
begin
null;
end sort;
The main benefit I have found for them is making reference counted access types that can be used for self referential types (like list or map nodes). That said GNAT like 10/11 through much of 13 and some of 14 has some serious bugs with incomplete types, so I haven’t been able to leverage them well in that use case for those versions.
Note that those are not stubs, those are forward declarations essentially which is an important distinction. Stubs already exist, a forward declaration means the code will be made available soon after when it is needed, so the compiler can go ahead and compile what it can for now and the rest will come.
The rationale’s section did help
But on the other hand, I was left with more questions than answers… I understand it has to do with freezing, but concretely, given a generic with these parameters :
generic
type Cursor;
with function Has_Element(Position: Cursor) return Boolean;
what can you actually do with the type Cursor ? Since its definition is meant to be completed after the generic’s instantation, it seems that inside the generic the compiler has no information except whether it is tagged or has discriminants.
This exemple (Iterator_interface) is pretty useless for my understanding since it’s purely abstract so it doesn’t do anything. I fail to see what you could do, with next to information about the type except that it exists ??
For the package iterator interfaces it basically only provides the type needed to specify First, Last, Next, and Previous. For the compiler it specifies the “under the hood” type used in “for of” and “for in” loops, as well as the validating function for “continuing iteration” (the Has_Element function). It’s a very special case generic package that the compiler has “more intimate knowledge” of in order to allow you to specify user custom iteration.
You’re right in that that package specifically doesn’t do much with the type. Other things you can do that are more interesting are make and use access types of that type, since the compiler doesn’t need to know the full details of the type at this point to allow you to declare access types for Cursor.
On my previous mention of reference counted access types that support self referencing, this become more useful (the ability to declare access types for an “Incomplete_Type” formal).
There isn’t a huge area of use cases for incomplete types, but I will say if you can get away with using them as your generic parameters, then your package can be declared for more user types than it would if you used a complete type like type Thing is private, so it allows your users to make more interesting uses of generics sometimes. That said, if your generic needs a complete type,then just use a complete type.
For example if you wanted to use your generic created type inside of the type you are passing in:
(not compiled, purely typed off the cuff)
generic
type Element_Type(<>);
type Element_Access is access Element_Type;
with procedure Deallocate(Element : in out Element_Access);
package Ref_Counted is
type Shared_Access is tagged private;
type Weak_Access is tagged private;
-- other operations
private
-- private declarations
end Ref_Counted;
type Node;
type Node_Access is access Node;
procedure Deallocate(Node : in out Node_Access);
package Node_Smart_Access is new Ref_Counted
(Element_Type => Node,
Element_Access => Node_Access,
Deallocate => Deallocate);
type Node is record
Value : Integer := 0;
Previous : Node_Smart_Access.Weak_Access; -- Note here!!
Next : Node_Smart_Access.Shared_Access; -- Note here!!
end record;
-- later on:
procedure Deallocate(Node : in out Node_Access) is
procedure Free is new Ada.Unchecked_Deallocation(Node, Node_Access);
begin
Free(Node);
end Deallocate;
In the above example note the two lines for Previous and Next in the full Node record definition. Those lines are not possible to do if the generic package used type Element_Type(<>) is limited private; instead of type Element_Type(<>); This is because you would have to wait until after the Node record is fully declared to instantiate the package. Because type Element(<>); is used instead you can declare the package before the full declaration of the Node record, allowing you to use the types inside that package early.
For Ada.Iterator_Interfaces, I don’t know the logic for the “why” an incomplete Cursor type is chosen, but since the package doesn’t “need” a complete type, but I think it is the correct choice. I always like to constrain myself as much as I can to ensure my coding mistakes are caught by the compiler as much as possible.
An incomplete type in the formal parameters means your generic indeed cannot manipulate variables of that type directly. So most of the time you can’t really use that in practice. But when you can, the main advantage is that you can instantiate the generic before the full view of your type is visible.
For instance
type T is private;
package T_Sort is new Sort (T);
If you are using is private, you would have to instantiate T_Sort somewhere in the private part, which means it would not be visible to users of your package.
I have used that, when possible, in my traits library: the idea of the trait is that you describe statically something that can you can with a type. For instance, you could have a Sortable trait which encapsulates the type and the “<” operator. Then the trait can be instantiated in the public part, and you can pass it on to any other generic package that needs a “Sortable” type.
Another usage of incomplete types for generics is for interfacing and enforcement.
Consider:
Generic
Type X;
with Procedure Needed_Operation( Object : X );
Package Example_1 is
End Example_1;
and
Generic
Type Z is private;
with Procedure Print( Data : Z ) is <>;
with Package Dependency is new Example_1(X => Z, Needed_Operation => Print);
Package Example_2 is
End Example_2;
So, just as you can use generics to do static polymorphism, you can also use them to structure subsystems; granted, most people don’t decompose their systems like this… probably because it’s more work, but it absolutely can be used to make things more robust.