Can't understand ada's oop model

Hi,
In the adacraft book they use the calculator/syntax parser exemple to illustrate the full power of dispatching. But I can’t follow what’s happening.
In the exemple below, both Expression_type and Token_type (behind Token_pointer) are tagged type.
I am asked why Syntax is not converted to a class-wide type when calling Parse recursively. Naively I would suspect that everything happens as if the procedure was copied and pasted but with syntax’s type changing to whatever derived type. So that syntax.Parse would call the right Parse statically. Am I right ?
And converting to the class-wide type would force a dynamic dispatching, with the same result but for no reason.

procedure Parse (Syntax : in Expression_Type;
                 Expr  : in String;
                 From  : in out Positive;
                 Prio  : in Priority_Type;
                 Result   : out Integer;
                 Next  : in out Token_Pointer) is
begin
    if Prio = Priority_Type'First then
        Get_Operand (Expression_Type'Class(Syntax),
                     Expr, From, Result, Next);
    else
        declare
            Right : Integer;
            Op    : Token_Pointer;
        begin
            Parse (Syntax, Expr, From, Prio-1, Result, Op);
            while Value(Op).all in Binary_Operator_Type'Class and then
                Priority(Binary_Operator_Type'Class(Value(Op).all)) = Prio
            loop
                Parse (Syntax, Expr, From, Prio-1, Right, Next);
                Result := Apply (Binary_Operator_Type'Class(Value(Op).all),
                                 Result, Right);
                Op := Next;
            end loop;
            Next := Op;
        end;
    end if;
end Parse;

For more code, see here

Here’s three papers explaining Ada’s features, the second one explains Ada’s OOP — I recommend reading the package paper first, brushing up on that.

  1. Explaining Ada’s Packages

  2. Explaining Ada’s Object Oriented Programming

  3. Explaining Ada’s Generics

Hope that helps.

1 Like

I read these once before, but somehow didn’t think of an objection to your explanation of inheritance in 2 that I think of only now. In particular,

Inheritance — Can be done via type derivation. Type Miles is new Natural; — Here Miles inherits Natural’s attributes, operations, and representation.

Then, later:

In Ada there is a distinction between “Some_Type” and “Some_Type, or anything derived therefrom” —this is the distinction between a specific thing, and a set of that sort of thing— and this distinction is realized by 'Class like so: Procedure Paint(Object : Some_Type) and Procedure Paint(Object : Some_Type'Class) — with the difference being with the former’s parameter referring specifically to Some_Type, whereas the latter’s refers to “Some_Type, or anything derived from Some_Type.”

In particular, the phrase “and anything derived therefrom,” which is emphasized in the original, implies that the following code makes for legal Ada

procedure Test_Class_From_New_Type is

   type Alternate is new Integer;

   function Perhaps (X : Integer'Class) return Integer is (X + 1);

   A : Alternate := Perhaps (0);

begin
   null;
end Test_Class_From_New_Type;

…but it is not, and the compiler rejects it accordingly:

test_class_from_new_type.adb:5:26: error: tagged type required, found type "Standard.Integer"

I don’t have a problem with this in general, but it does strike me as a minor flaw in the exposition.

The error message is saying that this (the 'Class attribute) only works with “tagged types” —the Ada LRM formal-name for OOP-types— which Integer is not a member of.

I suppose I could see about updating the paper, but I have a few other, more-pressign tasks to attend.

In the following exemple, what is the advantage of Syntax.Get_Operand over Expression_type'Class (Syntax).Get_Operand ? The text says it wouldn’t dispatch if I derive a new type from Expression_type, but I can’t make that work so I can’t test.
If New_expression_type is derived, and it inherits Parse and Get_Operand, and I call evaluate, and Parse is called inside evaluate, won’t the call to get_operand inside the latter also dispatch to the new primitive ?

procedure Parse (Syntax : in Expression_Type;
                  Expr  : in String;
                  From  : in out Positive;
                  Prio  : in Priority_Type;
                  Result   : out Integer;
                  Next  : in out Token_Pointer) is
begin
    if Prio = Priority_Type'First then
        Syntax.Get_Operand (Expr, From, Result, Next);
    else null; end if;
end Parse;

Sure, I understand that. Do you understand what I’m getting at, though?

You’re getting at that I forgot to distinguish types into the “tagged” and “untagged” sets.

Syntax.Get_Operand does not dispatch when it makes the Get_Operand call. It calls the Expression_Type’s version of the function.specifically. Using Expression_type'Class (Syntax).Get_Operand instead will do a dispatching call and call the version of Get_Operand based on the underlying object’s type, which might be different if the underlying type is a derived child with an overriding version of the function. The first method can call the operation statically for sure. The second method may have to use the dispatch table (or whatever method the compiler uses for overriding operations) at run time to figure out which version of the function to call if it can’t reason that the call is the same statically.

I don’t understand.
If Parse is inherited, then the Get_Operand called from within Parse should take a New_Expression_type as an argument, and not its ancestor type.
But I’ll try and digest your documentation first, and reread the book. Often I need a day to grasp a new concept.

My recommendation:

  1. Simplify Parse to just the object parameter (Temporarily for testing below):
  2. Make a base class with a Parse procedure that just prints to screen: “Base_Class.Parse”.
  3. Make multiple derived children that override Parse and print things like “Child_1.Parse” and “Child_2.Parse”
  4. Make at least one derived child that doesn’t override Parse at all.
  5. Setup a bunch of situations you are curious about and call Parse in them and see which prints. Make sure to try various combinations that both include 'Class and exclude it.

That’s how I learned.

1 Like

If you override Parse for New_Expression_Type then it won’t call the above version you specified at all, it will call the new version you overrode it with (so the internals of the procedure you posted won’t matter). If you don’t override Parse for New_Expression_Type then it will call the same version as above with Expression_Type replaced with New_Expression_Type.

Note: there are ways for you to call the ancestor version of the operation inside your new override version though if you need it to happen.

I found the issue in my code, and it wasn’t related to 'Class etc… I just forgot to increment the index in the string read, but since the error was raised in another subprogram, I had trouble understanding where it really came from. That’s what I like the least with recursion… tracking errors.
Right, We always much more easily like this by exemple. Until I understand everything in this program, because I’m mostly tinkering with what works for now.

Glad you found the answer! This convo spawned the motivation to set up some examples of how dispatching works in various scenarios. I know you solved your issue but was curious if you would take a look at this example setup and see if it helps understanding at all. I tried to put in a lot of various scenarios for dispatching (and not dispatching).

You can use gprbuild or alr run or gnatmake main.adb or you can just copy main.adb, classes.ads, and classes.adb and run them however you like.

1 Like

I just did. I like it a lot, except that the only case which wasn’t obvious to me, was lacking in it. I mean when the following is not overridden:

procedure Print_As_Latest_Descendent_without_dispatching(Self : Great_Grand_Parent) is
begin
   Self.Print;
end Print_As_Latest_Descendent_without_dispatching;

its output is always identical to

procedure Print_As_Great_Grand_Parent(Self : Descendant_type) is
begin
   Great_Grand_Parent(Self).Print;
end Print_As_Great_Grand_Parent;

It it this behavior that is completely unintuitive to me. I assumed that Self.Print always called on self’s Print, so that when written like this, it would always be dispatching, but I was wrong… when the parameter is Self: My_type every calls to Self.whatever_primitive inside the subprogram is actually “replaced” by the compiler with My_type(Self).whatever_primitive. You only get dispatching when asking for it explicitely, regardless of the prefixing.

Also you should make great_grand_parent abstract. You can’t declare variable of an abstract type, yet you can use a view conversion to call on an abstract ancestor’s primitive: that wasn’t immediately obvious to me.

I can add a case to it for that. I’ll add a function to the Great_Grand_Parent called Print_without_Dispatching and call that (not overriden) for each. I’ll fix it up later tonight. I can had an astract base as well.

Done, somewhat: https://pastebin.pl/view/bf8e0ee2