Learning Ada 2: more on packages, naming conventions, and bits of types

in #ada-lang4 years ago

Packages are very importat to organize your project, like modules in other languages, and they are also useful to “encapsulate data” — you don't need to go object oriented in order to benefit from that.

On package specs and body again

Reading the previous article you could have deduced the idea that for each specification files (.ads) you need a body file (.adb).

It isn't so.

In fact the specification contains whatever you want to make it usable from the users of your package, and the body contains the rest, and the implementations. In case when there's such a thing.

But you can use a package just as a container of string constants, for example.

package Greetings is
   Hello        : constant String := "hello";
   Hi           : constant String := "hi";
   Good_Morning : constant String := "good morning";
   What_s_Up    : constant String := "what's up";
end Greetings;

That's it. No .adb file.

In Greetings we want some behaviour: we want to add “greeting swears”, but only if you are an adult. An adult is a person, hence we have a Person package — only specs (short for specifications file) needed, person.ads.

package Person is
   subtype Age is Natural range 0 .. 140;
   
   Adult_Age : constant Age := 18;
end Person;

I am going to say something about subtype Age is Natural range 0 .. 140 later; for now, just consider it a type of variable which is useful to hold a number which represents the age of a person.

Our original greetings.ads grows like this:

with Person;

package Greetings is
   
   Unknown_Greeting : exception;

   -- ...
   
   procedure Swear (Greeting    : String;
                    Speaker_Age : Person.Age := Person.Age'First);
   
private
   
   Swear_Hello    : constant String := "fucking hello";
   Swear_Hi       : constant String := "fuck you";
   Swear_Morning  : constant String := "good fucking morning";
   Swear_Whats_Up : constant String := "what's the fuck";
   
end Greetings;

All the swearing greetings are private: only the procecure Swear can access them and decide to swear or not according to the age. The Speaker_Age is a default parameter: if omitted, it takes the value of the first valid value of the range of the type Age, i.e., 0.

Now the package Greetings must have a body, because we need to “complete” the specs with the implementation of Swear:

with Ada.Text_IO; use Ada.Text_IO;

package body Greetings is
   
   procedure Swear (Greeting    : String;
                    Speaker_Age : Person.Age := Person.Age'First) is
   begin
      if Speaker_Age >= Person.Adult_Age then
         Put_Line ((if Greeting = Hello then Swear_Hello
                    elsif Greeting = Hi then Swear_Hi
                    elsif Greeting = Good_Morning then Swear_Morning
                    elsif Greeting = What_s_Up then Swear_Whats_Up
                    else raise Unknown_Greeting));
      else
         Put_Line (Hello);
      end if;
   end Swear;
   
end Greetings;

The if as an expression is an Ada 2012 feature, so you need a compiler that understands Ada 2012.

If the greeting is said by an adult, then it becomes a swearing greeting, but only if it matches one of the known greetings; otherwise an exception is raised. For example

   Greetings.Swear (Greetings.Hello, 20);

works, but

   Greetings.Swear ("good evening", 20);

raises.

You can also see that in Greeter we have

with Ada.Text_IO; use Ada.Text_IO;

because in fact we use Put_Line. We need it even if the Greetings package had a with Ada.Text_IO line. Compare with C (and similarly C++):

/* in file greetings.h */
#include <stdio.h>
/* in file greeter.c */
#include "greetings.h"

In greeter.c we can call printf(), no need to include stdio.h again, in fact is already included by greetings.h. This is because C is “file based” and we are really including the file (a work done by the preprocessor).

In Ada, even if packages are inside files (and there are rules, at least in GNAT, to name correctly a file according to the package it contains), the “logical unit” is the package, and with Xxx doesn't mean “include the file Xxx.ads” but “this unit is using the feature of the package Xxx” (then, thanks to naming conventions, the compiler knows where to look for the package Xxx).

I have to say that C and C++ are behind other languages with respect to modularity. In C and C++ the preprocessor instruction #include literally includes the file. Maybe future C++ standard will change this according to existing proposals, but so far C/C++ programmers haven't anything like packages in Ada (or Java, or other languages).

Variables inside a package

Already seen the Y (and X to test symbol clashes) in the Greeter package of the previous article. Now, one can wonder if in each unit using the package, the Y is the same or is different each time. The answer is it's the very same thing, but let's check it in practice using something we have in this lesson: the package Person:

package Person is
   subtype Age is Natural range 0 .. 140;
   
   Adult_Age : constant Age := 18;
   
   Unit_Age : Age;
end Person;

Then, let's use this package in two different packages, A and B, each with a procedure which prints and then modifies Unit_Age's value.

-- a.ads
package A is
   procedure Print_Modify;
end A;
-- b.ads
package B is
   procedure Print_Modify;
end B;

You compile test_pkg.adb; the output of a run is

Test_Pkg:  0
          0
Test_Pkg:  10
         10
Test_Pkg:  20

The private part to complete a type

In a package specs you have a “public” part and a private part, but when you define a new type maybe you don't want the client (i.e., the user of the package) to see and access the details of the type — it is a sort of opaque type. But then, they can't use it in anyway: it would be unuseful, except if the package also give subprograms to manipulate the type. E.g.:

with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;

package Person2 is
   subtype Age_Type is Natural range 0 .. 140;
   
   type Person is private;
   
   procedure Set_Name (A_Person :    out Person;
                       Name     : in     Unbounded_String);
   procedure Set_Age  (A_Person :    out Person;
                       Her_Age  : in     Age_Type);
   
   function Get_Name (A_Person : Person) return Unbounded_String;
   function Get_Age  (A_Person : Person) return Age_Type;
   
   function Is_Adult (A_Person : Person) return Boolean;
   
private
   Adult_Age : constant Age_Type := 18;

   type Person is 
      record
         Name : Unbounded_String;
         Age  : Age_Type;
      end record;
   
end Person2;

The first argument is used as output, i.e., it is modified only. If we have

   I_Am_Hugo : Person;

the only way to manipulate this “thing” is to use the functions of the package. The “thing” can be in fact considered as an “object”, even if not yet like in the OO paradigm — but almost.

That is, packages allow for data encapsulation and information hiding.

The complete (boring) example's files are:

Packages in a package

Packages can be nested, both in private or public part.

package N1 is
   C1 : constant := 1;
   
   package N2 is
      type N2_Type is new Integer;
      PC : constant := 2;
   private
      Cnt : Integer;
   end N2;
   
   C2 : N2.N2_Type;
   
private
   
   package N3 is
      PC : constant := 3;
   private
      Cnt : Integer;
   end N3;
   
end N1;

Wherever you do with N1, you can access N1.C1, N1.N2.PC, but not N1.N3.PC (except in the body of the package N1). Also, inside the package there can be reference to other package or declarations in general, provided that they are already seen. For instance C2 can refer to N2 because N2 comes before. If we move C2 above N2, then C2 can't be N2.N2_Type, because N2 isn't seen yet ("N2" undefined).

It seems that there isn't a way to do a forward declaration like in C++.

class ClassB; // forward decl.

class ClassA
{
private:
    ClassB* m_that;
    // ...
};

// ...
class ClassB
{
  // ...
};

Package inside a subprogram

You can put a package also inside the declarative part of a procedure or function. The declarative part of a subprogram can become rather thick, containing types, functions, procedures, variables, constants, and packages too!

The example, thick_hello.adb, is totally meaningless, but shows how a “full” package can be inside the declarative part of a procedure!

The code contains also other interesting things, but they are not package-related, so we'll see them later.

Hierarchies

Instead of nesting packages, we can arrange them in a sort of hierarchy.

package Father is
   -- ...
end Father;
package Father.Son is
   -- ...
end Father.Son;

The child package Son can see everything the package Father has. (There are exceptions, but I haven't encoutered them yet).

It looks like a more useful feature with respect to the nesting.

Currently I can't think of a meaningful example, though. A not-so-meaningful example is Father.Son. This example is more interesting from the point of view of the types and the inherited operations, indeed. I consider this aspects in the next section.

The example just shows that Father.Son sees Father, no need for with Father inside the package Father.Son: we use Father_Type without problem, and also the Father's private procedure Father_What, as if it were part of the private part of the child package Father.Son.

Naming conventions

The Father - Father.Son example shows also a simple naming convention: you maybe have already deduced that if you have the package This_Is_A_Package, the file which keeps it has name this_is_a_package.ads (or .adb). Now you can deduce that the dot becomes a -; so the child package Father.Son is inside the file father-son.ads (and father-son.adb).

These rules are easy.

There's an exception: the - becomes ~ when the previous and only letter, the first one of the file name, is one of a, g, i, s, because these are used for standard packages.

So the specs for the child package I.A.Robot is in a file named i~a-robot.ads.

If you want to use non-conventional file names, GNAT offers a pragma which tells where to find the code for a certain package; this pragma must be put inside a configuration file (gnat.adc). I haven't experimented yet with this, so I'll say something more when I will do.

A little bit of a type

Ada is very type oriented, so to say; its type system is a huge (and important) part of it, because types establish the domain of your problem… Thus, another thing you likely will put into your specifications file are a bunch a “custom” type definitions, according to the specific problem you are trying to “model”.

For example,

   subtype Age is Natural'Base range 0 .. 140;

Here we are saying that Age is a subtype of the base type of Natural with a restricted range of values, between 0 (newborn) and 140 (a very old person). Of course this age is good for human beings — unless they are biblical human beings! —, not for trees…

The type Natural is itself a subtype of Integer, defined like this:

subtype Natural  is Integer range 0 .. Integer'Last;

This is why I think the 'Base attribute isn't necessary:

subtype Age is Integer range 0 .. 140;
  -- or
subtype Age is Natural range 0 .. 140;

All these three subtypes work (that is, the compiler compiles without complaints), but I am not sure which is the more “correct”. If I had to pick one, I'd say Natural range 0 .. 140, and that's why: negative ages are nonsense, so the age is a natural number; then unlikely it goes towards infinite, or any very huge value; hence we are restricting the range from 0 .. Integer'Last to 0 .. 140. If a woman reaches 141, our system fails…!

There are differences I haven't studied yet deeply. One of these is about the operations you can do on your type, what is inherited from the parent type e what's not.

There's another interesting fact I've stumbled upon when writing the example Thick_Hello: operations on a type defined in a package should be “qualified”, unless you add use My_Pack. This we've already seen, but now consider when your type inherit basic operations of another type.

   package Counter is
      type C_Type is mod 17;
      -- ...
   end Counter;

If then we have

   T : Counter.C_Type := 0;

can we do inherited operations on T? E.g., can we write

      if T = Counter.Value then
         Counter.Increment;
      end if;

We want to compare T with the value returned by Counter.Value. We can't, because the operations allowed on C_Type are in the package Counter, and we didn't use Counter.

So we should write:

     if Counter."=" (T, Counter.Value) then
         Counter.Increment;
      end if;

But this is super ugly! What can we do if we don't want to write operators in that way, but we also don't want to import all the symbols in Counter?

This is what the following line is for:

   use type Counter.C_Type;

The type C_Type is a “modulus” type; we know better other types of this kind, e.g.

type Byte is mod 2**8;

For such a type a “wrap-around” arithmetic is at work; in fact if we have the byte, here written as a binary number,

11111111

and we add 1, then we obtain 0 (no overflow, since it is how it is meant to work!) In Ada a binary number can be written as 2#10101010#, and if we want to visually separate nibbles, we can write 2#1010_1010#, and this can be done with any number in all accepted bases: a billion can be written as 1_000_000_000!

This “wrap-around arithmetic” maybe it isn't exactly what math guys would call modular arithmetic, but at some extent it is, and I think the keyword mod tells that.

Now, let's see again the Father.Son example. The package Father defines the type Father_Type (and a private procedure on it). The child package Father.Son defines a subtype of Father_Type, Son_Type, and it also pretends to provide for the operator + when the left operand is Son_Type and the right operand is Father_Type (and the result is a Father_Type, too).

If you compile and run the example test_children.adb the output will be:

using this
 116
 105

Despite the fact that we did use type Father.Son.Son_Type, the + operator is given by the one for the base type of Father_Type, that is, Integer.

On the other hand, Father.Son has also the type Sonic_Type; it can hold the same values as Son_Type, but it's a type. The operation + for Sonic_Type in Father.Son can be used, but we need

   use type Father.Son.Sonic_Type;

to avoid the ugly syntax. We also need, for the same reason

   use type Father.Father_Type;

What does it happen if we also say

use type Father.Son.Son_Type;

? We obtain a warning which explains us again an important difference between type (deriving) and subtype (subtyping).

warning: "Father_Type" is already use-visible through previous use type clause

Both use type Father.Son.Son_Type and use type Father.Father_Type make visible the operations for the same type.

Sonic_Type isn't a subtype, hence it doesn't inherit from its base; instead you can provide your own operation, and it gets called as expected:

-- ...
   B : Father.Father_Type := 100;
   C : constant Father.Son.Sonic_Type := 20;
-- ...
   R : Integer;
-- ...
   R := C + B;

This calls Father.Son."+" (Left : Sonic_Type; Right : Father_Type). But B + C (Father_Type + Sonic_Type) won't work, because it's an operation on two types, and neither Father nor Father.Son defines it — by the way, only Father.Son can define it: a child package can see the parent, but the parent can't see the child, therefore there's no way it can declare and implement any subprogram which needs to see Father.Son.Sonic_Type!

Incidentally, we've also seen that overloading works. Overloading is another feature typical of OO languages.

As said, the type system is huge, and also powerful and a great tool for safer software, provided you use it correctly: e.g., if you are writing a simulation of a car and you use the Float type for the speed, that's fine, but you are not taking advantage of the Ada's typing system. I bet your speed absolute value will be less than the speed of light, to start with!

The topic is large and will be explored in future articles.

Initialization of a package

A package can have a initialization part:

package body My_Pack is
   
   Internal_Value : Integer;

   -- ...
   
begin
   Internal_Value := 25;
end My_Pack;

Don't forget that when with-ed in two places, you haven't a copy: it's the same package, the same variables: we call Suppack.Print, which calls My_Pack.Get and prints the returned value, and this is 100 after My_Pack.Set (100). It works like a singleton. We also see that the initialization is called once.

Separate!

Sometimes you have carved your nice package, you are implementing its body, but you don't want to stuff everything into it, maybe some procedure is really long and complex, or you would like to put it in a separate file just to better keep an eye on it.

You can.

In your body, you'll say that a certain procedure or function is separate.

package body Separated is
   procedure Watch_Me is separate;
end Separated;

The file where you put the actual implementation must be named like package-procedure.adb, the obvious way, so to say, since you would call Watch_Me like this: Separated.Watch_Me, and I've already talked about naming convention: dots replaced by -, and lowercase (important on systems which distinguish lowercase and uppercase in file names).

Thus in separated-watch_me.adb you put the implementation:

with Ada.Text_IO; use Ada.Text_IO;
separate (Separated)
procedure Watch_Me is
begin
   Put_Line ("Better keep me separated");
end Watch_Me;

See separated.ads, separated.adb, and separated-watch_me.adb.

Conclusion

Having the code pasted into pastebin is good, except that I've read that they can delete the post if it hasn't accesses for a while (unless you pay for it). And the same is true for hastebin. This is why I've opened a github account and created the repository learning-ada.

Sources for this article are here.



Coin Marketplace

STEEM 0.22
TRX 0.06
JST 0.025
BTC 19457.71
ETH 1331.37
USDT 1.00
SBD 2.45