Gem #6: The Ada95 Multiple Views Idiom vs. Ada05 Interfaces

by Matthew Heaney —On2 Technologies

Let's get started…

One of the major changes to the Ada language is the addition of interfaces, stateless types that specify a set of operations. Like a tagged type, you can derive from an interface type, but unlike a tagged type, you can derive from multiple interfaces simultaneously.

Typically you use an interface type as a kind of specification. An abstraction can be written in terms of an interface type, which makes the abstraction completely general, in that it can be used with any type that implements that interface.

Ada95 does not have interface types, so an obvious question is, Can you create the effect of deriving from multiple interface types, but in Ada95? The answer is yes, using a technique called the "multiple views" idiom. The technique makes it possible for a type to provide different views of itself, with each view having a different type. This is very much like having multiple interfaces, but you have to build the infrastructure yourself.

To illustrate the difference between interfaces and multiple views, we'll first design a simple abstraction for persistence in terms of an interface type, and then redesign it using the multiple views technique. An interface for persistence would look something like this:

package Persistence_Types2 is
   type Persistence_Type is limited interface;

   procedure Write
     (Persistence : in Persistence_Type;
      Stream      : not null access Root_Stream_Type'Class) is abstract;
   ... -- Read not shown here
end Persistence_Types2;

Any type that derives from Persistence_Type is saying that it can be saved out to (and loaded in from) some persistent medium. Deriving from the interface type is easy:

package P2 is
   type T is limited new Persistence_Type with null record;

   overriding
   procedure Write
     (Persistence : in T;
      Stream      : not null access Root_Stream_Type'Class);
   ...
end P2;

Now all the application has to do is provide a persistence mechanism, that accepts an object that supports the persistence interface. Here's our abstraction for doing that:

package Persistence_IO2 is
   ... -- Initialization details omitted here
   procedure Save (Persistence : in Persistence_Type'Class);
end Persistence_IO2;

This provides the infrastructure that allows any type (here, type T) that derives from Persistence_Type to be written to the persistence medium. A file-based implementation might look something like this:

package body Persistence_IO2 is
   File : Ada.Streams.Stream_IO.File_Type;
   ...
   procedure Save
     (Persistence : in Persistence_Type'Class)
   is
   begin
      Persistence.Write (Stream (File));
   end Save;
end Persistence_IO2;

Now we did all this using an Ada05 interface type, but nothing we did strictly requires an interface type. You can achieve the same effect using a tagged type, which is how it would have been done in Ada95. The "interface" is just an abstract tagged null record:

package Persistence_Types is
   type Persistence_Type is abstract tagged limited null record;

   procedure Write
     (Persistence : in Persistence_Type;
      Stream      : access Root_Stream_Type'Class) is abstract;
   ... -- Read omitted here
end Persistence_Types;

Here the Persistence_Type is a tagged type instead of an interface type, but it's otherwise the same. Even the Persistence_IO abstraction is the same as before. What is different is how the type is used to support persistence in some other type. That other type will provide a "persistence view" of itself. We wish for the type to support multiple views (just as it would were it implementing multiple interfaces), so it's not a simple matter of directly deriving from persistence type, since Ada does not support derivation from more than one tagged type (this is true of both Ada95 and Ada05).

What we will do to implement a persistence view is to create an intermediary type that derives from Persistence_Type, but with an access discriminant that designates the parent (the type that supports the view). This allows operations of the intermediary type to bind to the parent type, since the discriminant provides access to the parent's state. To see how this all works, let us first show what the public part of the parent type looks like:

package P is
   type T is limited private;

   type Persistence_Class_Access is 
     access all Persistence_Type'Class;

   function Persistence (Object : access T)
     return Persistence_Class_Access;
   ... others views would be declared here
private
   ... -- see text below
end P;

Type T supports persistence by providing a Persistence function that returns an access value designating an instance of Persistence_Type. The "persistence view" of type T is obtained by invoking this accessor function. This mechanism can be extended any number of times, by providing multiple accessor functions that each return distinct views.

The Persistence function returns an access value that actually designates a component of record T. The type of the component is the intermediary type we mentioned earlier, that derives from Persistence_Type, declared like this:

private
   type Persistence_View (Object : access T) is
     new Persistence_Type with null record;  -- no state req'd here

   procedure Write
     (Persistence : in Persistence_View;
      Stream      : access Root_Stream_Type'Class);

Note that the Persistence_View type is a null extension (it doesn't require any state of its own), but with an access discriminant that designates the parent type T. This allows the implementation of operation Write to see the representation of the parent type, since the Persistence parameter has an access discriminant that designates the T instance, and so the operation has access to all of the state that needs to be written to persistent storage.

The parent type T is implemented by declaring an aliased component of the intermediary type:

   type T is limited record
      Persistence : aliased Persistence_View (T'Access);
      ... -- rest of state here
   end record;

The Persistence component is aliased since function Persistence returns an access value designating that component:

   function Persistence (Object : access T)
     return Persistence_Class_Access
   is
   begin
      return Object.Persistence'Access;
   end;

The other difference between interface types and the multiple views idiom is how a client would actually invoke the operation to perform the persistence operation. In the interface case it's simple: type T derives from Persistence_Type directly, so instances of type T can be passed to any operation whose type is Persistence_Type'Class. The Persistence_IO package has just such an operation, so the call would look like this:

procedure Test_Persistence2 is 
   Object : T;
begin 
   ...
   Persistence_IO2.Save (Object);
end;

You have to do a little more work in the multiple views case, since the conversion from T to Persistence_Type isn't automatic. Here we need to explicitly invoke the Persistence function to "convert" from type T to Persistence_Type'Class. The accessor function has an access parameter and so we must declare the instance of type T as aliased. The call looks like this:

procedure Test_Persistence is
   Object : aliased T;
begin
   ...
   Persistence_IO.Save (Persistence (Object'Access).all);
end;

That's all there is to it. The multiple views idiom is actually a very powerful mechanism for composing abstractions. I have characterized the technique as an Ada95 idiom, but note that even in Ada05 it is occasionally useful, such as when you need to mix a tagged type into an existing hierarchy. The most common example is needing to add controlledness to a leaf type in a class, because the root type doesn't itself derive from controlled.

Related Source Code

Ada Gems example files are distributed by AdaCore and may be used or modified for any purpose without restrictions.

gem_6.ada