Interface types and tagged types: abstract functions and ambiguous calls, oh my!

I thought I understood interface and tagged types pretty well, but apparently not!

learn.adacore.com has an example on interfaces that makes sense and compiles, etc. Once I get beyond that relatively trivial example, however, things fall apart.

I think this is as MWE as I can get:

    --  from the interface
    type A is abstract tagged null record;

    type B is new A with private;  --  private part has a String
    type C is new A with private;  --  private part has a vector of A's

    type I is interface;
    procedure AI (First : A'Class; Second : I) is abstract;

    type I2 is new I with null record;
    --  procedure AI (First : A'Class; Second : I2);
    procedure AI (First : B'Class; Second : I2);
    procedure AI (First : C'Class; Second : I2);

    -- from the implementation
    procedure AI (First : A'Class; Second : I2) is
    begin
        IO.Put_Line ("AI with A and I2");
    end AI;

    procedure AI (First : B'Class; Second : I2) is
    begin
        IO.Put_Line ("AI with B and I2");
    end AI;

    procedure AI (First : C'Class; Second : I2) is
    begin
        IO.Put_Line ("AI with C and I2");
    end AI;

A program with this fails to compile with this message:

my_interfaces.adb:6:10: error: ambiguous call to "Ai"
my_interfaces.adb:6:10: error: possible interpretation at my_interface.ads:16
my_interfaces.adb:6:10: error: possible interpretation (inherited) at my_interface.ads:13
my_interface.ads:13:10: error: type must be declared abstract or "Ai" overridden
my_interface.ads:13:10: error: "Ai" has been inherited from subprogram at line 11
  1. The error on line 13 goes away if I implement procedure AI (A'Class; I). I don’t understand why it wants that.
  2. The error on line 6 doesn’t go away no matter what I do, and I don’t understand what’s ambiguous. It’s referring to the abstract procedure (which it shouldn’t even attempt to use…?) and to the actual implementation.

Thanks for any help you can give!

Added later: I just closed something like 20 tabs that I had open last night, trying to make sense of this. So if it turns out to be something truly simple I’ll be :man_facepalming: myself for weeks.

1 Like

From your pasted example, you don’t have them in a package which prevents making operations primitive in some cases (being primitive is required for inheritance). The following example compiles on the jdoodle Ada compiler:

with Ada.Text_IO; use Ada.Text_IO;

procedure jdoodle is

    package IO renames Ada.Text_IO;

    package Inner is
        --  from the interface
        type A is abstract tagged null record;
    
        type B is new A with private;  --  private part has a String
        type C is new A with private;  --  private part has a vector of A's
    
        type I is interface;
        procedure AI (First : A'Class; Second : I) is abstract;
    
        type I2 is new I with null record;
        procedure AI (First : A'Class; Second : I2);
        procedure AI (First : B'Class; Second : I2);
        procedure AI (First : C'Class; Second : I2);
        
    private
    
        type B is new A with null record;
        
        type C is new A with null record;
        
    end Inner;
    
    package body Inner is

        -- from the implementation
        procedure AI (First : A'Class; Second : I2) is
        begin
            IO.Put_Line ("AI with A and I2");
        end AI;
    
        procedure AI (First : B'Class; Second : I2) is
        begin
            IO.Put_Line ("AI with B and I2");
        end AI;
    
        procedure AI (First : C'Class; Second : I2) is
        begin
            IO.Put_Line ("AI with C and I2");
        end AI;
        
    end Inner;
begin
    null;
end jdoodle;

Sure, I had something like that, and it compiles for me, too. Rereading the error and the code, I see that I didn’t supply enough information, or possibly wrong information. (Not-so-minimal example! :grin:) Let’s add a few things.

To the package interface, add

    function Get_T2 return I2;

    function Initialize return C;

To the package body, add

    function Initialize return C is
        Result : C;
    begin
        return Result;
    end Initialize;

    function Get_T2 return I2 is (T2);

To the main procedure body, add a variable Stuff: My_Interface.C := My_Interface.Initialize; and change null; to

Stuff.AI (My_Interface.Get_T2);

This is where I get the error. How about you?

(If you still don’t get it, I can post more of what I have, which is perhaps what I should have done to start with.)

I don’t have T2, is that meant to be I2 or something else?

If I guess that T2 is I2, then I have this example:

with Ada.Text_IO; use Ada.Text_IO;

procedure jdoodle is

    package IO renames Ada.Text_IO;

    package My_Interface is
        --  from the interface
        type A is abstract tagged null record;
    
        type B is new A with private;  --  private part has a String
        type C is new A with private;  --  private part has a vector of A's
    
        type I is interface;
        procedure AI (First : A'Class; Second : I) is abstract;
    
        type I2 is new I with null record;
        procedure AI (First : A'Class; Second : I2);
        procedure AI (First : B'Class; Second : I2);
        procedure AI (First : C'Class; Second : I2);
        
        function Get_T2 return I2;

        function Initialize return C;
        
    private
    
        type B is new A with null record;
        
        type C is new A with null record;
        
    end My_Interface;
    
    package body My_Interface is

        -- from the implementation
        procedure AI (First : A'Class; Second : I2) is
        begin
            IO.Put_Line ("AI with A and I2");
        end AI;
    
        procedure AI (First : B'Class; Second : I2) is
        begin
            IO.Put_Line ("AI with B and I2");
        end AI;
    
        procedure AI (First : C'Class; Second : I2) is
        begin
            IO.Put_Line ("AI with C and I2");
        end AI;
        
        function Initialize return C is
            Result : C;
        begin
            return Result;
        end Initialize;
    
        function Get_T2 return I2 is (I with null record);
        
    end My_Interface;
    
    Stuff: My_Interface.C := My_Interface.Initialize; 
    
begin
    Stuff.AI (My_Interface.Get_T2);
end jdoodle;

That gives the ambiguous error and I see why. You have two functions named AI which take both A’Class and C’Class. The problem is C’Class is always A’Class as well, so you will always be ambiguous (which of the 2 calls does the compiler pick?). There are two ways to fix this. Either name the functions different or put them in different packages and use Package_Name.AI(Stuff, My_Interface.Get_T2).

I tend to favor the different package approach since types A and C would normally be declared in separate packages (not always, but it’s most common in my experience).

Here is a reworked example using different packages that compiles (barring the T2 thing of course):

with Ada.Text_IO; use Ada.Text_IO;

procedure jdoodle is

    package IO renames Ada.Text_IO;

    package My_Interface is
        --  from the interface
        type A is abstract tagged null record;
    
        type B is new A with private;  --  private part has a String
        
        type I is interface;
        procedure AI (First : A'Class; Second : I) is abstract;
    
        type I2 is new I with null record;
        procedure AI (First : A'Class; Second : I2);
        procedure AI (First : B'Class; Second : I2);
        
        function Get_T2 return I2;

    private
    
        type B is new A with null record;
        
    end My_Interface;
    
    package My_C is
        use My_Interface;
        
        type C is new A with private;  --  private part has a vector of A's
        
        procedure AI (First : C'Class; Second : I2);
        function Initialize return C;
        
    private
        type C is new A with null record;
    end My_C;
    
    package body My_C is
        procedure AI (First : C'Class; Second : I2) is
        begin
            IO.Put_Line ("AI with C and I2");
        end AI;
        
        function Initialize return C is
            Result : C;
        begin
            return Result;
        end Initialize;
    end My_C;
    
    package body My_Interface is

        -- from the implementation
        procedure AI (First : A'Class; Second : I2) is
        begin
            IO.Put_Line ("AI with A and I2");
        end AI;
    
        procedure AI (First : B'Class; Second : I2) is
        begin
            IO.Put_Line ("AI with B and I2");
        end AI;
        
        function Get_T2 return I2 is (I with null record);
        
    end My_Interface;
    
    Stuff: My_C.C := My_C.Initialize; 
    
begin
    My_C.AI(Stuff, My_Interface.Get_T2);
end jdoodle;

Yes, T2 is an I2. Sorry, I thought I’d copied that.

I’m still not sure I understand the ambiguity. A is abstract. I understand that C'Class can also be A'Class, but given the choice between the two, I’d’a thunk the resolution would be… uh, “obvious”. :grin: But maybe that’s my impression from other OO languages.

I’ll give your approach a spin later and see what happens. Thanks for the patience.

Hi! I have to give three apologies here.

  1. A couple of things got in the way of my pursuing this.
  2. It still ain’t workin’.
  3. This will be long.

I’ve looked this over several times, but I’m not sure what I’m doing wrong. The only meaningful difference I see is that I have separated things into separate files, whereas you have them all in one. I’ll copy the contents below.

I’ve filled in a little of the types, but I don’t think they are the problem.

First, My_Interface specification:

with Ada.Containers.Indefinite_Vectors;

package My_Interface is

    type A is abstract tagged null record;

    type B is new A with private;

    type I is interface;
    procedure AI (First : A'Class; Second : I) is abstract;

    type I2 is new I with null record;
    procedure AI (First : A'Class; Second : I2);
    procedure AI (First : B'Class; Second : I2);

    package Container is new Ada.Containers.Indefinite_Vectors
       (Index_Type => Positive, Element_Type => My_Interface.A'Class);

    function Get_T2 return I2;

private

    type B is new A with record
        Name : String (1 .. 1);
    end record;

end My_Interface;

Its implementation:

pragma Ada_2022;

with Ada.Text_IO;

package body My_Interface is

    package IO renames Ada.Text_IO;

    procedure AI (First : A'Class; Second : I2) is
    begin
        IO.Put_Line ("AI with A and I2");
    end AI;

    procedure AI (First : B'Class; Second : I2) is
    begin
        IO.Put_Line ("AI with B and I2");
    end AI;

    function Get_T2 return I2 is (T2);

end My_Interface;

Next, My_Second_Interface specification (you called this My_C):

with My_Interface;
with Ada.Containers.Indefinite_Vectors;

package My_Second_Interface is

    type C is new My_Interface.A with private;

    procedure AI (First : C'Class; Second : My_Interface.I2);

    function Initialize return C;

    function Get_T2 return My_Interface.I2;

private

    type C is new My_Interface.A with record
        Elements : My_Interface.Container.Vector;
    end record;

end My_Second_Interface;

Its implementation:

with My_Interface;

package body My_Second_Interface is

    function Initialize return C is
        Result    : C;
        Extension : C;
    begin
        Result.Elements.Append (B'(Name => "1"));
        Result.Elements.Append (B'(Name => "2"));
        Result.Elements.Append (B'(Name => "3"));
        Result.Elements.Append (C);
        return Result;
    end Initialize;

    procedure AI (First : C'Class; Second : My_Interface.I2) is
    begin
        IO.Put_Line ("AI with C and I2");
    end AI;

    T2 : I2;

    function Get_T2 return My_Interface.I2 is (T2);

end My_Second_Interface;

Finally, the main program:

with My_Second_Interface;

procedure My_Interfaces is
    Stuff : My_Second_Interface.C := My_Second_Interface.Initialize;

begin
    Stuff.AI (My_Second_Interface.Get_T2);
end My_Interfaces;

This still gives me the error:

my_interfaces.adb:7:10: error: ambiguous call to "AI"
my_interfaces.adb:7:10: error: possible interpretation at my_second_interface.ads:8
my_interfaces.adb:7:10: error: possible interpretation at my_interface.ads:13

I haven’t gone through the whole example yet (it’s late tonight), but the first thing that looks different is you are using “Object.Operation” notation where I was using “Package.Operation(Object)” notation so Ada would know the exact function I intended.

I don’t know if this matters for your full example yet, but that is my first thought without much time spent on it.

Something like (untested):

My_Second_Interface.AI(Stuff, My_Second_Interface.Get_T2);

At first glance, using Object.Operation notation would still be considered ambiguious as both A’Class and C’Class versions of the AI function are technically primitive to the object Stuff.

Don’t bother. You hit the nail on the head. Besides the rest of the program has a lot of trash whose problems became apparent after fixing that.

Now that it’s compiling, though, something else confounds me: Correction: I think I got it. I’ll cover up the mistaken parts in case you want to look at it, but I think I had misunderstood how dynamic dispatch works. I’ll likely have a follow-up question tomorrow.

I actually want dynamic dispatch. As I understand it, this should be straightforward: just add overriding to the declaration of AI in My_Second_Interface. But that doesn’t do it; the compiler insists it’s not overriding.

Here are the specifications, if it helps. I also tried changing the signature to I2'Class from I2, but that didn’t help, either.

with Ada.Containers.Indefinite_Vectors;

package My_Interface is

   type A is abstract tagged null record;

   type B is new A with private;

   type I is interface;
   procedure AI (First : A'Class; Second : I) is abstract;

   type I2 is new I with null record;
   procedure AI (First : A'Class; Second : I2);
   procedure AI (First : B'Class; Second : I2);

   package Container is new Ada.Containers.Indefinite_Vectors
     (Index_Type => Positive, Element_Type => My_Interface.A'Class);

   function Get_T2 return I2;

   function Initialize (Value : Character) return B;

private

   type B is new A with record
      Name : Character;
   end record;

   T2 : My_Interface.I2;

end My_Interface;

with My_Interface;

package My_Second_Interface is

   type A is new My_Interface.A with private;

   overriding procedure AI (First : A'Class; Second : My_Interface.I2);

   function Initialize return A;

private

   type A is new My_Interface.A with record
      Elements : My_Interface.Container.Vector;
   end record;

end My_Second_Interface;

Yep, so this can confuse a lot of folks, but when you do something like A’Class as a parameter you are saying the operation already works for all descendants of A (and inside your operation all usages of “First” will use dynamic dispatch internally), so you cannot override it for descendants of A anymore, it is a universal function to that base class and descendants (my terminology, not Ada terminology). Your procedure AI, is only overridable on the parameter “Second” as that is not 'Class. If you want it overrideble on First, then you need to switch the 'Class part between the two parameters:

package My_Interface is

   type A is abstract tagged null record;
   procedure AI (First : A; Second : I2'Class);

and then you can override it:

package My_Second_Interface is

   type A is new My_Interface.A with private;
   overriding procedure AI (First : A; Second : My_Interface.I2'Class);

As an aside, if you have a procedure:

procedure Do_Something(Thing : A) is
begin
   Thing.Primitive_Operation;
end Do_Something;

This example will not dispatch on Primitive_Operation since thing is of type A, but Do_Something is overridable (since the parameter is not 'Class).

However if you have:

procedure Do_Something(Thing : A'Class) is
begin
   Thing.Primitive_Operation;
end Do_Something;

This example will dispatch on Primitive_Operation since it is of type A’Class, but Do_Something is no longer overridable (since the parameter is 'Class).

It’s very different from Java or C++ where calling an operation is automatically dispatching (except for in constructors). In Ada you tell it when to dispatch manually.

If you want to force the first example to be overridable AND dispatch internally then you do:

procedure Do_Something(Thing : A) is
begin
   A'Class(Thing).Primitive_Operation;
end Do_Something;

This works much closer to how it works in C++ or Java

1 Like

Thanks. The main point of this was that I was trying to implement the visitor pattern in Ada using tagged types. I finally made something work and added it to the relevant page on Rosetta Code.

Even with your help, eventually I had to resort to an old Ada Gem for the visitor pattern. An issue I kept stumbling over was that Ada only allows one argument to a function to be dispatching, which I take to mean, “overridable”. I don’t understand why that’s the case; do you? No searching I did gave me an answer that made sense. (e.g., one answer suggested it might be due to single inheritance, but that doesn’t make sense, since Java is single-inheritance, and as I recall it doesn’t have that limitation.

Do I misunderstand what “dispatching” means? That was also my impression from reading what you wrote.

Another question: My impression from what you write is that I2'Class is a way of saying what Java (and some other languages IIRC) call final: i.e., you can’t override it. Can you confirm or correct that?

Thanks again for the help.

So this is one of the reasons I recommend doing each tagged type in their own package, it helps avoid this problem (Though for the visitor pattern specifically you may not be able to break them up due to how it is often implemented). Basically, any operation within a package is potentially “primitive” for the types of its parameters if they are defined in that same package of the operation. Any operation that is primitive for a tagged type is potentially dispatching. The problem occurs when more than one parameter in the operation is potentially dispatching. The compiler doesn’t know which parameter to dispatch on because multiple options exist and for compiler writers, allowing multi dispatch is apparently very very difficult to actually implement (basing this off of comments I once read in ARG minutes and AI discussions).

So the answer is the ARG felt that it was way too difficult to implement multi dispatch in general and only allows single dispatch (like Java). That means if you have multiple tagged types in a package together you either have to avoid using more than one as a parameter or return type for an operation or you have to mark the extra ones as 'Class so that the operation cannot be potentially overridable for more than one parameter.

There is some overlap in the side effects of both for sure, but they are different in full function. Final just prevents you from overriding an operation that was already overriden so that only previous descendants were able to override it while new descendants cannot override it (past that point). Using 'Class on a parameter makes that operation universal to all descendants and it was never overridable to begin with (unlike “final” which makes a prevously overridable operation no longer overridable.

1 Like

Reading up on dispatching I see that I seem to have been a little fuzzier on the term than I thought. Wikipedia’s page on multiple dispatch made a good point that it’s a bit like multiple inheritance: ambiguities can arise on how to resolve a call, and the language designers have to decide whether to make a guess based on some heuristic or to make it an error – which can become a run-time error, so…

The discussion on final vs. 'Class was helpful, thanks. Not sure I’ll remember it :grinning: but it does help.

I saw your reply in the other thread but haven’t looked at it yet (it’s late). In the meantime, I worked out some more changes to my implementation; I’ll try to follow up here.

I looked over your solution as well as mine, and neither seems to get around the thing that really bothers me: the Visitor isn’t sufficiently modular. It has to know everything it needs to visit.

It seems to me that a better approach would be to have a very minimal Visitors package:

--  not-quite-boilerplate omitted
package Visitors is

   type Visitor is abstract tagged null record;

   procedure Receive (Self : Base.Base_Record; Whom : Visitor'Class) is null;

   procedure Visit_Base
     (Self : Visitors.Visitor; Dest : Base_Record'Class) is null;

   type Perform is new Visitor with null record;
   type Print is new Visitor with null record;

end Visitors;

…and that’s it. Then you define a Bodies package as before, and either declare how it implements the Visitor class functions there, or else in a separate Body_Visitors package. Otherwise, the Visitors class seems to be nothing more than a charming curiosity, useful only in the most limited of circumstances.

I tried looking at that, but can’t figure out how to make it work. At first I tried to move the following procedures to the Bodies package:

   procedure Visit_Body
     (Self : Visitor; Dest : Bodies.Body_Record'Class) is null;

   overriding procedure Visit_Body
     (Self : Perform; Dest : Bodies.Body_Record'Class);
   overriding procedure Visit_Body
     (Self : Print; Dest : Bodies.Body_Record'Class);

…but the compiler informed me that the later two Visit_Bodys were no longer overriding.

:question: Is that because they’re no longer overridable, because they don’t fall into the category of [ARG 8.3] “implicit declarations for predefined operators and inherited primitive subprograms”? (emphasis added)

Once you leave the package spec for the type (or use it in certain ways, such as supply it to some types of generics as a type parameter) you can no longer add primitive operations to the type, which means you can no longer override operations for that type. Basically the type “freezes” because the compiler needs to know everything about the type at that point (size, alignment, list of operations, etc). This is to allow you to change the package body (implementation of a type) without forcing users of the package spec to recompile unnecessarily.

Note that the Visitor interface doesn’t need to know anything about what it is visiting besides the base class and it needs to know nothing about that specifically other that it exists and is a class type. The implementers of the Visitor interface do need to know stuff about who they are visiting though. You can see this in my version where the implementers are completely separate from the Visitor interface and the Car_Element class.

My understanding of the visitor pattern is that the intent is that the object being visited doesn’t know who is visiting it, but the implementers of the visitor interface needs to know who they are visiting or at least what the operations of the visitor interface provide functionally. I’m not sure you can escape this coupling due to how the pattern works.

I think the main difference between our two approaches is that for yours, the implementers of the car element class know who their visitors are (visit_body(), etc.). In my implementation, I push all of that knowledge solely on the implementers of the visitor interface, creating a one way abstraction (the “visited” don’t know who visit them but the “visitors” know who they visit). — Hopefully that makes sense. I am particularly bad at putting my thoughts in writing, so I apologize if not.

There is a pattern that uses an interface type on both sides, but it isn’t observer/visitor pattern. It might provide more of what you want. You might look into the “Bridge Pattern”: Bridge pattern - Wikipedia (I’m not 100% sure this is what you are looking for… just from a cursory glance).

Thanks! Your reply made me revisit the page on the visitor pattern, and I see that I had forgotten some crucial things; in particular,

the classes that make up the object structure are known and not expected to change

(er… assuming I ever really noticed/thought of that)

In my implementation, I push all of that knowledge solely on the implementers of the visitor interface, creating a one way abstraction (the “visited” don’t know who visit them but the “visitors” know who they visit). … I am particularly bad at putting my thoughts in writing, so I apologize if not.

Nope, I had noticed that, and had wondered about it, and that definitely explains it.

Studying your code more closely (finally – it’s the weekend! :-)), I realized it achieves better the sorts of things I wanted, so I adapted it somewhat. I’ll update the Rosetta Code page later, but if you’d like to see what I have, I’ve been keeping my Rosetta Code contributions on GitHub. The readme links to your post above :grin:

Glad to help! This stuff is always fun to explore and I learn a lot in the process.

Side note, I know my weird use of the Make functions might be a bit to juggle (with the type conversions, the underlying dummy type). An alternate method is to have a classwide set name operation on the abstract base class and then the Make functions for each of the descendents do an extended return creating the object and setting the name before returning it.

I went with the more obtuse method because it saved on having to make .adb files for the engine and body packages. If you want to explore a simpler / easier to read method, here’s a first stab at it. It might be better for the rosetta code example (and it showcases extended return as a bonus).

Something like: Untested/uncompiled alternate method

vehicle_elements.ads

   -- To set the name
   procedure Name(Self : in out Element'Class; Value : String);

vehicle_elements.adb

   procedure Name(Self : in out Element'Class; Value : String) is
   begin
      Self.Name := To_Unbounded_String(Value);
   end Name;

engines.adb (doesn’t exist yet)

   function Make return Engine is
   begin
      return Result : Engine do
         Engine.Name("Engine");
      end return;
   end Make;

I’m sure there are other ways too, this was just off the top of my head.