Learning Ada 7: generics and (static) overloading

in #ada-lang6 years ago

007-title.png

Generics

Ada has generics. I don't know about Ada of the ancient time, and I don't care — as programmer on the edge of exploration, I don't care about old revisions of a language, unless I have to maintain old code bases. (For example I've been stuck with C++03 and couldn't use C++17 features in my everyday job code.)

Other languages has generics in these modern days: Java, C++ (templates), and several others.

Syntax differences apart, what's peculiar in Ada is that Ada tends to need the programmer to be explicit in many thing: most of the time he has to express in plain sight what he wants to do and how.

So, Ada generics must be instantiated explicitly and you need to assign a name to them.

You can do generic packages as well as generic functions and procedures.

Formal parameters for the generic are typed, of course, and can be specified so that the client (the programmer using the package, procedure or function) must use the proper type.

Generic subprograms and packages

Let's roll an example:

   generic
      type T is (<>);
      N : in T;
      with function "*" (A, B : T) return T is <>;
   function Multiplier (X : T) return T;

What does it mean?

We are declaring a generic function Multiplier, which takes a single parameter of type T and return something of type T again.

The type T is

type T is (<>);

Which simply means that T can be any discrete type. Discrete types are integers, modular types, and enumeration. In our little exploration of Ada's types I've mentioned modular types (let's call the Ada way into unsigned integers), but didn't show an actual examples, and enumerations too.

Let's recall these last two.

An example of modular type could be a byte. In Ada you can define the Byte type as follow:

type Byte is mod 256;

In modular arithmetic, the last value plus one is the first value. It is like a clock.

An enumeration is, well, an enumeration. We've seen the Character type. Its syntax is fairly simple:

type Month is (January, February, March, ...);

Given a certain value of an enumeration, we go to the next as shown below:

Enumeration_Type'Succ (Enum_Variable);

Using the attribute Prec we go to the previous value.

Back to the generics.

When we'll instantiate the generic function, will use one of these kind as argument, because it is how the generic function is declared.

Next line of the generic specification is

N : in T;

This generic parameter is called N, is of type T, and looks like in parameter — we also say that it is, in fact, an in parameter.

The last line of the generic specification is

with function "*" (A, B : T) return T is <>;

We can pass functions to a generic. In this case we call this function as the binary operator *: it takes two T in input and spits out another T. The final is <> is quite interesting. It means roughly this: if you don't specify the function when you instantiate the generic, then look for a default for * given in the scope of where the generic has been instantiated.

The function's implementation will be:

   function Multiplier (X : T) return T is
   begin
      return N * X;
   end;

Now let's try to instantiate this function:

   function Double_It is new Multiplier (Integer, 2);

This is how we instantiate the generic Multiplier, so that T is an Integer and N is 2. We don't give a function * and the surrounding scope has already one (because in fact we can alraedy multiply two integers…)

Therefore we can use the Double_It function like this:

 R : Integer := Double_It (10);

The variable R will hold 20.

If we try to instantiate the function with an incompatible type, we obtain a compile time error. For example, if we write

function Double_It is new Multiplier (Float, 2.0);

the compiler stops and says: expect discrete type in instantiation of "T", instantiation abandoned.

Let's try with this instead:

   type Num is (Zero, One, Two, Three, Four, Five, Six);
   function Double_It is new Multiplier (Num, Two);

There isn't any problem with the type and the value Two, but unfortunately the surrounding scope doesn't have the function * for that type.

We can write one, like this (it must be before the instantiation):

   function "*" (A, B : Num) return Num is
   begin
      if Num'Pos (A) * Num'Pos (B) <= Num'Pos (Six) then
         return Num'Val (Num'Pos (A) * Num'Pos (B));
      else
         return Six;
      end if;
   end;

Given an enumeration type E, E'Pos (NAME) gives the number corrisponding to the “position” of the value NAME (for Num, Zero has position zero, for example), and E'Val (N) is the opposite: it gives the value at position N.

We don't need to use the operator as the name of the function. The following works perfectly:

   function Star_Op (A, B : Integer) return Integer is
   begin
      return A * (B + 1);
   end Star_Op;
   
   function Strangeness is new Multiplier (Integer, 1, Star_Op);

It is a good idea to use meaningful names for the generic arguments, and use them when instantiating. This clarifies the use especially when the generic arguments are more than one or two.

In the given example, names are too short, likely, to be qualified as acceptable. Anyway, our instantiation can be written like this:

   function Strangeness is new Multiplier (T   => Integer, 
                                           N   => 6, 
                                           "*" => Star_Op);

In the generic function specification we should have use something more like Value_Type instead of T, and Multiplication_Factor instead of N.

The following table shows several kind of formal parameters for generics:

typeexplanation
type T is privateAny copiable, definite type, also it doesn't need an initial value; most of the time you want to use this
type T is (<>)Any discrete type: integer, modular, enumeration
type T is range <>Any signed integer
type T is mod <>Any modular type
type T is delta <>Any non-decimal fixed-point type
type T is delta <> digits <>Any decimal fixed-point type
type T is digits <>Any floating point type
type T (<>) is privateAny copiable type, definite or indefinite
type T (<>) is limited privateAny type, copiable or not, definite or indefinite
type T (<>) is tagged privateAny copiable, non-abstract tagged type
type T (<>) is new ParentAny non-abstract type derived from Parent.
type T (<>) is new Parent with privateAny non-abstract type derived from tagged type Parent
type T (<>) is tagged limited privateAny non-abstract tagged type, copiable or not
type T (<>) is abstract tagged privateAny copiable tagged type, abstract or non-abstract
type T (<>) is abstract tagged limited privateAny tagged type, abstract or not, copiable or not
type T is array (I) of EAny array type with index type I and elements' type E.

ada-formal-type-generics.png

About types, we have just scratched the surface with a previous lesson. These lessons aren't meant to cover every details — for those, you need a full-featured tutorial, or dig into the sites promoting Ada, where they also list resources and you can also download the standard — not an easy reading, but everything's there!

Here a quick, not necessarily exact, tour to make sense of the table above.

Think of <> (the diamond) as whatever.

The keyword limited means a type can't be copied (you can't do A := B), but in the context of a generic it means it can or it can't. We've seen it in the lesson on the OO.

The tagged type in Ada are those type on which the object orientation is built on. See the fifth lesson. In there I've also covered the abstract part. In the generic, it means the type can be abstract, not that it must be abstract.

The rest is mostly syntax. For example private means just that the generic doesn't know other details about the type, just those expressed by the rest of the clause.

In the Ada 2012 Reference Manual, chapter 12 (Generic Units), you can read other examples.

We can learn also a little bit of nomenclature: things like our N are called formal objects; when we have a type, it's called formal type — it's easy; the functions, or procedures, are called formal subprograms. When we “create” a specific instance of a generic package or subprogram, that's called instantiation, as you have already deduced.

The types or subprograms or object of a specific instantiation are called the actuals. Thus, in our Double_It, Integer and 2 are the actuals.

Standard generic package

A typical usage of generics is in the containers of the Ada standard library. In these cases we have a generic package. For example Ada.Containers.Doubly_Linked_Lists. And we instantiate it like this:

package Ints is new Ada.Containers.Doubly_Linked_Lists (Integer);

Now we can have lists of integers:

A_List : Ints.List;

Don't forget that Ints it's an instance of a (generic) package, so it is, indeed, a package, not a type. That's why we need to write Ints.List.

Here it is the specifications of the generic package for the doubly linked lists.

Writing a generic package isn't very much different than writing a generic subprogram. You start with generic, followed by your formal types and objects and subprograms, then you can write the package as usual.

generic
   type Element_Type is private;
package Elements_List is
   -- ...
end;

This could be how a custom container package begins.

Another usage is shown for the IO subprograms. We have Ada.Text_IO, but it's Put works just for Strings. What about integers?

Here how it can be done in code:

   package Int_IO is new Ada.Text_IO.Integer_IO (Integer);
   use Int_IO;

Since that lines, the subprogram Put (and others) is overloaded so that it accept an Integer as input.

It is a very common need, so there are already these package that you can with (and use) when you need to output integer or others as text:

with Ada.Integer_Text_IO; -- integer types
with Ada.Float_Text_IO;   -- float types

If you don't use use, you have to write Ada.Integer_Text_IO.Put; so, likely you will use it somewhere.

Both

use Ada.Integer_Text_IO;
use Ada.Float_Text_IO;

makes visible a subprogram called Put (among others). That is, now Put is overloaded so that it can accept strings, integers and floats.

Overloading subprograms

Overloading is often presented altogether with object oriented paradigm, but it is a separated concept. Ada had subprogram overloading before it embraced the object oriented paradigm!

Here I talk about overloading outside the OO world. This is something which is done statically, at compile time: there's no need for runtime support. Thus it is different from dynamic dispatching, which works only on the first argument (explicit or implicit), i.e. on the object, to select the correct method of the correct “level” of the hierarchy of classes, and it is done at runtime. This wil be shown better in future lessons.

Basically we can talk of overloading of subprograms when the names of several subprograms are the same, but the types of their parameters are not.

Languages like C++ and Java distinguish functions or methods only by their parameters. Ada does it considering also the return value!

int do_something(int i);
int do_something(double i);

These two C++ functions are different, but have the same name do_something. They return an int, and you can't add the following:

double do_something(int i);

This conflicts with the previous declaration.

Ada thinks differently and the following is good:

   function Do_Something (A : Integer) return Integer;
   function Do_Something (A : Integer) return Float;

Which one will be actually called depends on the context of the call.

This

A : Integer := Do_Something (10);

will call the first version; this

F : Float := Do_Something (10);

will call the second version, because it's the one matching the type of the returned value.

You must pay attention to subtypes, though.

   subtype Alfa_Type is Integer range -10 .. 100;
   subtype Beta_Type is Integer range -101 .. -11;
   
   function Do_Something (A : Alfa_Type) return Beta_Type is
   begin
      Put_Line ("Alfa_Type returns Beta_Type");
      return (if A <= 0 then A - 11 else -A);
   end;

This gives error at compile time (duplicate body), because Alfa_Type and Beta_Type are, from the point of view of the type system, integers (i.e., they are of type Integer, even if range restricted), and we have already covered that case!

If we create a brand new type, instead, the we can do it:

   type Alfa_Type is new Integer range -10 .. 100;
   type Beta_Type is new Integer range -101 .. -11;
   
   function Do_Something (A : Alfa_Type) return Beta_Type is
   begin
      Put_Line ("Alfa_Type returns Beta_Type");
      return Beta_Type (if A <= 0 then A - 11 else -A);
   end;

I've changed subtype ... Integer ... in type ... new Integer, and then we need an explicit conversion in the return statement. Now we can play with overloading Do_Something with the combination of Alfa_Type and Beta_Type and anything else!

Procedure can be overloaded as well, of course.

   type Salute_Type is (Hello, Bye);
   type Work_Type   is (Light, Heavy, Routine);
   
   procedure Do_Action (A : Salute_Type); -- (A)
   procedure Do_Action (A : Work_Type);   -- (B)
   
   -- ... implementation ...
   
   -- ... use example ...
   Do_Action (Hello); -- call (A)
   Do_Action (Heavy); -- call (B)

Code

As usual I am putting some test code on github.

For this lesson, take a look at 007.

  • dll.adb: example of use of the standard generic package Doubly_Linked_Lists. It also shows the syntax for E of L for iterating over the elements of a container, and alternate use of the dot syntax already described in lesson 5 in A matter of syntax.
  • generics.adb: playing with a generic function, overloading of instances of a generic function, instantiation of generic package, and you can learn also something about enumeration
  • overlo.adb: properly on overloading of subprograms outside the OO world (static overloading), addressing the quirk with subtypes, and showing that the overloading is done considering the return type too.

Previous lessons

In order to make it easier to track back to older lessons, here a list of the current lessons you can read.

Useful links