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.
|