Ada Programming/Object Orientation

From Wikibooks, the open-content textbooks collection

Jump to: navigation, search

Contents


[edit] Object-orientation on Ada

An Ada class consists of three building blocks:

  • a package
  • a record (data storage)
  • primitive operations (methods).

This is different from C++ which has no concept of a package.

Note: Rigorously the meaning of the term class is different in Ada than in C++, Java and others. In Ada, a class is a special type whose values are those of the union of all the types derived from a given tagged type, called the class-wide type. In C++, the term class is a bit polysemic, it does mean the former concept, and it is a concrete type with operations associated, and finally a construct to define them.

[edit] The package

Every class has to be defined inside a package. One package may contain more than one class. You might be surprised about that requirement but you should remember that every class needs some housekeeping information which has to be kept somewhere; in Ada this housekeeping information is part of the package.

[edit] A little note for C++ programmers

If you have ever, when linking a C++ program, got an error message telling you that the virtual function table for class X was not found then you might appreciate this restriction. In Ada you never get this error message - the Ada equivalent of the virtual function table is part of the package. The same goes for the Ada equivalent of a virtual inline function, another cause of trouble when linking C++ programs - and they too work flawlessly in Ada.

[edit] The tagged record

A tagged type provides support for dynamic polymorphism and type extension in Ada. A tagged type bears a hidden tag that identifies the type and its position in the inheritance tree at run-time. It also provides the means of storing instance data.

package Person is

   type Object is tagged 
      record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
      end record;

end Person;
with Person;

package Programmer is

   type Object is new Person.Object with 
      record
         Skilled_In : Language_List;
      end record;

end Programmer;

[edit] Creating Objects

The Language Reference Manual's section on 3.3 Objects and Named Numbers (Annotated) states when an object is created, and destroyed again. This subsection illustrates how objects are created.

The LRM section starts,

Objects are created at run time and contain a value of a given type. An object can be created and initialized as part of elaborating a declaration, evaluating an allocator, aggregate, or function_call.

For example, assume a typical hierarchy of object oriented types: a top-level type Person, a Programmer type derived from Person, and possibly more kinds of persons. Each person has a name; assume Person objects to have a Name component. Likewise, each Person has a Gender component. The Programmer type inherits the components and the operations of the Person type, so Programmer objects have a Name and a Gender component, too. Programmer objects may have additional components specific to programmers.

Objects of a tagged type are created the same way as objects of any type. The second LRM sentence says, for example, that an object will be created when you declare a variable or a constant of a type. For the tagged type Person,

declare
   P: Person;
begin
   Text_IO.Put_Line("The name is " & P.Name);
end;

Nothing special so far. Just like any ordinary variable declaration this O-O one is elaborated. The result of elaboration is an object named P of type Person. However, P has only default name and gender value components. These are likely not useful ones. One way of giving initial values to the object's compontents is to assign an aggregate.

declare
   P: Person := (Name => "Scorcese  ", Gender => Male);
begin
   Text_IO.Put_Line("The name is " & P.Name);
end;

The parenthesized expression after := is called an aggregate (4.3 Aggregates (Annotated)).

Another way to create an object that is mentioned in the LRM paragraph is to call a function. An object will be created as the return value of a function call. Therefore, instead of using an aggregate of initial values, we might call a funtion returning an object.

Introducing proper O-O information hiding, we change the package containing the Person type so that Person becomes a private type. To enable clients of the package to construct Person objects we declare a function that returns them. (The function may do some interesting construction work on the objects.) We also declare a function that returns the name of Person objects.

package Persons is

   type Person is tagged private;

   function Make (Name: String; Sex: Gender_Type) return Person;

   function Name (P: Person) return String;

private
   type Person is tagged
      record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
      end record;

end Persons;

Calling the Make function results in an object which can be used for initialization. Since the Person type is private we can no longer refer to the Name component of P. But there is a corresponding function Name declared with type Person making it a socalled primitive operation. (The component and the function in this example are both named Name However, we can choose a different name for either if we want.)

declare
   P: Person := Make (Name => "Orwell", Sex => Male);
begin
   Text_IO.Put_Line("The name is " & Name(P));
end;

Objects can be copied into another. The target object is first destroyed. Then the component values of the source object are assigned to the corresponding components of the target object. In the following example, the default initialized P gets a copy of one of the objects created by the Make calls.

declare
   P: Person;
begin
   if 2001 > 1984 then
      P := Make (Name => "Kubrick", Sex => Male);
   else
      P := Make (Name => "Orwell", Sex => Male);
   end if; 

   Text_IO.Put_Line("The name is " & Name(P));
end;


So far there is no mention of the Programmer type derived from Person. There is no polymorphism yet, and likewise initialization does not yet mention inheritance. Before dealing with Programmer objects and their initialization a few words about class-wide types are in order.

[edit] The class-wide type

Whenever you declare a tagged type, the compiler will also provide a Class type for you. This type is called a class-wide type. Beginners often consider the class-wide type as some abstract concept. But this is not true! The Class type is a real data type. You can declare variables of the class-wide type, assign values to them, use the class-wide type as a parameter to functions and procedures and so forth.

The interesting part is that the class-wide type can hold values of any child class of the declared base type; in fact, there are no objects of this class-wide type - an object always is of a specific child type. This means that, unlike with most other object-oriented programming languages, you do not need to resort to the use of heap memory for storing arbitrary members of a class family but you can use the Class type instead.

Since the size of the child class is not known at compile time, any class-wide type is an indefinite subtype.

[edit] Primitive operations

The primitive operations of a given tagged type are those having a parameter or return value of this type and are declared immediately in the same package as the type.

Primitive operations are inherited and can be overridden for derived types.

Examples:

package X is

   type Object is tagged null record;

   procedure Class_Member_1 (This : in     Object);
   procedure Class_Member_2 (This :    out Object);
   procedure Class_Member_3 (This : in out Object);
   procedure Class_Member_4 (This : access Object);
   function  Class_Member_5 return Object;

end X;

A note for C++ programers: Class types are "by reference types" - they are always passed to a subprogram by using a pointer and never use "call by value" - using an access explicitly is therefore seldom needed.

Please note that neither named access types nor class-wide types qualify as class members. Procedures and functions having those as a parameter are just normal subprograms sharing the same package without belonging to the class.

Examples:

package X is

   type Object is tagged null record;
   type Object_Access is access Object;
   type Object_Class_Access is access Object'Class;

   procedure No_Class_Member_1 (This : in     Object'Class);
   procedure No_Class_Member_2 (This : in out Object_Access);
   procedure No_Class_Member_3 (This :    out Object_Class_Access);
   function  No_Class_Member_4 return Object'Class;


   package Inner is

      procedure No_Class_Member_5 (This : in Object);

   end Inner;

end X;

Note for C++ programmers: Procedures and functions like these can serve the same purpose as "non virtual" and "static" methods have in C++.

Ada 2005 adds a number of new features to the support for object oriented programming in Ada. Among these are overriding indicators and anonymous access types.

This language feature is only available in Ada 2005 standard.

Anonymous access types can now be used at more places. So also a function like Class_Member_4 below is a primitive operation.

The new keyword overriding can be used to indicate whether an operation is overriding an inherited subprogram or not. Note that its use is optional because of upward-compatibility with Ada 95. For example:

package X is

   type Object is tagged null record;

   procedure Class_Member_1 (This : in     Object);
   procedure Class_Member_2 (This : in out Object);
   function  Class_Member_3 return Object;
   function  Class_Member_4 return access Object;

   type Derived_Object is new Object with null record;

   overriding
   procedure Class_Member_1 (This : in     Derived_Object);

   -- 'Class_Member_2' is not overridden

   overriding
   function  Class_Member_3 return Derived_Object;

   not overriding
   function  New_Class_Member_1 return Derived_Object;

end X;

The compiler will check the desired behaviour.

This is a good programming practice because it avoids some nasty bugs like not overriding an inherited subprogram because the programmer spelt the identifier incorrectly, or because a new parameter is added later in the parent type.

It can also be used with abstract operations, with renamings, or when instantiating a generic subprogram:

not overriding
procedure Class_Member_x (This : in Object) is abstract;
 
overriding
function  Class_Member_y return Object renames Old_Class_Member;

not overriding
procedure Class_Member_z (This : out Object)
      is new Generic_Procedure (Element => Integer);

[edit] The class-wide type, again

Class-wide types play an important part in dynamically dispatching calls. Primitive operations of a type are bound to a specific type. Each object carries with it a tag that identifies its specific type at run time, hence it identifies specific primitive operations. How, then, are operations selected for polymorphic entities? This is where class-wide types enter.

Consider the types Person.Object and Programmer.Object as above, with one primitive operation added to them:

 package Person is
   type Object is taggedprocedure Inform (Who: in out Object; What: in String);
 package Programmer is
   type Object is new Person.Object withprocedure Inform (Who: in out Object; What: in String);

Supposedly, each type of person needs to be informed in their own ways, so Programmer.Inform overrides Person.Inform. Now another subprogram is used to call operation Inform for objects of any Person type, that is, of any type in the class rooted at Person.

  procedure Ask
     (Audience : in out Person.Object'Class;
      What : in String) is
  begin
     Inform (Audience, What);
  end Ask;

The effect of 'Class is that at run time, the specific object passed in for the parameter Audience will be inspected. It must be of type Person or of some type derived from Person. Depending on its run time tag, the proper Inform is selected for dispatching.

  Coder: Programmer.Object;
  Sales: Person.Object;

  …

  Ask (Sales, "Will you join us?");
  --  dispatches to Person.Inform

  Ask (Coder, "Do you want Pizza or Chop Soey?");
  --  dispatches to Programmer.Inform

[edit] Abstract types

A tagged type can also be abstract (and thus can have abstract operations):

package X is

   type Object is abstract tagged …;

   procedure One_Class_Member      (This : in     Object);
   procedure Another_Class_Member  (This : in out Object);
   function  Abstract_Class_Member return Object  is abstract;

end X;

An abstract operation cannot have any body, so derived types are forced to override it (unless those derived types are also abstract). See next section about interfaces for more information about this.

The difference with a non-abstract tagged type is that you cannot declare any variable of this type. However, you can declare an access to it, and use it as a parameter of a class-wide operation.

[edit] Interfaces

This language feature is only available in Ada 2005 standard.

Interfaces allow for a limited form of multiple inheritance. On a sematic level they are similar to an "abstract tagged null record" as they may have primitive operations but cannot hold any data. These operations cannot have a body, so they are either declared abstract or null. Abstract means the operation has to be overridden, null means the default implementation is a null body, i.e. one that does nothing.

An interface is declared with:

package Printable is

   type Object is interface;

   procedure Class_Member_1 (This : in     Object) is abstract;
   procedure Class_Member_2 (This :    out Object) is null;

end Printable;

You implement an interface by adding it to a concrete class:

with Person;

package Programmer is

   type Object is new Person.Object
                  and Printable.Object
   with 
      record
         Skilled_In : Language_List;
      end record;

   overriding
   procedure Class_Member_1   (This : in Object);

   not overriding
   procedure New_Class_Member (This : Object; That : String);

end Programmer;

As usual, all inherited abstract operations must be overridden although null subprograms ones need not.

[edit] Class names

Both the class package and the class record need a name. In theory they may have the same name, but in practice this leads to nasty (because of unintutive error messages) name clashes when you use the use clause. So over time three de facto naming standards have been commonly used.

[edit] Classes/Class

The package is named by a plural noun and the record is named by the corresponding singular form.

package Persons is

   type Person is tagged 
      record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
      end record;

end Persons;

This convention is the usually used in Ada's built-in libraries.

Disadvantage: Some "multiples" are tricky to spell, especially for those of us who aren't native English speakers.

[edit] Class/Object

The package is named after the class, the record is just named Object.

package Person is

   type Object is tagged 
      record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
      end record;

end Person;

Most UML and IDL code generators use this technique.

Disadvantage: You can't use the use clause on more than one such class packages at any one time. However you can always use the "type" instead of the package.

[edit] Class/Class_Type

The package is named after the class, the record is postfixed with _Type.

package Person is

   type Person_Type is tagged 
      record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
      end record;

end Person;

Disadvantage: lots of ugly "_Type" postfixes.

[edit] See also

[edit] Wikibook

[edit] Wikipedia

[edit] Ada Reference Manual

[edit] Ada 95

[edit] Ada 2005

[edit] Ada Quality and Style Guide

Personal tools
In other languages