Ada 9X support for multiple inheritance

This page contains motivations for the way multiple inheritance is supported in Ada 9X. It was started after a posting by someone on the Usenet comp.lang.ada newsgroup.


Multiple inheritance in 9X: Why it is the way it is...

Newsgroups: comp.lang.ada
From: stt@dsd.camb.inmet.com (Tucker Taft)
Subject: Re: Ada 90 inheritance request?
Organization: Intermetrics, Inc.
Date: Sat, 17 Dec 1994 13:55:36 GMT
In article JGOODSEN.94Dec13140247@treasure.radsoft.com, John Goodsen (jgoodsen@treasure.radsoft.com) wrote:

I am dismayed at how anyone could look at this so-called "solution" to multiple inheritance and walk away comfortable that Ada9X properly supports MI. The lack of *direct* support for MI in Ada 9X, is IMHO, a serious foobar that has to this point been explained away by the ivory tower language designers ...
Feel free to accuse of us of all kinds of crimes against nature, but please don't put us in an ivory tower ;-). I trust that those who know me recognize that I tend to live more in the cement dungeon of language design, scrabbling around in the cement dust of human engineering, implementation concerns, efficiency, etc. The Ada 9X mapping/revision team consisted almost entirely of dedicated, very experienced, Ada and other-language programmers with a strong penchant for designing the most productive, reliable, readable, and usable language they knew how, given the constraints of the process.

An easy to use, intuitive, efficient, straightforward, multiple inheritance mechanism in Ada 9X would have been great. Unfortunately, we have never seen such a beast. Perhaps you have. What we found when investigating ways to support multiple inheritance was that everybody knew what was the best way to do it -- their own way! When we have encountered such things in the past during language design, the conclusion to be drawn is that implementing multiple inheritance is still a *programming* problem, not a language design problem. If even two existing OOP languages did Multiple Inheritance the same way we would have seen that as a reasonable starting place. But the fact is that the rules for dealing with the inevitable additional complexities of multiple inheritance are different in essentially every langauge we looked at. Even Sather and Eiffel, which otherwise have a lot in common, differ in how they resolve the MI issues. C++ in its usual pragmatic way provides two different ways to do MI, using regular or virtual base classes. CLOS just merges based on names and a search order for methods. Eiffel provides renaming. Etc...

By constrast, all of the languages agree on how to do single inheritance in the language, and it is essentially unchanged since the days of Simula '67. It is intuitive, efficient, straightforward, reliable, type-safe, productive, useful, ... That sounds like a feature for Ada ;-). Note that Smalltalk, Object Pascal, Modula-3, Oberon-X, and Objective-C all have chosen to stick with single inheritance. There are (ivory tower ;-) researchers studying ways to add MI in some of these languages, but the practitioners seem to be plowing ahead very productively and happily relying on the rock-solid single inheritance, building up their conceptual multiple inheritance hierarchies as they see fit, in an application-specific way.

We expect that most Ada 95 programmers will find the same thing. Single inheritance is there, in its full glory and simplicity, right in the language. In addition, there are building blocks for constructing robust application-specific solutions to problems that might otherwise use a language-provided multiple inheritance mechanism.

Furthermore, what happens in some cases, is that the language-provided multiple inheritance mechanism turns out to be not quite the right solution for a given problem that is "reminiscent" of multiple inheritance. Then you have the worst of both worlds -- a language made more complex by the addition of linguistic multiple inheritance, and an application made more difficult by the lack of appropriate building blocks for solving the problem the "right" way.

To summarize, it is our view that it is a bit ivory-towerish to look at a language, and say that if it doesn't have feature X implemented in some specific way, then it is useless and FUBAR. I can't tell you how many times we have been threatened over the past 5 years with similar statements about all kinds of obscure features. It is interesting to follow the C++ standardization process, where the same desparate statements are made in support of one obscure feature after another.

The fact is that a good language has its own self-consistent logic, and some features make a good addition to that structure, and others just don't fit. We couldn't find a language-provided multiple inheritance mechanism that enhanced Ada. We could find plenty that made it more complicated, less intuitive, less reliable, less readable, etc. We welcomed proposals and suggestions for one that would be a net addition to the language, but on deeper investigation into the nitty-gritty, we never found one where the pluses outweighed the minuses. We had an "ivory-tower" desire to try to find one, but we had a more practical concern that we didn't want to FU the language to get one in, just for the marketing potential. We wanted the features to be useful and reliable, not just there for show. Perhaps the designers of Ada 007 will find the silver bullet... Meanwhile, we will be productively building reliable systems in our cement basement.

>John Goodsen                         Currently on-site at:
>The Dalmatian Group                       JP Morgan
>User Interface Specialists                60 Wall St., New York City
>jgoodsen@radsoft.com                      jgoodsen@jpmorgan.com

S. Tucker Taft   stt@inmet.com
Ada 9X Mapping/Revision Team
Intermetrics, Inc.
Cambridge, MA  02138

Multiple Inheritance in Ada 9X

Newsgroups: comp.lang.ada
From: stt@spock.camb.inmet.com (Tucker Taft)
Subject: Re: Ada 90 inheritance request?
Organization: Intermetrics, Inc.
Date: Fri, 2 Dec 1994 16:46:08 GMT
Here is the note on multiple inheritance in Ada 9X.

-Tucker Taft

P.S. This note is absolutely not intended to start Y.A.F.W. (yet another flame war ;-). -TT

                       MULTIPLE INHERITANCE IN ADA 9X

                        S. Tucker Taft stt@inmet.com
                        Ada 9X Mapping/Revision Team
                             Copyright (C) 1994
                             Intermetrics, Inc.
                             733 Concord Avenue
                            Cambridge, MA 02138
                 May be copied if accompanied by this notice.

This note discusses the creation of multiple inheritance type hierarchies
using the Ada 9X object-oriented programming features.  It is in part
directed at programmers familiar with other object-oriented programming
languages that build in syntax for a particular multiple-inheritance
mechanism, rather than simply providing the building blocks needed to
support it.

In this discussion, we will in general use Ada 9X terminology, where every
object has a single "type," and multiple similar types (typically in some
kind of hierarchy or oligarchy) form a "class" of types.  If we want to use
the term "class" as it is used in C++ or Eiffel, we will always say "C++
class" or "Eiffel class."

In some languages, such as Eiffel, multiple inheritance serves many
purposes.  For example, there is no equivalent in Eiffel to the "include"
statement of C/C++ or the "with/use" clauses of Ada.  Therefore, to gain
direct visibility to the declarations of some other module, one must inherit
from that module (Eiffel class).

In C/C++, one can simply "include" a file containing a set of type and
object definitions.

In Ada, one first identifies the external modules of interest via "with"
clauses, and then chooses selectively whether to make only the name of the
module (package) visible, or its contents (via a "use" clause).

In both Eiffel and C++, one can choose to inherit from some other type,
without making that visible to clients of the new type.  Effectively, the
linguistic multiple inheritance mechanism is being used to express not an
"is a refinement of" relationship, but rather an "is implemented using"
relationship.

Finally, there are the situations where a single type visibly inherits from
two or more other types.  In these cases, this is rarely a symmetric
situation.  Rather, one of the ancestor types is the "primary" ancestor,
while the others are typically "mix-ins" designed to augment behavior of the
primary ancestor.

Ada 9X supports multiple-inheritance module inclusion (via multiple
"with"/"use" clauses), multiple-inheritance "is-implemented-using" via
private extensions and record composition, and multiple-inheritance mix-ins
via the use of generics, formal packages, and access discriminants.

The Ada 9X mechanisms are designed to eliminate "distributed" overhead, so
that there is no added expense for the general user of the language because
of the presence of the mechanisms supporting multiple inheritance.
Furthermore, the Ada 9X building blocks that support multiple inheritance
are general purpose, and support many other paradigms beyond conventional
multiple inheritance.

There are basically three distinct situations associated with multi-
inheritance mixins:

   1. The case where the mix-in provides components and operations, and
      any overriding of these operations needs only to look at the
      components of the mix-in itself.

   2. The case where the mix-in provides components and operations, and
      some of the overriding of these operations needs access to the
      whole object, rather than just the components of the mix-in.

   3. Like (2), and in addition, any object with the mix-in must be
      able to be linked onto a list (or into some similar heterogeneous
      data structure) of other objects with the same mix-in.

Case (1) is supported directly in Ada 9X by a record or private extension,
with the type being mixed in (in a possibly extended form) as a component of
the record extension.

Case (2) is handled with a generic, that takes any type in a given class
(formal derived type), adds components (via extension) and operations, and
then reexports the extended type.  The new operations have access to the
whole object, not just to the components being added.

Case (3) is handled with an access discriminant, that provides access to the
enclosing object for the operations of the mix-in, while still allowing
links through the mix-in.  Generics can also be used to automate the
approach.

Here are a few examples:

   - Case (1) -- One has an abstract type "Set_of_Strings" and one
     wants to implement it by reusing an existing (concrete) type
     "Hash_Table":

     Here is the abstract type:

           type Set_Of_Strings is abstract tagged limited private;
           type Element_Index is new Natural;  -- Index within set.

           No_Element : constant Element_Index := 0;

           Invalid_Index : exception;

           procedure Enter(
             -- Enter an element into the set, return the index
              Set : in out Set_Of_Strings;
              S : String;
              Index : out Element_Index) is abstract;

           procedure Remove(
             -- Remove an element from the set; ignore if not there
              Set : in out Set_Of_Strings;
              S : String) is abstract;

           procedure Combine(
             -- Combine Additional_Set into Union_Set
              Union_Set : in out Set_Of_Strings;
              Additional_Set : Set_Of_Strings) is abstract;

           procedure Intersect(
             -- Remove all elements of Removal_Set from Intersection_Set
              Intersection_Set : in out Set_Of_Strings;
              Removal_Set : Set_Of_Strings) is abstract;

           function Size(Set : Set_Of_Strings) return Element_Index
             is abstract;
             -- Return a count of the number of elements in the set

           function Index(
             -- Return the index of a given element;
             -- return No_Element if not there.
              Set : Set_Of_Strings;
              S : String) return Element_Index is abstract;

           function Element(Index : Element_Index) return String is abstract;
             -- Return element at given index position
             -- raise Invalid_Index if no element there.
         private
           type Set_Of_Strings is abstract tagged limited ...


     Here is an implementation of this abstract type that inherits its
     interface from Set_Of_Strings, and its implementation from
     Hash_Table:

           type Hashed_Set(Table_Size : Positive) is
             new Set_Of_Strings with private;

           -- Now we give the specs of the operations being implemented
           procedure Enter(
             -- Enter an element into the set, return the index
              Set : in out Hashed_Set;
              S : String;
              Index : out Element_Index);

           procedure Remove(
             -- Remove an element from the set; ignore if not there
              Set : in out Hashed_Set;
              S : String);
           . . . etc.

         private
           type Hashed_Set(Table_Size : Positive) is
             new Set_Of_Strings with record
               Table : Hash_Table(1..Table_Size);
             end record;

     In the body of this package, we would provide the bodies for each
     of the operations, in terms of the operations available from
     Hash_Table.  Chances are they don't match exactly, so a little bit
     of "glue" code will be necessary in any case.

   - Case (2) -- One has a type Basic_Window that responds to various
     events and calls.  One wants to embellish the Basic_Window in
     various ways with various mix-ins.

          type Basic_Window is tagged limited private;
          procedure Display(W : Basic_Window);
          procedure Mouse_Click(W : in out Basic_Window;
                                Where : Mouse_Coords);
          . . .


     Now one can define any number of mix-in generics, like the
     following:

          generic
            type Some_Window is new Window with private;
              -- take in any descendant of Window
          package Label_Mixin is
            type Window_With_Label is new Some_Window with private;
              -- Jazz it up somehow.

            -- Overridden operations:
            procedure Display(W : Window_With_Label);

            -- New operations:
            procedure Set_Label(W : in out Window_With_Label; S : String);
              -- Set the label
            function Label(W : Window_With_Label) return String;
              -- Fetch the label
          private
            type Window_With_Label is
              new Some_Window with record
                Label : String_Quark := Null_Quark;
                  -- An XWindows-Like unique ID for a string
              end record;

     In the generic body, we implement the Overridden and New
     operations as appropriate, using any inherited operations, if
     necessary.  For example, this might be an implementation of the
     overridden Display:

          procedure Display(W : Window_With_Label) is
          begin
              Display(Some_Window(W));
                -- First display the window normally,
                -- by passing the buck to the parent type.

              if W.Label /= Null_Quark then
                -- Now display the label if it is not null
                  Display_On_Screen(XCoord(W), YCoord(W)-5, Value(W.Label));
                    -- Use two inherited functions on Basic_Window
                    -- to get the coordinates where to display the label.
              end if;
          end Display;

     Presuming we have several such generic packages defined, We can
     now create the desired window by mixing in repeatedly.  First we
     declare the tailored window as a private extension of
     Basic_Window, and then we define it via a sequence of
     instantiations:

            type My_Window is Basic_Window with private;
            . . .
          private
            package Add_Label is new Label_Mixin(Basic_Window);
            package Add_Border is
              new Border_Mixin(Add_Label.Window_With_Label);
            package Add_Menu_Bar is
              new Menu_Bar_Mixin(Add_Border.Window_With_Border);

            type My_Window is
              new Add_Menu_Bar.Window_With_Menu_Bar with null record;
                -- Final window is a null extension of Window_With_Menu_Bar.
                -- We could instead make a record extension and
                -- add components for My_Window over and above those
                -- needed by the mix-ins.

   - Case(3) -- In this case, let us presume we have two independent
     hierarchies, one for Windows, which represent areas on the screen,
     and one for Monitors, which wait for an object to change, and then
     do something in reaction to it.  An object that supports Monitors
     keeps a linked list of Monitors, and calls their Update operation
     whenever the object is changed.  For example:

          type Monitor;
          type Monitor_Ptr is access Monitor'CLASS;

          type Monitored_Object is abstract tagged limited
            -- Monitored objects are derived from this root type
            record
              First : Monitor_Ptr;  - List of monitors
              -- More components to be added by extension
            end record;

          type Monitored_Object_Ptr is access Monitored_Object'CLASS;

          type Monitor is abstract tagged limited
            record
              Next : Monitor_Ptr;
              Obj : Monitored_Object_Ptr;
              -- More components to be added by extension
            end record;
          procedure Update(M : in out Monitor) is abstract;
            -- Dispatching operation, called when monitored object
            -- is changed.

     Now suppose we want to create a Window that can act as a Monitor
     as well as a Window:

     First we define a mix-in that is a monitor, and override its
     Update op:

          type Monitor_Mixin(Win : access Basic_Window'CLASS) is
            new Monitor with null record;
          procedure Update(M : in out Monitor_Mixin);

     The body for this Update could be:

          procedure Update(M : in out Monitor_Mixin) is
            -- On an Update, simply re-Display the window
          begin
              Display(M.Win);  -- This is a dispatching call
          end Update;

     Now we can "mix" this Monitor_Mixin into any window type, as
     follows:

          type Window_That_Monitors is
            new My_Window with record
              Mon : Monitor_Mixin(Window_That_Monitors'ACCESS);
            end record;


     We could define a tailored Monitor mix-in that did something
     besides just call the Display operation of the associated Window.
     But in many cases, this simple one will do the job.

As these examples illustrate, Ada 9X provides support for the construction
of essentially arbitrary multiple inheritance type hierarchies, without
having to commit itself to a single linguistic mechanism for multiple
inheritance, and without burdening simple single-inheritance problems with
the complexity and implementation burden of linguistic multiple inheritance.

Furthermore, the building blocks that support multiple inheritance are very
general, and allow the programmer to easily go well beyond the limitations
of conventional multiple inheritance mechanisms.

Dirk Craeynest (Dirk.Craeynest@cs.kuleuven.ac.be)
Ada-Belgium Newsletter Editor & Team Ada