Section author: Raphaël Amiard
Object-oriented programming (OOP) is a large and ill-defined concept in programming languages and one that tends to encompass many different meanings because different languages often implement their own vision of it, with similarities and differences from the implementations in other languages.
However, one model mostly "won" the battle of what object-oriented means, if only by sheer popularity. It's the model used in the Java programming language, which is very similar to the one used by C++. Here are some defining characteristics:
- Type derivation and extension: Most object oriented languages allow the user to add fields to derived types.
- Subtyping: Objects of a type derived from a base type can, in some instances, be substituted for objects of the base type.
- Runtime polymorphism: Calling a subprogram, usually called a method, attached to an object type can dispatch at runtime depending on the exact type of the object.
- Encapsulation: Objects can hide some of their data.
- Extensibility: People from the "outside" of your package, or even your whole library, can derive from your object types and define their own behaviors.
Ada dates from before object-oriented programming was as popular as it is today. Some of the mechanisms and concepts from the above list were in the earliest version of Ada even before what we would call OOP was added:
- As we saw, encapsulation is not implemented at the type level in Ada, but instead at the package level.
- Subtyping can be implemented using, well, subtypes, which have a full and permissive static substitability model. The substitution will fail at runtime if the dynamic constraints of the subtype are not fulfilled.
- Runtime polymorphism can be implemented using variant records.
However, this lists leaves out type extensions, if you don't consider variant records, and extensibility.
The 1995 revision of Ada added a feature filling the gaps, which allowed people to program following the object-oriented paradigm in an easier fashion. This feature is called tagged types.
It's possible to program in Ada without ever creating tagged types. If that's your prefered style of programming or you have no specific use for tagged types, feel free to not use them, as is the case for many features of Ada.
However, they can be the best way to express solutions to certain problems and they may be the best way to solve your problem. If that's the case, read on!
Before presenting tagged types, we should discuss a topic we have brushed on, but not really covered, up to now:
You can create one or more new types from every type in Ada. Type derivation is built into the language.
Type derivation is useful to enforce strong typing because the type system treats the two types as incompatible.
But the benefits are not limited to that: you can inherit things from the type you derive from. You not only inherit the representation of the data, but you can also inherit behavior.
When you inherit a type you also inherit what are called primitive operations. A primitive operation (or just a primitive) is a subprogram attached to a type. Ada defines primitives as subprograms defined in the same scope as the type.
A subprogram will only become a primitive of the type if:
- The subprogram is declared in the same scope as the type and
- The type and the subprogram are declared in a package
This kind of inheritance can be very useful, and is not limited to record types (you can use it on discrete types, as in the example above), but it's only superficially similar to object-oriented inheritance:
- Records can't be extended using this mechanism alone. You also can't specify a new representation for the new type: it will always have the same representation as the base type.
- There's no facility for dynamic dispatch or polymorphism. Objects are of a fixed, static type.
There are other differences, but it's not useful to list them all here. Just remember that this is a kind of inheritance you can use if you only want to statically inherit behavior without duplicating code or using composition, but a kind you can't use if you want any dynamic features that are usually associated with OOP.
The 1995 revision of the Ada language introduced tagged types to fullfil the need for an unified solution that allows programming in an object-oriented style similar to the one described at the beginning of this chapter.
Tagged types are very similar to normal records except that some functionality is added:
- Types have a tag, stored inside each object, that identifies the runtime type of that object.
- Primitives can dispatch. A primitive on a tagged type is what you would call a method in Java or C++. If you derive a base type and override a primitive of it, you can often call it on an object with the result that which primitive is called depends on the exact runtime type of the object.
- Subtyping rules are introduced allowing a tagged type derived from a base type to be statically compatible with the base type.
Let's see our first tagged type declarations:
To remain consistent with the rest of the language, a new notation needed to be introduced to say "This object is of this type or any descendent derives tagged type".
In Ada, we call this the classwide type. It's used in OOP as soon as you need polymorphism. For example, you can't do the following:
This is because an object of a type
T is exactly of the type
T is tagged or not. What you want to say as a
programmer is "I want O3 to be able to hold an object of type
My_Class or any type descending from
My_Class". Here's how you
Because an object of a classwide type can be the size of any descendent of its base type, it has an unknown size. It's therefore an indefinite type, with the expected restrictions:
- It can't be stored as a field/component of a record
- An object of a classwide type needs to be initialized immediately (you can't specify the constraints of such a type in any way other than by initializing it).
We saw that you can override operations in types derived from another tagged type. The eventual goal of OOP is to make a dispatching call: a call to a primitive (method) that depends on the exact type of the object.
But, if you think carefully about it, a variable of type
always contains an object of exactly that type. If you want to have a
variable that can contain a
My_Class or any derived type, it has
to be of type
In other words, to make a dispatching call, you must first have an object that can be either of a type or any type derived from this type, namely an object of a classwide type.
You can convert an object of type
Derived to an
object of type
My_Class. This is called a view conversion in
Ada parlance and is useful, for example, if you want to call a
In that case, the object really is converted to a
object, which means its tag is changed. Since tagged objects are
always passed by reference, you can use this kind of conversion to
modify the state of an object: changes to converted object will
affect the original one.
You can also call primitives of tagged types with a notation that's more familiar to object oriented programmers. Given the Foo primitive above, you can also write the above program this way:
If the dispatching parameter of a primitive is the first parameter, which is the case in our examples, you can call the primitive using the dot notation. Any remaining parameter are passed normally: