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.
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:
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).
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.
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.