Confusion about abstract types, Function of an abstract result type must be abstract

Hi, I’m learning about the finer details of OOP, dynamic dispatching and all that jazz. Oh my. There’s a rule whose justification is in the AARM, but it still doesn’t quite cut it for me.

If the result type of a function is abstract, then the function shall be abstract. If a function has an access result type designating an abstract type, then the function shall be abstract.
Reason: This ensures that values of an abstract type cannot be created, which ensures that a dispatching call to an abstract subprogram will not try to execute the nonexistent body.

This means I can inherit a constructor from an abstract type without making it a procedure. It seems to limit severly the use of returning results. But why, can someone show me a case where it would cause problem?
Because the compiler knows which type is abstract, so at the times of dispatching, it knows not to call on an abstract procedure’s body but instead dispatch. I don’t see that subprogram being a function changes things. It should not be possible to return a value with that tag anyway.

I think I found where I was mistaken, but it leaves me even more puzzled. I need an explanation for the following :

package test is
	type Appointment_Type is
	abstract tagged record
	Time : Integer;
	Details : String (1..50);
	Length : Natural := 0;
	end record;
	procedure Put (Appt : in Appointment_Type) is abstract;
	type newtype is new Appointment_Type with null record;
	overriding procedure Put (Appt : in newtype);
private
end test;

package body test is
	overriding procedure Put (Appt : in newtype) is
		essai: appointment_type'class := Appointment_type(newtype'(Appointment_type with null record));
		essai2: appointment_type'class := Appointment_type'(others => <>);
	begin
		return;
	end Put;
end test;

How come Essai is fine but not Essai2 ?! If I understand well the second has a statically determined tag of an abstract type so it would make no sense. But the first should be the same, it just appears to be a view conversion.

… oh wait, except a view conversion is also a name so can a target for assignement / in out parameter. But this

Appointment_type(essai) := (5,“a”,0);

is still illegal, an aggregate can’t be of an abstract type. So we can’t update an object only on its abstract parent’s part with the above as we would if the parent was not abstract ? This seems confusing and limited… It seems forbidding a variable to be of an abstract type should suffice. Why forbid aggregates too?

I’m not Ada language lawyer, but my interpretation:

I believe Essai constructs an object of type newtype which is not abstract. It does a view conversion but not an object conversion. Essai holds an object of type newtype. Essai2 tries to actually construct an abstract object. Essai2 tries to hold an object of type Appointment_Type which is not allowed.

1 Like

You’re right, I checked, the tag is that of newtype.
So conversion with tagged type when assignating to a class-wide object preserves the tag, I assume it’s only useful for dispatching to a particular type’s primitive. And normal conversion only happens from a class-wide object/value to a target of a normal descendant type.
Makes sense.

Still I wish functions returning a value of an abstract type could be used to fill up the ancestor part of an extension aggregate, instead of using a procedure !

There are some syntax sugars you can use to make it a bit easier:

  1. Use an extended return statement when creating constructing functions for your child class objects and call your constructing procedure inside the extended return (it would need to be a classwide procedure).
  2. Use a default constructed object that inherits off of the abstract class for child construction.

Not saying they are always pleasant options, but things you can consider if it makes something else better.

1 Like

What do you mean ? I can’t declare a constant.

You create a special derived type (like your newtype) that is not abstract (so it is construct-able) as a utility type for your later derived types. You can make a constructing function for that type to help make the later derived types

See below:

with Ada.Text_IO; use Ada.Text_IO;

procedure jdoodle is
    package Test is
    
        subtype Details_String is String(1..50);
    
    	type Appointment_Type is abstract tagged record
        	Time    : Integer;
        	Details : Details_String;
        	Length  : Natural := 0;
    	end record;
    	
    	procedure Put (Appt : in Appointment_Type) is abstract;
    	function Make
    	    (Time    : Integer;
    	     Details : Details_String;
    	     Length  : Natural := 0)
    	     return Appointment_Type is abstract;
    	
    	
    	------------------------------
    	-- Utility constructing type
    	------------------------------
    	type Constructable is new Appointment_Type with null record;
    	overriding procedure Put(Appt : in Constructable);
    	
    	overriding function Make
    	    (Time    : Integer;
    	     Details : Details_String;
    	     Length  : Natural := 0) 
    	     return Constructable;
    	
    end Test;
    
    package body Test is
    
        procedure Put(Appt : in Constructable) is
        begin
            Put_Line("Time    =>" & Appt.Time'Image);
            Put_Line("Details => " & Appt.Details(1..Appt.Length));
        end Put;
        
        function Make
    	    (Time    : Integer;
    	     Details : Details_String;
    	     Length  : Natural := 0) 
    	     return Constructable 
        is begin
            return Result : Constructable do
                Result.Time    := Time;
                Result.Details := Details;
                Result.Length  := Length;
            end return;
        end Make;
        
    end Test;
    
    package Test_Child is
        use Test;
        
        -- Notice here it is derived off of appointment_type
        -- publicly.  See private section
        type Child is new Appointment_Type with private;
        
        overriding procedure Put(Appt : Child);
        
        overriding function Make
    	    (Time    : Integer;
    	     Details : Details_String;
    	     Length  : Natural := 0) 
    	     return Child;
    	function Make
    	    (Value   : Integer;
    	     Time    : Integer;
    	     Details : Details_String;
    	     Length  : Natural := 0) 
    	     return Child;
        
    private
    
        -- Here we inherit off of constructable so that we
        -- can more easily make constructors for the derived
        -- types
        type Child is new Constructable with record
            Value : Integer := 100;
        end record;
        
        function Make
    	    (Time    : Integer;
    	     Details : Details_String;
    	     Length  : Natural := 0) 
    	     return Child
    	is (Constructable'(Make(Time, Details, Length)) 
    	        with Value => <>);
        
        function Make
    	    (Value   : Integer;
    	     Time    : Integer;
    	     Details : Details_String;
    	     Length  : Natural := 0) 
    	     return Child
    	is (Constructable'(Make(Time, Details, Length)) 
    	        with Value => Value);
    	        
    end Test_Child;
    
    package body Test_Child is
        procedure Put(Appt : Child) is
        begin
            Put_Line("Value =>" & Appt.Value'Image);
            Constructable(Appt).Put;
        end Put;
    end Test_Child;
    
    Child : Test_Child.Child := Test_Child.Make
        (Value   => 125,
         Time    => 123456789,
         Details => (1 => 'a', others => ' '),
         Length  => 1);
begin
    Child.Put;
end jdoodle;

Results:

Value => 125
Time    => 123456789
Details => a

gcc -c jdoodle.adb
gnatbind -x jdoodle.ali
gnatlink jdoodle.ali -o jdoodle

The default constructed part is was specifically:

            return Result : Constructable do
                Result.Time    := Time;
                Result.Details := Details;
                Result.Length  := Length;
            end return;

Sure, it does works. But then what’s the point of having an abstract type at all then, except reminding yourself to override a number of subprograms ? Seems redundant !

So that you can design your software generically. You can have your software work with Appointment_Type'Class objects so that you can use any object type derived from it. Plus not all types need to use the utilty constructable type. It’s just there for basic cases to have code reuse for those. More complex cases might have to do something else.

For example, I made an abstract message class once so that I could pass “any” message to my software and process generically. My default derived class returned an empty payload (a null record) with a payload length in the header set to 0, which was useful for like 60% of the messages I had to implement (they had no payload). The other 40% I derived differently based on their payloads, so they couldn’t leverage the default derived class in full.

I’m not saying it is a must do kinda thing. It’s just options. Don’t use them if you think they stink for sure.