Persistence of Type Conformity in the Generic Model


 One of the features of Ada that is more powerful than C++ templates
 is the persistence of type conformity throughout the entire generic
 model.  Modula-3 comes pretty close to Ada in this respect because
 it also requires type-checking (and parameter conformity) of 
 instantiated units.  This benefit of Ada is especially important
 when creating generic units that approximate what we see in C++.
 That is, in C++, we can instantiate a class with one or more 
 entire templates.  Although the C++ syntax is simpler, the conformity
 checking is less reliable than Ada or Modula-3 where we can instantiate
 a module (Ada package) with an entire module as the generic formal
 parameter.   In doing this with Ada, you will sometimes have to include
 some type (view) conversion in the design.  We provide the following
 abbreviated example of a numeric package.  Some of the features of the
 full example have been eliminated in the interest of brevity, but it 
 should be easily seen how this can be useful in creating reusable
 software components.  Also, I realize the original discussion dealt with
 class-wide types, but Ada's consistency model suggests that the same  
 principles would apply even in the example shown.


 with Numeric_Model;
 generic
     with package Number_Ops is new Numeric_Model(<>);
 package Number_Operations is
    type Real is private;
    type Real_Reference is access all Real;
    function Zero return Real;
    function "+"  (L, R : Real) return Real;
    function "-"  (L, R : Real) return Real;
    function "*"  (L, R : Real) return Real;
    function "/"  (L, R : Real) return Real;
    function "="  (L, R : Real) return Boolean;
    function ">"  (L, R : Real) return Boolean;
    function "<"  (L, R : Real) return Boolean;
    function ">=" (L, R : Real) return Boolean;
    function "<=" (L, R : Real) return Boolean;
    function SQRT (V    : Real) return Real;
    -- More Math Operations on Real
    function Machine_Number (V : Real) return Real;
    -- More attribute operations as functions
    function  Get return Real;
    procedure Put (V : Real; Fore, Aft : Natural);
    -- Specialize the exceptions for this type 
    Real_Constraint_Error : exception;
    Divide_By_Zero        : exception;
    Outside_Base_Range    : exception;
  private
    type Real is new Number_Ops.Number;
  end Number_Operations;

In this example we are defining our own Real number type with associated
operations.  It is possible to actually declare the full type of Real
as a tagged type or a simple record using this model, thereby encapsualting
some unique properties for a specialized numeric application.  The with
statement refers to a generic package.  In this case, the generic package
only contains generic formal parameters for a set of functions.  Here
is the specification for package Numeric_Model. 

generic
  type Number is digits <>
  with function "+"    (L, R : Number) return Number  is <>
  with function "*"    (L, R : Number) return Number  is <>
  with function "-"    (L, R : Number) return Number  is <>
  with function "/"    (L, R : Number) return Number  is <>
  with function "abs"  (N : Number)    return Number  is <>
  with function "="    (L, R : Number) return Boolean is <>
  with function ">"    (L, R : Number) return Boolean is <>
  with function "<"    (L, R : Number) return Boolean is <>
  with function ">="   (L, R : Number) return Boolean is <>
  with function "<="   (L, R : Number) return Boolean is <>
package Numeric_Model is end Numeric_Model;

At first glance, one might conclude that this is not useable because
it has no body.  That is exactly what makes it so powerful.  It is 
also what makes it possible to preserve type and profile conformity
across the entire compilation process.  In this respect it is slightly
more complex syntax than that of Modula-3 but also checked at earlier
stages of the design process.  

Notice that the statement,

    with package Number_Ops is new Numeric_Model(<>);

looks like an instantiation itself.  In fact, the package for
which this is a generic formal parameter will always refer to
Number_Ops not to Numeric_Model just as if Number_Ops was a
true instantiation.  This is frequently a point of confusion for
Ada programmers but once you understand it, it becomes quite sensible.
How else could we preserve full profile conformity checking throughout
the entire compilation process?  Some of us have taken to calling this
kind of generic, one with only generic formal parameters and no body,
as a "generic formal model."  We need to find some agreement in the 
Ada community on what to finally call it so it can be referred to 
in the literature more concisely.

Our purpose in using this package might include declaring our own versions
of division and equality checking.  This is not an unusual requirement
for floating point numbers.  Here is a simple procedure with some of the
calls "stubbed out" to illustrate the idea.

with Numeric_Model;
with Number_Operations;
procedure Test_Own_Float_Operations is
  type Own_Float is digits 12;
  function Divide    (L, R : Own_Float) return Own_Float is
     Result : Own_Float := 0.0;
  begin
     -- your own division operation;
     return Result;
  end Divide;
  
  function Is_Equal  (L, R : Own_Float) return Boolean is
     Result : Boolean := False;
  begin
     -- your own equality operator
     return Result;
  end Is_Equal;
  package Math_Ops is new Numeric_Model (Number => Own_Float,
                                         "=" => Is_Equal,
                                         "/" => Divide);
  package Own_Math is new Number_Operations (Number_Ops => Math_Ops);
  use type Own_Math.Real;
  Data : Own_Math.Real := Own_Math.Zero;
  Left, Right : Own_Math.Real := Own_Math.Zero;
begin

  Data := Left / Right;
  Data := Left * Right;
  if Left = Right then
    null;
  end if; 
end Test_Own_Float_Operations;

You will observe that we have two instantiations, one for the 
"generic formal model" and another for the actual generic package.
The model is instantiated with the operations.  The generic package
is instantiated with an instance of the model.  A more interesting
case of this would be one where the generic package has multiple
generic formal package parameters.  This latter case would suggest the 
potential for generic reusable frameworks, a larger idea than that
used in Ada 83.  It makes Ada every bit as powerful as the corresponding
capability in C++, but safer, much safer.

This has already been a long posting, and I apologize for that.  However,
the discussion originally centered around the notion of type and view
conversions.  Therefore, the following package body demonstrates at least
one example of how the type conversions become useful in the actual
implementation.  

with Ada.Numerics.Generic_Elementary_Functions;
with Ada.Text_IO;
package body Number_Operations is
  package GEF is new Ada.
                     Numerics.
                     Generic_Elementary_Functions
                          (Float_Type => Real);
  package FIO is new Ada.Text_IO.Float_IO(Num => Real);
  F_Zero : constant := 0.0;
  function Zero return Real is
  begin
     return F_Zero;
  end Zero;
  
  function "+"  (L, R : Real) return Real is
  begin
     return Real(Number_Ops."+"(Number_Ops.Number(L),
                                Number_Ops.Number(R)));
  end "+" ;

  function "-"  (L, R : Real) return Real is
  begin
     return Real(Number_Ops."-"(Number_Ops.Number(L),
                                Number_Ops.Number(R)));
  end "-" ;
  function "*"  (L, R : Real) return Real is
  begin
     return Real(Number_Ops."*"(Number_Ops.Number(L),
                                Number_Ops.Number(R)));
  end "*" ;

  function "/"  (L, R : Real) return Real is
  begin
     if R = F_Zero then
        raise Divide_By_Zero;
     else
       return Real(Number_Ops."/"(Number_Ops.Number(L),
                                  Number_Ops.Number(R)));
     end if;
  end "/" ;


  function "="  (L, R : Real) return Boolean is
  begin
     return Number_Ops."="(Number_Ops.Number(L),
                                Number_Ops.Number(R));
  end "=" ;
  
  function ">"  (L, R : Real) return Boolean is
  begin
       return Number_Ops.">"(Number_Ops.Number(L),
                             Number_Ops.Number(R));
  end ">" ;

  function "<"  (L, R : Real) return Boolean is
  begin
       return Number_Ops."<"(Number_Ops.Number(L),
                             Number_Ops.Number(R));
  end "<" ;

  function ">=" (L, R : Real) return Boolean is
  begin
       return Number_Ops.">="(Number_Ops.Number(L),
                              Number_Ops.Number(R));
  end ">=" ;

  function "<=" (L, R : Real) return Boolean is
  begin
     return Number_Ops."<="(Number_Ops.Number(L),
                            Number_Ops.Number(R));
  end "<=" ;

  function SQRT (V    : Real) return Real is
  begin
     return GEF.SQRT(V);
  end SQRT;
  
  function Machine_Number (V : Real) return Real is
  begin
     return Real'Machine(V); -- renaming does not work for this
  end Machine_Number;
                              
  function Get return Real is
     Result : Real;
  begin
     FIO.Get(Result);
     return Result;
  end Get;
  
  procedure Put (V : Real; Fore, Aft : Natural) is
  begin
     FIO.Put(V, Fore => Fore, Aft => Aft);
  end Put;


end Number_Operations;

I hope this helps a little bit in understanding generic formal 
package parameters and some of the issues related to type conversion,
where and when.

Contributed by: Richard Riehle
Contributed on: May 14, 1999
License: Public Domain
Back