$ gnatmake example.adb
gcc -c example.adb
example.adb:16:17: error: ambiguous expression (cannot resolve "New_B")
example.adb:16:17: error: possible interpretation at pak.ads:8
example.adb:16:17: error: possible interpretation at pak.ads:6
gnatmake: "example.adb" compilation error
pak.ads
package Pak is
type A is abstract tagged private;
type B is new A with private;
function New_B return B;
type C is new B with private;
function New_C return C;
private
type A is abstract tagged record
null;
end record;
type B is new A with null record;
type C is new B with null record;
end Pak;
pak.adb
package body Pak is
function New_B return B is
begin
return (others => <>);
end New_B;
function New_C return C is
begin
return (others => <>);
end New_C;
end Pak;
example.adb
with Ada.Containers.Indefinite_Vectors;
with Pak;
procedure Example is
use type Pak.A; -- compiler insists for this
package Pak_Vectors is new Ada.Containers.Indefinite_Vectors
(Index_Type => Positive,
Element_Type => Pak.A'Class);
subtype Vec is Pak_Vectors.Vector;
V : Vec;
begin
V.Append (Pak.New_B);
V.Append (Pak.New_C);
end Example;
C inherits New_B from B. Thus you have two overloaded functions:
function New_B return B;
and
function New_B return C;
both can be converted to AâClass. So the error goes.
P.S. In Ada 95 it was a bug because you had to explicitly override a covariant function since there is no safe way to inherit it. Later this rule was relaxed for degenerate cases like when the extension is null record (your case).
In my view it was language design bug. If the compiler had rejected your code, you would probably redesign it. Having New_B and New_C is C rather than Ada. You should have one constructing primitive function working for all class.
Thanks, I suspected it was caused by the second level of inheritance.
The above example is dumbed down to distill the problem, the actual case looks more like:
type A is abstract tagged private;
-- ... some abstract subprograms of A
type B is new A with null record;
function New_B return B;
type C is new A with private; -- additional components in full view
function New_C (P : Param) return C;
type D is new A with private; -- likewise
function New_D return D;
type E is new A with private; -- etc.
function New_E (P : Param) return E;
type F is new E with private;
function New_F (P : Param) return F;
[ A ] abstract
|
__________________________
| | | |
[ B ] [ C ] [ D ] [ E ]
|
[ F ]
So descendant types inherit some data component, implement the abstract subprograms, but may add individual record components and may even ask for some parameters in the constructor subprogram.
I donât know if itâs possible to avoid making a separate New_* constructor for each descendant.
I need those types to be constructible without much ceremony and if one type gets such a constructor, for symmetry all others should get it.
Please, if you have some example of how it could be done using inheritance, leave a link to it.
As I see it, I can just make F descend from A and compose E into its record.
That way Iâm acknowledging defeat, but at least it will compile.
You already have aggregates for that. But if you want a constructing function that is:
type A is abstract tagged private;
function Create [(...)] return A is abstract;
When you create AâClass you need to tell what type it should be, e.g. using a qualified expression or a dispatching constructor. It is like dispatching, but you have no object yet to tell the tag.
If you want type-specific constructing functions then they must be contravariant, i.e. non-primitive. There are two ways of doing this:
Class wide function New_B return A'Class;
Declaring after the type freezing, e.g. in a separate package.
Reading your responses, I realise how complicated the intersection of OOP with packages and visibility is in Ada.
Thereâs much for me to learn, but I like this early experience.
Your Create approach works and I sort of like the result.
package Pak is
type A is abstract tagged private;
function Create return A'Class is abstract;
type B is new A with private;
function Create return B;
type C is new A with private;
function Create return C;
function Create (N : Natural) return C;
type D is new C with private;
function Create return D;
function Create (N : Natural) return D;
type E is new D with private;
function Create return E;
function Create (N : Natural) return E;
function Create (N : Natural; F : Float) return E;
private
type A is abstract tagged record
null;
end record;
type B is new A with null record;
type C is new B with record
N : Natural := 0;
end record;
type D is new C with null record;
type E is new D with record
F : Float := 0.0;
end record;
end Pak;
I dislike that I need to override all the inherited constructors, and that theyâre even visible and callable, but other than that it does what itâs supposed to do.
Why? It is quite simple. When you derive a new type all primitive operations get declared with the dispatching arguments and results having the new type. The technical term is covariance. Other remain as is. The term is contravariance.
If the implementation can be inherited = a body can be constructed out of the old body with covariant arguments and results converted as appropriate (a view conversion). Otherwise, you must provide the new body by overriding.
It depends on whether you can figure out an extension aggregate.
That is the contract you promised to fulfill by declaring Create a primitive operation. It reads: all descendants must have it. Either change the contract or choose another one. You did that by inheriting C privately from B and publicly from A.
You can control it even more finely by using interfaces. You can factor out constructing function into an interface:
type B_Implementation is new A_Implementation with private;
... -- Inheritable stuff goes here
type B_Construction is interface;
function Create return B_Construction is abstract;
type B is new B_Implementation and B_Construction with private;
overriding function Create return B;
type C_Implementation is new B_Implementation with private;
type C_Construction is interface;
function Create return C_Construction is abstract;
type C is new C_Implementation and C_Construction with private;
overriding function Create return C;
I suspect thatâs true, yet Iâd argue it requires a bit of knowledge that cannot be intuited or inferred.
There are multiple ways to do things.
I very much like this gentleman in the Ada space who always says âwhy do we need this new feature? to save a few keystrokes? and whoâs gonna teach it?â.
For example, I took the New_{A,B,C} pattern from the Barnesâ book (in the OOP chapter, but Iâve seen it in other books and code).
The design you showed is very good, but I donât think I could ever have come up with it myself or arrived at it by reading the compiler errors about the resolution problems.
One also falls prey to foreign language influences.
This is what I will be thinking about next.
To broaden the perspective, the type hierarchy discussed here represents different âstatisticsâ.
The idea comes from a Julia library that uses a bit of OOP and which I decided to implement partially in Ada as a pretext to get practice with Ada 95+ features.
The âstatisticâ types have a common âinterfaceâ, but of course they differ vastly in structure and parameters. So I will be thinking if these type âcontractsâ can be rearranged without overengineering the whole thing.
That gives me some ideas: to some statistics you may provide a âweightâ and others donât accept it, and itâs something I thought could maybe be made into an interface.
As a fellow learner whoâs working through Barnes chapter 14 right now, this is the central insight for me.
Since primitive functions are inherited with the type adapted, your naming scheme results in accumulating constructors down the type hierarchy. A type H might have its own New_H function, but also inherited New_E, New_F, and New_G on the way. I doubt that thatâs your goal.
Reusing the same constructor name, maybe even initially marked as abstract, avoids that ambiguity.
type A is abstract tagged private;
type B is new A with private;
function Create return B;
type C is new A with private;
function Create (N : Natural) return C;
type Ext_A is abstract new A with private;
type D is new Ext_A with private;
function Create (F : Float) return D;
To me, this is optimal to avoid the constructor explosion (as long as we derive from abstract types).
Basically, the data extensions (and hence special constructor needs) dictate the type hierarchy.
Iâm not sure how canonical or correct this approach is, but it looks alright.
Not a direct answer, but more of a footnote, I mosty stopped using âwithâ and âuseâ constructs and nearly always use child packages; inheritance is smooth and I find I can manage scope and visibility more easily. Though my primary project is rather small and child parent is only half a dozen or so. I do use âwithâ and âuseâ in my highest parent level package, so it flow down into all the children and grandchildren packages.
The only thing Iâd warn about at this point is not to fall into the common OOP trap to use inheritance where it doesnât really apply. See âComposition over Inheritanceâ.
Iâm hitting these OO roadblocks trying to adapt a statistics library in Ada.
Statistical measures differ widely in their representations and thatâs something I find difficult to accomodate because of Adaâs strong typing and inheirtance model.
John Barnes writes in his book that interfaces are more useful than abstract types, but I just canât see it.
I wish I could inherit Weight component field saying type Mean is new Statistic and Weighted..., but I canât do it (both have record components).
At the same time, the variability in representations makes writing a common interface that could be inherited almost impossible without a lot of abstraction (e.g. functions returning indefinite holders of class-wide type to a tagged record with a value field implemented for each statistic).
So after some time wrangling with the type system and inheritance, I come back to the starting position of just composing Weight into relevant statistics.
These are not roadblocks. You just faced the reality of designing a software system a bit larger than a classroom example. Regardless Ada or OO you must analyse the class (in its common sense) of statistics and organize it into specimen sharing some properties. Once you did that you need to express this in terms of the language type system.
You need full multiple inheritance for that. There was a massive opposition for doing it so a weak MI form of Java-esque interfaces was introduced in Ada 2005.
[ The problem is that there are a few people who understand the complexity of MI and do not want it for that reason and there are many people who understand nothing and do not want it too. Those who must face the music of large projects are always in minority, ]
A poor manâs multiple inheritance can be hacked using generics. You go this way:
generic
type T is new Statistics with private;
package Add_Weight is
type Weighted is new T [and Weighted_Interface] with record
Weight : Float;
end record;
end Add_Weight;
You instantiate this package to get new types with weight. You can move it to the private part, use an interface Weighted_Interface. I must warn you that this approach explodes quite quicky and generic code is utterly unreadable and unmaintainable.
Or you can use good old cut-and-paste approach called âcomposition.â
P.S. I would throw all Ada extensions since Ada 95 for multiple inheritance and abstracting arrays and record types. (You cannot have record interface, e.g. declare getter and setter visible as a record member.)
Thanks.
Iâve been able to learn a lot thanks to this thread and my work is progressing.
The key takeway for me is that thinking hard about the shape of things takes one most of the way.
Being used to working with simpleton languages, Iâm perhaps conditioned to be ignorant of that.
Ada helps me stop being ignorant.
Thereâs a section for this too in Barnesâ book (âmixinsâ).
Such mixtures of packages, generics and OOP are troublesome to read and think about, language lawyer territory.
Perhaps it could find its way into big projects written by specialists, but itâs not for me.