AdaPower Logged in as Guest
Ada Tools and Resources

Ada 95 Reference Manual
Ada Source Code Treasury
Bindings and Packages
Ada FAQ


Join >
Home >
Articles >
Getting Started >
Ada FAQ >
Source Treasury >
Books & Tutorials >
Packages for Reuse >
Latest Additions >
Ada Projects >
Press Releases >
Ada Audio / Video >
Home Pages >
Links >
Contact >
About >
Login >
Back
Programming by Extension (Samuel Mize)

Programming by Extension means programming a large system so that new
processing cases can be handled by adding new code, without having to
recode any existing modules.  Language support is extremely helpful
for true programming by extension.

Object-oriented development is a very specific way of analyzing,
designing and coding a system.  It provides a way to program by
extension -- that's one of its selling points -- but it brings along
a lot of other baggage too.

Sometimes you want that baggage.  That's why "object-oriented"
technology exists.  But sometimes you just want to do plain functional
programming, and be able to extend the system later without having to
recode the existing part.  Ada supports this also.

The Ada 95 Rationale describes how Ada supports programming by
extension, but it doesn't really define the term.  Here's a concrete
example of programming by extension.

Consider a function to read up to 20 messages from some interface,
compute statistics and generate a summary.  The rest of the system
only cares about the summary; it doesn't care about the input
messages, or about how the interfaces work.  One simple approach would
put the summarizer function in a separate package, with an enumeration
variable to tell it which interface to use:

   package Simple_Approach is
      type Interfaces is (Disk_File, Serial_Interface, ...);

      type Summary_Type is
         record
            ...
         end record;

      function Summarizer (Interface_To_Use: Interfaces)
         return Summary_Type;

   end Simple_Approach;

The problem, of course, is that adding a new interface will cause
recoding of the type Interfaces and the function Summarizer, and
recompilation of anything that "withs" package Simple_Approach.

Note that Ada is often used in life-critical systems like avionics,
where each unit goes through a long and expensive validation of the
compiled object code.  This must be redone if the unit is recompiled,
even if the source code is not changed.

Lets use programming by extension to prevent all this recoding and
recompilation.  The first question is, what needs to be extended?
In this case, the operation of getting a message from an interface.
So, we'll create a procedure Get that gets a message from an
interface, basing it on a tagged type so it's polymorphic:

   with Message;
   package Generic_Interface is

      type Flag is tagged null record;

      procedure Get (Which_Interface: Flag;
                     Data: Message.Data_Type);

   end Generic_Interface;

We can now write Summarizer as a class-wide procedure, which uses
dispatching to get messages from whatever interface is appropriate:

   with Generic_Interface;
   package Extension_Approach is

      function Summarizer
         (Interface_To_Use: Generic_Interface.Flag'Class)
         return Summary_Type;

   end Extension_Approach;
   - - - - - -
   with Messages;
   package body Extension_Approach is

      function Summarizer
         (Interface_To_Use: Generic_Interface.Flag'Class)
         return Summary_Type
      is
         Data: array (1..20) of Message.Data_Type;
      begin
         for I in 1 .. 20 loop
            Get (Interface_To_Use, Data (I));
            exit when Data (I).Last_Message;
         end loop;
         ...

The body of Get in the package Generic_Interface may return an
exception, or get messages from a default interface (in which case
the package should probably be named Default_Interface).

To extend Summarizer to get messages from a new interface, we just
extend the type Generic_Interface.Flag, and override its Get:

   with Message;
   with Generic_Interface;
   package Disk_Interface is

      type Flag is new Generic_Interface.Flag with null record;

      procedure Get (Which_Interface: Flag;
                     Data: Message.Data_Type);

   end Disk_Interface;

In no case will the type Flag contain any data.

Now we can call Summarizer to get messages from disk and return us
the summary:

    Summary
        := Extension_Approach.Summarizer
            (Disk_Interface.Flag'(null record));

Even if Disk_Interface was written after Summarizer, we don't need to
recompile Summarizer.

With a bit more care we can code almost all the user system so that it
won't need to be recoded, or even recompiled, to handle any new
interface. First we add a declaration to Generic_Interface that lets
the user store a flag variable:

      type Interface_Selector is access constant Flag'Class;
   end Generic_Interface;

In each interface package, we put a constant Selected, so the user
won't have to use the rather clunky syntax "TYPENAME'(null record)".
We also put an access constant that denotes Selected, which can be
stored in a variable of type Interface_Selector.

      Selected: constant Flag := Flag'(null record);

      Selection: constant Generic_Interface.Interface_Selector
         := Selected'Access;

   end Disk_Interface;

(These additions to the _Interface packages are just convenience
items, the user code could make these declarations itself.)

Now, to hard-code which interface is being used, the code can say:

   with Disk_Interface;
   with Extension_Approach; use Extension_Approach;

   procedure User is
      Sum: Summary_Type;
   begin
      Sum := Summarizer (Disk_Interface.Selected);

To encapsulate the knowledge about interfaces in a single package,
the code might look like:

   package Interfaces is
      Current: Generic_Interface.Interface_Selector;
   end Interfaces;
   - - - - -
   with Interfaces;
   with Extension_Approach; use Extension_Approach;
   procedure User is
      Sum: Summary_Type;
   begin
      Sum := Summarizer (Interfaces.Current);
   ...

Now, to extend User, we only have to add another class, and somewhere
there will be a bit of code -- possibly buried in the user interface
-- that we will have to change so it can set Interfaces.Current to
New_Interface.Selection.

The entire rest of the system won't have to be recoded, or even
recompiled.

Another feature of the Programming-by-Extension facilities in Ada 95
is that, when you derive a new type from a tagged type, you can re-use
the "parent" type's procedures, if they still apply.  For example,
suppose that some interfaces must be explicity started and stopped.
The code might look like:

   with Message;
   package Generic_Interface is
      type Flag is tagged null record;

      procedure Get (Which_Interface: Flag;
                     Data: Message.Data_Type);

      procedure Start (Which_Interface: Flag); -- does nothing
      procedure Stop (Which_Interface: Flag); -- does nothing
      ...

   - - - - - -
   with Message;
   package Disk_Interface is
      type Flag is new Generic_Interface.Flag with null record;

      procedure Get (Which_Interface: Flag;
                     Data: Message.Data_Type);

      -- we don't need to start or stop a disk, so the do-nothing Start
      -- inherited from Generic_Interface is just fine
      ...

   - - - - - -
   with Message;
   package Serial_Interface is
      type Flag is new Generic_Interface.Flag with null record;

      procedure Get (Which_Interface: Flag;
                     Data: Message.Data_Type);

      -- Start and stop the serial interface
      procedure Start (Which_Interface: Flag);
      procedure Stop (Which_Interface: Flag);
      ...

And the user code would look like:

   with Interfaces;
   with Extension_Approach; use Extension_Approach;
   procedure User is
      Sum: Summary_Type;
   begin
      Start (Interfaces.Current);
      Sum := Summarizer (Interfaces.Current);
      Stop (Interfaces.Current);
   ...

I hope you find this extended sketch useful.

I would certainly NOT call this object-oriented programming.  But it
does take advantage of the Ada 95 facilities for programming by
extension.  It's sort of an "infinite case statement."  

Certainly this is a contrived example.  I HAVE used tagged types to
generate such an "infinite case statement," but it was buried in an
obscure and proprietary system, so it would be neither clear nor legal
for me to reproduce the code.

You can do the same general things in C++, but you have to make the
window type a class, and each new window type a subclass of that
class.  An instance of the class is an "object."  The terms "class"
and "object" carry a lot of connotations that may not be true in all
cases, for instance that the item in question is a discrete entity
that exists in its own right, or that the "classes" involved form a
meaningful taxonomy and not just a hierarchy that reflects some
implementation concern.

Further, I believe there may be extra semantic constraints and extra
coding effort in using C++ for non-object-oriented programming by
extension.  However, I am not familiar enough with the language to
give you specific details.


(c) 1998-2004 All Rights Reserved David Botton