The type system of Ada that existed in Ada83 has been extended to include
support for object oriented programming. Rather than an extension that would
be discordant with what existed, the object oriented facilities extend the
existing type system to allow for type extension. These provide all the
classical object oriented facilities such as inheritance, dynamic dispatching,
polymorphism, with the usual Ada features of readability and safety.
Types are for many purposes similar to classes.
The following object diagram is implemented using Ada.
Inheritance can be split into two distinct concepts, inheritance of operations (methods) and inheritance and further addition of attributes (type extension). Ada 83 supported inheritance of operations. If a new type is derived from a base type, then the derived type inherits the operations available from the parent. For example,
type person is record name :string(1..10);
dob :date; salary :float := 0.0; -- :-( end record; procedure employ(someone:in out person); procedure dismiss(someone:in out person); type manager is new person; -- derived type manager inherits the operations of person, -- and can add new operations of its own. procedure allocate_master_key(someone:in out manager);
This model of inheritance does not allow for extension of types, that is we cannot add in extra attributes. The procedures (or functions) that take a parameter of a type are said to be the operations on the type and are equivalent to methods in other OO languages.
Ada95 has extended the notion of type derivation to support conventional object oriented programming. A new type, the tagged record, has been introduced to represent a type that can be extended (inherited). The syntax for a tagged type is
type person is tagged record name :string(1..10);
dob :date; salary :float := 0.0; end record;
A tagged record is treated like a conventional record in most situations. Field accessing, and setting is just as normal.
jane :person := ("Jane ", (6,10,1978), 210.0);
Operations on the type are written just as before...
procedure employ(someone:in out person); procedure dismiss(someone:in out person); procedure pay_rise(someone:in out person);
If we want to extend the type, we follow a notation similar to the type derivation common in Ada...
type manager is new person with record master_key_code :string(1..10); end record;
The "with record" part indicates the extension to the base type (in this case "person").
New operations for the manager can be added, just as in Ada83...
procedure allocate_master_key( someone:in out manager; code :in string);We may even want to override an inherited operation...
procedure pay_rise(someone:in out manager);
We can now write a different algortihm for pay_rise's for manager's compared to person's.
In Ada, the package concept is simply used for encapsulating the declaration of a type, and it's operations. The package is used to hide the implementation details, and as an aid to recompilation (we can split a program up into smaller chunks, which individually are quick to compile). A very common convention is to place one type and it's operations into one package.
The above example would thus be placed into two packages...
with dates; use dates; -- definition of type date package persons is type person is tagged record name :string(1..10);
dob :date; salary :float := 0.0; end record; procedure employ(someone:in out person); procedure dismiss(someone:in out person); procedure pay_rise(someone:in out person); end persons;
The declaration of the derived type would be placed in a second package...
with persons; use persons; package managers is type manager is new person with record master_key_code :string(1..10); end record; procedure allocate_master_key( someone:in out manager; code :in string); procedure pay_rise(someone:in out person); end managers;
For tagged types, the operations on the type that are included in the same package as the type declaration (as all of the above are) have a special status - they are called the primitive operations of the type - and are all potentially dispatching calls. This will be described in further detail later.
Using these types is quite simple. You just declare variables as you would have before...
with text_io; use text_io; with persons; use persons; with dates; use dates; with managers; use managers; procedure demo is me :person; him :manager; begin me.name := "Santa "; me.dob := (29,11,1962); him.name := "Rudolph "; -- inherited field him.dob := (28, 6, 1962); -- inherited field employ(him); -- inherited operation -- new method for type manager allocate_master_key(him, code => "XYZ398"); pay_rise(him); -- Manager version pay_rise(me); -- Person version end;
A type may need to be created that has no fields (attributes), only operations (methods). This can be achieved by specify an empty record when declaring a type.
type root is tagged record null; end record;
Ada has a special syntax for null records being used with tagged types:
type root is tagged null record;
This can now be used as the basis of further derivations.
Operations on the type can be added as normal within a package specification...
procedure do_something(item :root);
More typically a new type may be derived from an existing type without the need for new attributes, but with the need for new subprograms.
The syntax for such an extension would be...
type director is new manager with null record; procedure pay_rise(someone :in out director);
A director has no new components added, but can has yet another method for allocating a pay_rise.
Sometimes we wish to create an artificial type that we never expect to use, but serves as the "root" of a tree of types. These are called abstract types. They may also include abstract subprograms , which do not have a body and must be overridden by a derived type. This forces all descendants of a type to support a common functionality.
An abstract type has the following syntax...
package Sets is type Set is abstract tagged null record; function Empty return Set is abstract; function Empty(Element:Set) return boolean is abstract; function Union(left, right:set) return Set is abstract; function Intersection(left, right:set) return Set is abstract; procedure Insert(Element:natural; into:Set) is abstract; end Sets;
This implementation of a set of natural numbers is taken from the Ada Language Reference Manual. Note that you cannot declare a variable of type Set because it is an abstract type. E.g.
with Sets; use Sets; procedure wont_compile is my_set :Set; -- illegal, abstract type begin null; end;
However a derived type may or may not be abstract...
with Sets; package quick_sets is type bit_vector is array(0..255) of boolean; pragma pack(bit_vector); type quick_set is new Sets.set with record bits:bit_vector := (others => false); end record; -- advertise concrete implementations function Empty return quick_set; function Empty(Element:quick_set) return boolean; etc. etc.
For any given tagged type, there is an associated class wide type, that encompasses the tagged type and all types derived from it. Ada has used the word "class" differently to most object oriented languages. It is perhaps used in a more traditional English sense, as a collection of similar types.
For example in the type family (inheritance hierachy)
vehicle motorised truck car train unmotorised bicycle
the type vehicle'class refers to all the types above. Motorised'class refers to the classes motorised, truck, car and train.
This can be used to allow for dynamic selection of the appropriate operation.
Assume that the following procedure was defined in package persons.
procedure reward_good_work(someone:in out person) is begin print_certificate(someone.name); pay_rise(someone); end;
If we call this with a manager as a parameter, then the procedure persons.pay_rise will be called, not managers.pay_rise.
A solution to this problem is to accept as a parameter any variable in the class wide type. This is done as follows...
procedure reward_good_work(someone:in out person'class) is begin print_certificate(someone.name); pay_rise(someone); end;
If the following declarations are made...
me :person; him :manager;
reward_good_work(me); -- calls persons.pay_rise reward_good_work(him); -- calls managers.pay_rise
Here the pay_rise procedure called depends on the specific type passed in as a
parameter. This is called dynamic dispatching - deciding which procedure to
dispatchi to (or call) at run time (dynamically). Passing in a manager object
results in a call to the manager's pay_rise routine. Passing in a person object
results in a call to the person's pay_rise routine.
What pay_rise routine would be called if a Director object was passed in?
In fact all the primitive operations of type are potentially dispatching calls, given the right circumstances. The example using a class wide type is one example. Another (closely related) example is the use of class wide access (pointer) types.
What you can't do with a class wide type (such as person'class) is create an array of them. This is because each object is potentially a different size - arrays require all elements to be the same size. Similar considerations apply for any place where a declaration must be of a fixed size (constrained, in Ada parlance).
Most pointers in Ada are restricted to pointing at just a single type. For example...
type node; type ptr is access node; type node is record item :integer; next :ptr; end record;
Here variables of type ptr are restricted to always pointing at variables of type node.
With the concept of class wide types in Ada, comes the concept of a pointer
that can point to any item within an inheritance hierachy.
This is achived by...
who' can now point at any object from the derivation tree rooted at person, i.e. person, manager, director, and quite importantly, any future types included in the inheritance hierachy.
type person_ptr is access person'class;
who := new person; who := new manager; who := new director;
Interestingly, we could create an array of class wide access types...
type person_list is array(1..10) of person_ptr; people :person_list; ... people(1) := new person; people(2) := new manager; people(3) := new person; people(4) := new director; -- initialise the variables appropriately. ... for i in people'range loop exit when people(i) = null; -- now dispatch to the appropriate routine... pay_rise(people(i).all); end loop;
Class wide pointers can be used to build hetrogeneous data structures, that is those were the components are not all the same type (they must however be in the same class).
A type can be declared private to help enforce information hiding (especially of the exact details of its representation, e.g. is it an array, or a linked list?). Similarly tagged types can be made private.
type person is tagged private;
The private section of the package must complete the declaration (provide what is called a full view of the type)...
type person is tagged record...
A type extension can also have a private extension if desired...
type manager is new person with private;
This gives sufficient flexibility for most purposes.
Tagged types can now be specified as generic type parameters (they can still be passed as actuals to the normal private or limited private type parameters). The syntax is as follows...
generic -- actual must be a tagged type type T is tagged private; -- actual must be direct descendent of S type T is new S; -- actual must be derived from S somewhere type T is new S with private; package some_interesting_pacakge is...
Ada95 does not directly support MI. It provides features that can be used to build MI "by hand" with a bit of extra work. The options are generics (to extend a type with a set of new features), access discriminants (to allow a type to point at it's containing object, and therefore allow delegation) and another one that I can't quite remember now.