Learning Ada 5: object oriented paradigm

in #ada-lang6 years ago

Ada is born as an imperative procedural language in the era of imperative procedural languages. With Ada 95 it entered the club of programming languages with object orientation features.

hierarchy.png

Package ~= class

What's a class in a language like Java? Basically it is both a container of data and code operating on those data — maybe you know it by the name of methods — plus a definition of a type.

In Ada these are separated concepts. The scope is given by the package, which is the natural home for data, types' definitions, and subprograms.

The data of the object can be defined in a type which is a record. In Ada we can derive a new type from a “parent”, and this new type would inherit the parent's operations according to certain rules. But this isn't enough for dealing with run-time polymorphism and to control which code should be run when calling a “method” on an object which is part of a hierarchy.

To achieve this aim Ada has the tagged type (since Ada 95). That is, a tag is associated to the type and this tag contains informations useful to identify the type among all descendants of the same ancestor.

It is more or less a virtual table (or vtable) well known in the C++ language (the vtable is how C++ does part of its magic).

A tagged type can look like this:

   type Object is tagged
      record
         Counter : Natural;
         Id : Natural;
      end record;

Like a record, but with the keyword tagged.

This type definition inside a package would be a class without methods in other languages like C++ and Java. We can add “methods”, as well. In languages like C++ and Java, methods are “inside” the class — the class is the unit which encapsulates data and behaviours — and implicitly can see and manipulate the data of the instance.

Ada resembles languages like Python or Go. In these language the “variable” referencing the object itself is passed explicitly to “instance methods”, and there isn't the need for this or similar. Indeed in languages like Python you distinguish between class and instance methods because the former don't need the reference to the object — instead, C++ and Java use the “static semantic”. (Go is still a little bit different.)

Inside the package we can put subprograms to do things on the object. E.g.

   procedure Initialize (Obj : in out Object; P_Id : in Positive);

(I've discovered later that it is better not to use the Initialize name, because of the controlled types; but I'll explore them in the future.)

This procedure can be used to initialize the object. It isn't the exact analogue of a constructor in C++ (or other languages), though — one of the differences is that the constructor is called for you automatically when the instance is allocated. Nothing like that (by default) in Ada.

This must be put in a package, as said:

package My_Class is
   type Object is tagged
      record
         Counter : Natural;
         Id : Natural;
      end record;
   
   procedure Initialize (Obj : in out Object; P_Id : in Positive);
end My_Class;

And a possible body could be:

package body My_Class is
   procedure Initialize (Obj : in out Object; P_Id : in Positive) is
   begin
      Obj.Counter := P_Id * 2 + 1;
      Obj.Id := P_Id + 1;
   end Initialize;
end My_Class;

The type is publicly accessible to every client, so that it is something like this in Java:

class My_Class
{
    public int Counter;
    public int Id;

    public My_Class(int p_Id) {
        this.Counter = p_Id * 2 + 1;
        this.Id = p_Id + 1;
    }
};

Which isn't good because you can access and modify the data inside the object… Usually we don't want people to mess with our data, so we make them private and let the methods handle them.

In Ada we do this:

package My_Class is

   type Object is tagged private;

   procedure Initialize (Obj : in out Object; P_Id : in Positive);

private
   type Object is tagged
      record
         Counter : Natural;
         Id : Natural;
      end record;
end My_Class;

Now only the subprograms of the package can access the data of the object directly. This means that we need to give subprograms to read those data, for instance, or our “class” won't be very useful.

Do not copy me!

This operation:

   A := B;

is usually possible in Ada and automatically handled. But sometimes we don't want the use to be able to copy a variable like that. For these cases we can say that the type is limited. We can do it with tagged types, too:

   type Object is tagged limited private;

This means that the private type Object (details in the private part of the package) is “tagged” (has special informations attached to it) and it cannot be copied. That is,

   A, B : Object;
   
   -- ...
   
   A := B;

doesn't compile. Avoiding the Ada's automatic copying facility seems a good idea when you have an opaque private type; for example it could contain accesses — that is, pointers or references — which can't be copied like other types. So you want to give your Copy procedure and disallow :=.

There's no need a limited type to be also a tagged type.

Naming convention

In C++, Java, and others, the class name is also the type name and the enclosing unit containing data and methods (behaviour). The very same name is special because it defines a method which “builds” the instance.

In Ada there isn't anything of this, as already said: we've the package which gives a “scope”, it's the entity which separates our object type from the rest of the world.

There aren't mandatory names: the package could be named Zorro and the type could be La_Vega. It doesn't matter, except that we like to be consistent and express ideas in a clear way; thus we stick to a logical convention.

Actually there isn't just a convention because there are more than one reasonable choices.

This is one: the package has the name of the class, and the type is Object. If you do with My_Class; use My_Class, you can get trouble when you have more classes using this convention.

package My_Class is
   type Object is tagged private;
   -- ...
end My_Class;

Another option is to call the package with a plural name and the type with the singular.

package Animals is
   type Animal is tagged private;
   -- ...
end Animals;

Another convention, the one I like less, uses the same pattern we've seen in Ada, to add _Type to stress that we're dealing with a type.

package Animal is
   type Animal_Type is tagged private;
   -- ...
end Animal;

I think the Plural/Singular convention is the nicest.

Primitive operations and derivation

The primitive operations are those operations which are inherited when you create a derived type. The concept doesn't apply to tagged types only, and in fact we've already seen it at work in a previous article,

The new type inherits operations from the base type Integer, but still they are considered operations on Cows_Counter_Type, not on Integer.

You can find the exact definition in the glossary. For untagged type, primitive operations of a type

are the operations (such as subprograms) declared together with the type declaration. They are inherited by other types in the same class of types.

(The expression class of types has nothing to do with OOP, but it is a more general concept.)

In the case of a tagged type,

the primitive subprograms are dispatching subprograms, providing run-time polymorphism.

Ok, I have deduced a simpler rule. If a procedure has at least one parameter of the type, then the procedure is a primitive operation for that type. If a function returns a certain type, then the function is a primitive operation for that type. The same is true if we have accesses (references) instead of the type.

This maybe isn't accurate, but can give intuitively a quick idea of which operations are inherited for a derived type.

For the typical OO approach which tries to mimic OO in other common languages (C++, Java…), we can reduce the rule to just this:

  • An operation is a primitive operation for a tagged type if one of the parameters, usually the first, is of that type, or
  • in case of a function, if the function returns that tagged type.

When the type is the very first argument, we can benefit from the special syntax, which is just that, syntax, i.e. syntactic sugar.

A matter of syntax

Usually an Ada “method” is called like this:

   Method (X);

Where X is the “instance”, the object — that is, the variable of a tagged type. As said, in languages like C++ or Java this argument is implicit, so that this Ada “method” is like a parameterless C++/Java method, and we would write the call like this:

  x.method();

In more modern Ada (since 2005) we can use a similar syntax:

   X.Method; -- the same as Method (X);

The syntax seems clearer because this way we see at a glance that the method belongs to the instance X, otherwise it would look as a regular procedure (which, indeed, is!)

Humans are animals

In order to test a little bit of it, I've built a simple type which represents an animal and two features, the number of wings and the number of legs.

I have made this Animal type abstract, because I don't want clients to be fuzzy on a definition: when you see an horse, you don't see just an animal, you see exactly a horse! If you see an animal you don't know, yet it isn't just an animal because you don't know how to call it — you will invent your own name, then, and properties to describe that unique class of animals.

Abstract types can't be used of their own: you can only derive a new type from them.

The Animal type, even if abstract, has four primitive operations. When you derive your new specific animal type, those primitive operations are inherited.

At this point I did this:

with Animals;
package Humans is
   type Human is new Animals.Animal with private;
   -- ...
end Humans;

This seemed good, until I've tried to use Number_Of_Legs and Number_Of_Wings. I couldn't: the private part of Animals isn't accessible to Humans, even if Human is derived from Animal.

Which is exactly what happens in C++ if the members are private instead of protected.

But nowhere in Ada we say protected (private, but not for derived classes). All we have is public (not private) and private inside a package. How do we mimic protected?

By using child packages. Because child packages can see private members of parent packages.

package Animals.Humans is
   type Human is new Animals.Animal with private;
   -- ...
end Animals.Humans;

And then in one of our subprograms, let's say Make, we can refer to Numbers_Of_Legs and Number_of_Wings instead of using the public subprograms given by Animals to manipulate those members — thing that we can anyway choose to do.

The derived type Human overrides the subprogram Add_Wings which it inherited from Animal. The keyword overriding isn't mandatory but it expresses the real intention of the programmer, so that the compiler can check if the overriding is actually happening.

We can express also the opposite, writing not overriding instead of overriding.

Now that Add_Wings overrides the inherited Add_Wings from Animal, can we call the original subprogram? Yes we can: we convert Karl_Marx into an Animal, and then Add_Wings calls the Animal's version.

This isn't like C++. If we do something like dynamic_cast<Animal*>(a_human_ptr)->add_wings(n), we are still calling the “right” version of add_wings, the one of Human, because the object is still a human, after all… (We can mimic Ada's behaviour in C++, of course, but not through a cast.)

This looks terrible.

Let's suppose I have a procedure which must accept any kind of animals, humans as well as, say, flying horses:

   procedure Transform_Accordingly (O : in out Animal) is
   begin
      O.Add_Wings (2);
      O.Add_Legs (1);
   end Transform_Accordingly;

Internally the procedure calls Add_Legs and/or Add_Wings, and these should be the overridden versions, if there are any, not the one given by Animal. But this isn't so!

First, when we call that procedure, we need a conversion, because indeed the type of the argument is Animal, not Human or Flying_Horse.

   Transform_Accordingly (Animal (Rider));
   Transform_Accordingly (Animal (Pegasus));

We are indeed saying Rider and Pegasus are just animals! Already seen this when we said that Karl Marx was just an Animal, and gave him new wings despite the fact that we couldn't add wings to humans as humans…

This isn't what we want.

In Ada, to make a procedure to accept any type of a class of types (a hierarchy) we write this:

   procedure Transform_Accordingly (O : in out Animal'Class) is
   begin
      O.Add_Wings (2);
      O.Add_Legs (1);
   end Transform_Accordingly;

Now O.Add_Wings and O.Add_Legs will be dispatched correctly according to the actual type passed in, resulting in no wings for human, who accepts legs, though, and nothing at all for Pegasus — flying horses are perfect as they are and their mystic nature can't be altered.

New objects

Ada prefers static memory on dynamic memory allocation. It is easier, faster (asking for memory is a costly operation, even if it can be mitigated in several ways), and safer — you don't risk to hog the system memory, to start with. Garbage collectors aren't good in many situation when you need to control what is run and when, and Ada is also made to fit the requirements of hard realtime systems and embedded systems, too.

This doesn't mean you don't have those things like allocating memory from the system heap. Ada has also pointers, which it calls accesses, and they are exactly that, but don't expect you can use them as you do in C or C++: Ada anyway tries to keep your “safe”. Even if you can screw it in any language, also Ada, of course.

You get the access of an object (a variable) using the attribute Access, like this:

   X     : aliased X_Type;
   X_Ref : access X_Type;
   -- ...
   X_Ref := X'Access;

This give the access of an already allocated (statically) object. The keyword aliased tells two things: to the programmer, it tells that there will be accesses to that variable; to the compiler, it tells there must exist an actual memory address for that variable, that is, it must be held in memory — this is because otherwise the compiler is allowed not to reserve space in memory for the variable: it could be a CPU register.

We get an access also when we allocate memory, and we do it with the new keyword.

   X_Ref := new X_Type;

We can use the following syntax to allocate and initialize it as we wish:

   X_Ref := new X_Type'(Value);
   Y_Ref := new Structure'(Field1 => Value1,
                           Field2 => Value2);

Allocating memory is easy, as you can see. Incredibly, it is not that easy to free it! This must be because freeing memory is a delicate things to do. If you free memory whose address is kept in more than an access, suddenly you have a bunch of dangling pointers — addresses which aren't valid anymore.

This is well known to any decent C programmer — not those guys who lives on Java, which has a garbage collector and hasn't anything like pointers, except behind the scenes, because in fact Java internally works with references to instances.

I think the purpuse of Ada is to discourage using dynamic memory allocation light-heartedly, and when you do because you have to, you need to be very aware of it and the burden it brings in.

So, to manage memory deallocation you need to with a package:

with Ada.Unchecked_Deallocation;

The deallocator must be “created” giving both the type of the object you need to deallocate, and the type of the access. Something like this:

procedure Free_Something is new Ada.Unchecked_Deallocation
   (Object => Something_Type, Name => Something_Access);

You need to do this for every type you need to free!

Back to the object oriented speech, one of your primitive operations could be a New function which returns an access to the tagged type.

But if you do as I did in oriented.ads, i.e.

   type Object_Access is access Object;
   -- ...
   function NewObj return Object_Access;

you don't get a primitive operation, and the NewObj isn't inherited. In order to make it a primitive operation, you must write it explicitly, without creating a new type:

   function NewObj return access Object;

Let's remember that Ada isn't C where typedef gives just an alias. In Ada, you actually have a new type, and it is handled more like a new type, not just as an easier way to write the same thing.

Once you have a reference (an access), you dereference it using .all, like if an access had this property:

   Put_Line (Integer'Image (X_Ref.all));

This is a strange choice, anyway, that's it: .all dereference the access.

There's also the possibility to request that an access must not be null, that is, it must points to an object, always, hence it must be initialized when declared.

   Y_Ref : not null access Integer := X'Access;

When we pass an access to a subprogram, we can say that it must be not null:

   procedure Change_Int (I : not null access Integer) is
   begin
      -- ...
   end Change_Int;

See oriented_test.adb (and related files).

The sources

You can find the sources of my experiments for this article here.

Coin Marketplace

STEEM 0.26
TRX 0.11
JST 0.033
BTC 64678.67
ETH 3086.68
USDT 1.00
SBD 3.87