Tuesday, August 28, 2012

AdaTutor - Subprograms and Packages (1)

Procedures and Functions

When we compiled procedures Hello and Add, we made it possible for other units to with them and call them.  We can also compile just a specification, supplying the body later.  For example, we could compile

   function English_Upper_Case(S : in String) return String;
and then compile the calling program.  When we later write the body, it must agree with the specification:
   function English_Upper_Case(S : in String) return String is
      Answer : String(S'Range) := S;
   begin
      for I in S'Range loop
         if S(I) in 'a' .. 'z' then
            Answer(I) := Character'Val(Character'Pos(S(I)) - 32);
         end if;
      end loop;
      return Answer;
   end English_Upper_Case;

Functions and procedures may also be declared locally.  In Ada 83, such declarations must follow any simple declarations like S : String(1 .. 5);.

   with Ada.Text_IO; use Ada.Text_IO;
   procedure Greeting is
     S : String(1 .. 5) := "Hello";
     function English_Upper_Case(S : in String) return String is
       Answer : String(S'Range) := S;
     begin
       for I in S'Range loop
         if S(I) in 'a' .. 'z' then
           Answer(I) := Character'Val(Character'Pos(S(I)) - 32);
         end if;
       end loop;
      return Answer;
     end English_Upper_Case;
   begin
     Put_Line(English_Upper_Case(S));
   end Greeting;

It's usually better to declare local functions and procedures to be separate.  These subprograms can in turn declare separate subprograms, to any depth:

   procedure Main is
   function A return Float is separate;
   begin
     ...
   end Main;

   separate (Main)
   function A return Float is
      procedure B is separate;
   begin
     ...
   end A;

   separate (Main.A)
   procedure B is
      procedure C(I : in Integer) is separate;
   begin
     ...
   end B;

   separate (Main.A.B)
   procedure C(I : in Integer) is ... (etc.)

However, is separate may be used only at the outermost level.  The example below is legal as it stands, but we may not make procedure B separate unless we also make function A separate, thus bringing function A to the outermost level.

procedure Main is
   F : Float;
   function A return Float is
      Answer : Float;
      procedure B is
      begin
         ...
      end B;
   begin
      ...
      return Answer;
   end A;
begin
  ...
end Main;

A program that begins separate (...) must be compiled after the program that says is separate, because a subprogram depends on its "parent."

In Ada 95, Hierarchial Libraries provide an alternative to separate subprograms.  We'll learn about Hierarchial Libraries a little later.  Also, the function To_Upper in the Ada 95 package Ada.Characters.Handling is better than our function English_Upper_Case, because it handles accented letters.  We presented our function only to give a simple example that will work in both Ada 83 and Ada 95.

A procedure or function specification gives the name, mode, and type of each parameter.  A function specification also gives the type of the result.  The mode of the parameters can be in, out, or in out.  The subprogram may read from, but not write to, in parameters, and it may write to, but not read from, out parameters. (In Ada 95, it may read out parameters.)  If the mode is omitted, it's assumed to be in.  Thus, these two lines have the same effect:

   procedure Hanoi(N : in Natural; From, To, Spare : in Character);
   procedure Hanoi(N : Natural;    From, To, Spare : Character);

The parameters of a function must always be of mode in, never out or in out.

Note that when several parameters have the same mode and type, they can be placed in one list, separated by commas.  The lists themselves are separated by semicolons.  The two specifications above are equivalent to:

   procedure Hanoi(N : Natural; From: Character; To: Character;
                   Spare: Character);

In any event, the parameters in a call to a procedure or function are always separated by commas:

   Hanoi(5, 'A', 'B', 'C');

Question

  1. procedure P(A; B; C : in Integer);
  2. procedure P(A, B, C : Integer; D, E : in out Float) return Character;
  3. function F(A, B, C : Integer; D, E : in out Float) return Character;
  4. procedure P(A, B, C : Integer; D, E : in out Float);
Which one of the above is legal?

Recall that type conversion from Float to Integer in Ada rounds to the nearest integer.  (The Ada 83 standard doesn't specify which way rounding occurs when the fractional part is exactly 0.5; the Ada 95 standard specifies rounding away from zero in such cases.)  This function Floor computes the largest integer less than or equal to a given Float, always rounding toward negative infinity:

   function Floor(X : in Float) return Integer is
      Answer : Integer := Integer(X);
   begin
      if Float(Answer) > X then
         Answer := Answer - 1;
      end if;
      return Answer;
   end Floor;
Similarly, this function Truncate converts from Float to Integer, always truncating toward zero:
   function Truncate(X : in Float) return Integer is
      Answer : Integer := Integer(X);
   begin
      if Answer > 0 and Float(Answer) > X then
         Answer := Answer - 1;
      elsif Answer < 0 and Float(Answer) < X then
         Answer := Answer + 1;
      end if;
      return Answer;
   end Truncate;

In Ada 95, the attributes 'Floor, 'Ceiling, and 'Truncation, available for any floating point type, are better than the above functions.  If X is of type Float, then Float'Floor(X) is the largest integer <= X, Float'Ceiling(X) is the smallest integer >= X, and Float'Truncation always truncates toward zero.

Default Parameters

The in parameters of a subprogram specification may be given default values.  For example, here's a simplified version of the specification for the procedure Put in Ada.Integer_Text_IO (the actual specification can be found in Annex A.10.1 of the Ada 95 RM):

    procedure Put(Item  : in Integer;
                  Width : in Integer := 6;
                  Base  : in Integer := 10);

This means that, in calls to Put, the Width and Base parameters are optional.  If Width is omitted, it's assumed to be 6, and if Base is omitted, it's assumed to be 10.  If either of these parameters is given in the call, the default value is overridden.  (The default value for Width is shown here as 6.  Actually, it depends on the implementation of Ada.  Of course, the default value for Base is always 10.)

Default parameters let us make our Ada subprograms both flexible and easy to use.  In other languages, we'd often have to choose between these two qualities.  For example, suppose J is an integer. Here are some calls to Put:

 Put(J);
 Put(J, Width => 4);
 Put(J, Base => 16);
 Put(J, Base => 16, Width => 4);

The first parameter in each call could have been given as Item => J, but everyone remembers that the first parameter of Put is the item, so named notation seems unnecessary for this parameter.  However, Width and Base are used less frequently.  We used named notation for these parameters so the reader of our code wouldn't have to remember which parameter comes second and which comes third.  Note that if we omit the second parameter and specify the third, we must use named notation for the third parameter; we're not allowed to write Put(J, ,16); as in some languages.

If we were writing Put in another language, we'd have to choose either making the user specify the width and the base in every call (giving flexibility), or writing Put with only one parameter (giving ease of use).  In Ada, default parameters give us both flexibility and ease of use!

Ada.Text_IO.New_Line has one parameter, Spacing, defaulted to 1.  Thus, we can call New_Line; to get one CR-LF, or, for example, New_Line(3); to get three.

Default values may also be given in record definitions.  If we write

 type Month_Type is (Jan, Feb, Mar, Apr, May, Jun,
         Jul, Aug, Sep, Oct, Nov, Dec);
 subtype Day_Subtype is Integer range 1 .. 31;
 type Date is record
    Day   : Day_Subtype;
    Month : Month_Type;
    Year  : Integer := 1776;
 end record;
 USA : Date;
then USA.Year is set to 1776 when the line declaring USA is elaborated.  Every time an object of type Date is declared, its Year field is set to 1776.  However, there's a difference between default values in records and default parameters in subprograms.  We can't write USA := (4, Jul); all fields of a record must be specified in an aggregate.

Question

   procedure Put(Item  : in Integer;
                 Width : in Integer := 11;
                 Base  : in Integer := 10);
Which one of these is legal?
  1. Put(Item => 23, 5, 10);
  2. Put(23, 5)
  3. Put(23; 5; 10);

< prev   next >

No comments: