Troubleshooters.Com and Code Corner and Ada Advice Present:

Ada 101

See the Troubleshooters.Com Bookstore.

Semester Equivalent:

This web page is the equivalent of a first semester Ada language course, which is why it's so long. With this page you can learn the basics of the Ada language in a week or two instead of an entire semester.

CONTENTS:

Introduction

When driving a stick shift car, the hardest part is getting up to five miles an hour. With that under your belt, the rest is fairly easy to learn. In the same way, with any language including Ada, you learn how to compile, output to the screen, use loops, if statements, functions and procedures and access command line arguments. From there incremental learning is reasonably easy. This document provides you these basics.

There's a reason that this document is long. It's basically a stand-alone substitute for a first semester Ada class. You can't expect to read it for four hours and be able to write Ada programs. I suggest reading it for a couple hours at a time, trying out all the code and making your own modifications to it. Doing it this way you can become a proficient Ada beginner in a whole lot less than a semester, and you'll be well poised to strike out on your own to learn more Ada.

The information in this document is based on working code I tested myself, using the GNAT compiler. If you're compiling with something other than GNAT, some of the code in this document won't compile. As one example, some Ada compilers don't have support for unbounded strings, which is used heavily in this document. If possible, I suggest you obtain the GNAT compiler for use with the code in this document, and then, once your learning is complete, you can go back to your normal compiler and adapt your code. Or perhaps you can just continue using GNAT: GNAT's pretty good.

Hello World

"Hello World" programs are intended to showcase the simplest possible program in a language, and show how to run it. The following is a Hello World program written in Ada.

with Ada.Text_IO;

procedure Hello is
begin
    Ada.Text_IO.Put_Line("Hello World!");
end Hello;

An explanation follows:

To compile this program, assuming its filename is hello.adb, perform the following command:

gnatmake hello.adb

The preceding command should issue no errors or warnings, and should create an executable file called hello. Otherwise, troubleshoot.

Note:

The gnatmake command is for the Gnu Ada compiler available on Linux machines. If for some reason the Gnu Ada compiler isn't available, you'll need a different compiler, which will have a different syntax.

So now that you have your hello executable, run it with the command ./hello. This prints the string "Hello World!" to the screen. That's it. You've written, compiled and run your first Ada program.

Note:

The .adb extension stands for "ADa Body", and is a special extension that makes life easier. Unless you have a very compelling reason to do otherwise, use extension .adb on your Ada program files.

Note:

Like many other languages, the end statement matches the begin statement within a procedure or function. Unlike most other languages, the end statement must include the name of the procedure or function whose begin it is ending. This compiler enforced commenting makes matching much more intuitive and prevents many careless mistakes.

Utilizing Predefined Ada Packages

Did you see me use the pretentious word "utilize" instead of "use"? I did this because in Ada use is a keyword to do something you very well might not want to do. This is discussed later in this section.

Every language has its standard library. Some are better curated than others. C is very well curated. Python is even better curated: There are standard library constructs for almost anything you want to do, and they're all well tested and work well.

Ada is another language with a very well curated standard library. C, Python and Ada all have hierarchical standard libraries, with Ada the most hierarchical, Python the next and C standard libraries are rarely hierarchical.

To use C's Standard IO standard library, insert the following code near the top:

#include<stdio.h>

The preceding code brings into scope every public variable and function from the Standard IO library without the need to identify where each specific variable or function came from. This can lead to deterioration of the namespace causing subtle errors. There are ways to get around this, but the default behavior is to make everything publicly declared in the Standard IO library to come into the current namespace.

Python is a little cleaner, in my opinion. Consider the following three line Python program, which prints "4.0":

#!/usr/bin/python
import math
print(math.sqrt(16));

In the preceding notice that even though you imported the math library, you still need to preface sqrt with math.. If you had not done this, the program would have aborted with a NameError exception.

Let's say for some reason you don't want to use the math. prefix. Consider the following program:

#!/usr/bin/python
from math import *
print(sqrt(16));

The preceding lets you call math() as if it's defined in the current file. Pretty much like you'd use C's #include facility. Matter of fact, you can use any publicly declared thing in the math package. That's a lot of stuff you're bringing into the current namespace, and if you're anything like me you don't even know everything that's in there. So I'm not a fan.

Note:

There's also the syntax from math import sqrt, which imports only the sqrt() function. I find this more acceptable, but if you need to import a lot of things from a library it's tedious, and it still has the disadvantage that it's not obvious which library is supplying sqrt().

Ada

Ada calls its libraries packages. Ada's reason for existence is to write safe code, and in many cases huge code by many different authors. To facilitate these goals, the Ada culture names things with phrases constructed from whole words, not abbreviations. Consider the following extremely long line of code, which does something often needed:

Ada.Strings.Fixed.Move(Ada.Strings.Fixed.Trim(St1, Ada.Strings.Right) & Ada.Strings.Fixed.Trim(St2, Ada.Strings.Right) & Ada.Strings.Fixed.Trim(St3, Ada.Strings.Right), St4);

Is the preceding ugly enough for you? Maybe back in the days of 132 column greenbar paper (don't worry if you don't know what that is) the preceding would have been acceptable, but lines this long aren't acceptable today. The following are some ways to avoid this ugliness, each with its own advantages and disadvantages:

  1. Utilize the use command to bring everything from the package into the current namespace, eliminating the need for package prefixes. This is good in very short procedures or functions unlikely to grow, although you lose the self-documentation of knowing where things came from.
  2. Break up the line into several lines. This keeps the self-documentation while keeping all lines to a reasonable width. However, it can often be hard to read.
  3. Create your own tiny procedure or function to do the task, and use technique #1 or #2 within this function or procedure.
  4. Create abbreviations for common packages. This keeps the sources of the procedures, functions and other things intact while shortening commands, at the expense of making you remember the abbreviations.

All four of the preceding techniques are used later in this document.

Ada Is All About Strict Typing

Ada is built from the ground up for safety, and a big factor in making it so safe is its ultra-strict typing. To make an analogy, consider the following four fathers:

When starting Ada, I guarantee you'll hate the insanely strict typing. It has no implicit type conversions, and only a few explicit type conversions. Your first few days with Ada, everything you try to do will be blocked by Ada's insanely strict typing, and you'll long for C. But stick with it for awhile and you'll notice that once your program compiles, and runs without runtime exceptions, it usually produces the exact result you want. Stick with it awhile and you'll consider Ada's ultra-strict typing your friend.

Procedures and Functions

Both procedures and functions are subroutines, meaning you call them, and when they're done executing they return control to the statement immediately following the call. Both can take arguments. The difference between procedures and functions is:

  1. Procedures never return a return value.
  2. Functions always return a return value.

Relating to the preceding two facts, you can never set a variable to a procedure because procedures don't return anything, and you can't use a procedure as an argument to another procedure. Similarly, you cannot call a function without using its return to either set a variable or serve as an argument. This helps the programmer guard against doing something stupid and potentially harmful.

Input and Output Arguments

The Procedures and Functions section I mentioned "Procedures never return a return value." This is not exactly true. The reality is a procedure cannot return a value in a function return. However, it can return a value via an output argument. Consider the following program:


Structure Of An Ada Source Code File

This section describes the structure of most Ada source code files. There are exceptions, but these exceptions are relatively unusual. Please keep in mind that Comment lines, which start with a double hyphen (--) can be put absolutely anywhere in the program. They have no effect: From the compiler's point of view they're not there. Also, comments can be placed after an executable line, as shown in the following example:

I := I+5; --Add 5 to I

The following is An Ada source code file (the .ads variety) is built from the top down as follows:

-- vvvvvv Begin withs and uses vvvvvv
with Ada.Text_IO;
use Ada.Text_IO;
-- ^^^^^^  End  withs and uses ^^^^^^

-- Main procedure declaration follows
procedure My_Outer_Procedure is
   -- vvvv Start main procedure declarations vvvv
   subtype Dice_Roll_Type is Integer range 1..6 ;
   Dice_Roll: Dice_Roll_Type;
   First_Word: String := "Hello";
   Second_word: String := "World";
   -- ^^^^  End  main procedure declarations ^^^^
-- Main procedure begin follows
begin
   -- vvvv Start main proc executable stmts vvvv
   Dice_Roll := 3;
   Put(Dice_Roll'image & ": ");
   Put(First_Word);
   Put(" ");
   Put(Second_Word);
   Put_Line("!");
   -- ^^^^  End  main proc executable stmts ^^^^
-- Outer procedure end follows
end My_Outer_Procedure;

The following list highlights things you should notice about the structure of the preceding source code:

  1. Any necessary with statements, and possibly some use statements. Note that with statements must occur before the outermost procedure or function starts, but use statements can be used in the declaration area of any function or procedure. The narrower the scope of a use statement, the better.
  2. Declaration of the outermost function or procedure. Note that there can be only one outermost function or procedure: Any others must be contained within the outermost one or declared and defined in different filed. Assuming the outermost function or procedure is a procedure called "My_Outer_Procesure, this declaration statement looks like procedure My_Outer_Procedure is. Also know that the declaration can have arguments, and if it's a function it must also state what type it returns.
  3. Declarations section for the outermost function or procedure. This includes declarations of types, subtypes and variables. It also includes declarations and definitions of any procedures or functions contained without the outermost function or procedure.
  4. The word begin, which starts the executable section of the outermost function or procedure.
  5. All the executable statements (not declarations) for the outermost procedure or function.
  6. An end My_Outer_Procedure statement to finish out the program.

The following is an example in a tiny working program that illustrates this structure for a program with no lower level procedures or functions:

A note on indentation. Indentation doesn't influence the program at all. Every line could be flush at column 1. If the program had no comments, you could even replace every line break with a space, and it would work just fine.

But the Ada community is in strong agreement on how indentation should be done. If you're working on a team, or if you want to get help from Ada experts, things go much better when you indent the way Ada people do. Read on for a description of well accepted indentation on a program with no sub-procedures:

So the indentation looks something like the following:

with and use statements
procedure whatever is
   type and variable declarations
begin
   executable statements
end whatever;
The following show indentation of loops, if statements and case statements:
if condition1 then
   statements
elsif condition 2 then
   statements
else
   statements
end if;

loop
   statements
   exit when condition
   statements
end loop;

case expression is
   when choice1 =>
      statements
   when choice2 =>
      statements
   when others =>
      statements
end case;

There are variations on the case statement, especially when the when clauses have only one short statement each, but try to maintain consistency unless it makes sense to do otherwise.

Source Code With Nested Procedures/Functions

Nested procedures and functions are indented the same as the outer paragraph:

   My_Nested_Procedure is
      declarations
   begin
      executable statements
   end My_Nested_Procedures;

So putting it all together, the following shows the structure of a working program equivalent to the working program earlier in this section, except that the following working program uses nested procedures:

-- vvvvvv Begin withs and uses vvvvvv
with Ada.Text_IO;
with Ada.Strings.Fixed;
use Ada.Text_IO;
-- ^^^^^^  End  withs and uses ^^^^^^

-- Main procedure declaration follows
procedure My_Outer_Procedure is
   -- vvvv Start main procedure declarations vvvv
   subtype Dice_Roll_Type is Integer range 1..6 ;
   Dice_Roll: Dice_Roll_Type;

   -- Declaration of nested fcn Say_Hello follows
   function Say_Hello return string is
   -- Say_Hello begin follows
   begin
      return "Hello ";
   -- Say_Hello end follows
   end Say_Hello;

   -- Declaration of nested fcn Say_World follows
   function Say_World return string is
      -- Say_World declarations follow
      My_String: String := "World!";
   -- Say_World begin follows
   begin
      return My_String;
   -- Say_World end follows
   end Say_World;

   -- Declaration of nested fcn Say_Dice follows
   function Say_Dice(
         Roll: Dice_Roll_Type
         ) return String is
      -- Say_Dice declarations follow
      -- vvvv Start Say_Dice declarations vvvv
      use Ada.Strings.Fixed;
      Roll_String: String := Roll'Image;
      Width: constant Positive := 5;
      Number_Of_Spaces: Integer :=
            Width - (2 + Roll_String'length);
      -- ^^^^  End  Say_Dice declarations ^^^^
   -- Say_Dice begin follows
   begin
      -- vvvv Start Say_Dice statements vvvv
      if Number_Of_Spaces < 0 then
         Number_Of_Spaces := 0;
      end if;
      return Number_Of_Spaces * " " & Roll_String & ": ";
      -- ^^^^  End  Say_Dice statements ^^^^
   -- Say_Dice end follows
   end Say_Dice;
   -- ^^^^  End  main procedure declarations ^^^^

-- Main procedure begin follows
begin
   -- vvvv Start main proc executable stmts vvvv
   Dice_Roll := 3;
   Put(Say_Dice(Dice_Roll));
   Put(Say_Hello);
   Put(Say_World);
   New_Line;
   -- ^^^^  End  main proc executable stmts ^^^^
-- Outer procedure end follows
end My_Outer_Procedure;

Take a few minutes to peruse the preceding program. Copy it into a file and compile and run it. Get acquainted with the program's basic structure: What goes where, which is compiler enforced, and indentation, which is very helpful if you're interacting with other Ada people.

A Simple Procedure

In Ada a procedure is a callable subprogram that doesn't return a value. The following program, this time called with_procedure.ada, puts the output statement in a procedure and calls the procedure to do the output. The output is the same "Hello World!" as last time:

with Ada.Text_IO;

procedure With_Procedure is
   procedure Say_Hello is
   begin
      Ada.Text_IO.Put_Line("Hello, World!");
   end Say_Hello;

begin
    Say_Hello;
end With_Procedure;

Compile with the following command:

gnatmake with_procedure.adb

WARNING:

The procedure name in the procedure definition, the procedure name in the end statement, and the program's filename (minus the .adb must match. Failure to heed this standard results in errors or warnings.

The procedure name in the procedure definition, the procedure name in the end statement, and the program's filename (minus the .adb must match. Failure to heed this warning results in errors or warnings.

Experts will tell you that you can get around some of this by jumping through some hoops, but when starting out, who needs hoops?

Adding an Argument to the Procedure

The following program, called with_procedure_argument.adb, feeds the string to be printed to the procedure as a command line argument:

with Ada.Text_IO; 

procedure With_Procedure_Argument is
   procedure Say_Hello(Mystring: String) is
   begin
      Ada.Text_IO.Put_Line(Mystring);
   end Say_Hello;

begin
    Say_Hello("Hello World via argument!");
end With_Procedure_Argument;

Naming Conventions: Beware

Ada is a case insensitive language, but...

Ada experts are of one mind on upper case and underscores in any Ada variable, object, procedure, function, or package you create or declare. They want you start the object with an uppercase letter, and separate all words with an underscore followed by an uppercase letter. See the following contrived Ada program that showcases underscores and uppercase:

with Ada.Text_IO; 
with Ada.Integer_Text_IO;

procedure Print_Messages is
   Number_Of_Repetitions: Integer;
   I: Integer;

   procedure Print_One_Message(
      Repetition_Number: Integer;
      The_String: String) is
   begin
      Ada.Integer_Text_IO.Put(Repetition_Number);
      Ada.Text_IO.Put(" ");
      Ada.Text_IO.Put_Line(The_String);
   end Print_One_Message;

begin
   Number_Of_Repetitions := 5;
   for I in Integer Range 1..Number_Of_Repetitions loop
      Print_One_Message(I, "My message.");
   end loop;
end Print_Messages;

Using both underscore and uppercase makes your typing task more difficult and slows you down, and from the compiler's perspective it's unnecessary. But if you ever want to ask for help from an Ada expert, you'll fair far better if you use Ada's conventions in the code you show him or her.

Note:

Ada reserved words (not library routines but reserved words) are customarily entirely lower case.

Ada is case insensitive except where filenames are involved. I therefore make sure all my filenames are lower case.

With a .adb file defining a package, the package name is like the following:

with My_Package_Name;
package body My_Package_Name is
--implementation code
end My_Package_Name

The filename for the preceding code should be my_package_name.adb, which case-insensitively matches the package name with ".adb" appended. Similarly, the .ads file should be named my_package_name.ads. If this matching is not followed, a warning results.

In the main program, the outermost procedure or function name follows similar rules, such that in the following program:

procedure My_Procedure_Name is
   -- declarations
begin
   -- implementation code
end My_Procedure_Name

The filename for the preceding should be my_procedure_name.adb to case insensitively match the procedure name. If they don't match, a warning results.

Whenever you get errors or warnings, investigate whether a filename doesn't match a procedure, or the procedure definition doesn't match its end statement.

Functions

Functions are like procedures except they return a value. The following is a very simple example of function use:

with Ada.Text_IO;

procedure My_Function is
   Result: Integer;

   function Add_Two(A, B: Integer) return Integer is
   begin
      return A+B;
   end Add_Two;

begin
   Result := Add_Two(3, 5);
   Ada.Text_IO.Put_Line("The sum is " & Integer'Image(Result)); 
end My_Function;

The preceding code is only 14 lines, so take some time to study it. Notice the following:

Practice writing this program from scratch until you're good at it. Then move on.

A Program To Showcase Ada

This section displays a program to showcase the syntax and philosophy of Ada. The whole reason for this program is to give an overview of how things are done in Ada, so don't try to understand the program: Its various syntax parts are explained later in this document. Besides this section, the following sections also document this program to showcase Ada:

The program follows this paragraph. Please scan it now rather than trying to fully understand it, because a deeper discussion of each part comes after the program. The program follows:

with My_Abbreviations;
use My_Abbreviations;

-- ATIO renames Ada.Text_IO
-- ATIOUIO rnms Ada.Text_IO.Unbounded_IO
-- ASU renames Ada.Strings.Unbounded
-- ATIO has a use clause
-- ASU has a use clause

procedure Convert_File is
   use ATIO;
   use ASU;
   Input_File : ATIO.File_Type;
   Output_File : ATIO.File_Type;

   procedure Open_Input_File(
         Input_File: out File_Type;
         Filename: String 
         ) is
   begin
      Open(Input_File, In_File, Filename);
   end Open_Input_File;

   procedure Open_Output_File(
         Output_File: out File_Type;
         Filename: String
         ) is
   begin
      Create(Output_File, Out_File, Filename);
   end Open_Output_File;


   procedure Get_Next_Line(
         File: in File_Type;
         Line: out ASU.Unbounded_String;
         More: out Boolean
         ) is
      subtype Buffer_String is String(1..512);
      Last: Natural;
      Buffer: Buffer_String;
      begin
         Line := ASU.Null_Unbounded_String;
         if End_Of_File(File) then
            More := False;
            return;
         end if;
         loop
            Get_Line(File, Buffer, Last);
            ASU.Append(
                  Line,
                  ASU.To_Unbounded_String(
                     Buffer(1 .. Last)
                  )
            );
            exit when Last < Buffer'Length;
         end loop;
         More := not End_Of_File(File);
      end Get_Next_Line;

   function Right_Justify_Number(
          Number : Positive;
          Width : Natural)
          return String is
      Num_Str : constant String :=
            Integer'Image(Number);
      Len     : constant Natural :=
            Num_Str'Length;
      Padding : constant Integer :=
            1 + Width - Len;
   begin
      Put_Line("dia:" & Padding'Image);
        if Padding > 0 then
           return
                 (1 .. Padding => ' ')
                 & Num_Str;
        else
           return Num_Str;
        end if;
   end Right_Justify_Number;


   Procedure Process_Line(
         Line: in out ASU.Unbounded_String;
         Line_Number: in out Positive) is
      use ASU; --Required for &

   begin
      Line := Right_Justify_Number(
            Line_Number, 2) &
            ": " & Line;
      Line_Number := Line_Number + 1;
   end;

   Procedure Write_The_Line(
         Output_File: File_Type;
         Line: ASU.Unbounded_String
      ) is
   begin
      ATIOUIO.Put(Output_File, Line);
      New_Line(Output_File);
   end;


   Line: ASU.Unbounded_String;
   Line_Number: Positive := 1;
   More: Boolean;
begin
   Open_Input_File(Input_File, "/usr/bin/Xorg");
      -- or gunzip, ldd, startx, dracut,
      -- update-grub, xdg-open, Xorg,
      -- xzless, zcat, zless
   Open_Output_File(Output_File, "./out.txt");
   More := True;
   While More loop
      Get_Next_Line(Input_File, Line, More);
      Process_Line(Line, Line_Number);
      Write_The_Line(Output_File, Line);
   end loop;
   Close(Input_File);
   Close(Output_File);
end Convert_File;

The preceding is a very simple file conversion program, rather contrived, just for instruction's sake. It reads line by line from the input file, prepending a right justified line number, colon and space to the line, and writing the modified line to the output file. Typical programming 101 exercise done in just about any beginning computer language course. The following pseudocode comprises the high level logic:

Open input file
Open output file
While more lines to read
	Read a line
	Modify the line
	Write the line
Close the output file
Close the input file

The concept is simple, right? This program's main routine pretty much matches the preceding. Read on...

The Main Routine

The following is the Ada language implementation of the pseudocode:

   Open_Input_File(Input_File, "/usr/bin/Xorg");
   Open_Output_File(Output_File, "./out.txt");
   More := True;
   While More loop
      Get_Next_Line(Input_File, Line, More);
      Process_Line(Line, Line_Number);
      Write_The_Line(Output_File, Line);
   end loop;
   Close(Input_File);
   Close(Output_File);

Things to note about the preceding code:

The File's Declarations

The files declarations occur above the definition of the main procedure or function. Please understand, these are the declarations for the file, not declarations of files. The following is a listing of this file's file declarations:

with My_Abbreviations;
use My_Abbreviations;

In the preceding, the with My_Abbreviations statements enable one to use the procedures, functions and types from the named package (in this case My_Abbreviations), assuming that same named package name precedes all calls to the procedure, function or type. The use statements bring everything public contained in the respective package (in this case My_Abbreviations) into the current namespace, which is the namespace of the whole file because they're declared outside of the main procedure.

The My_Abbreviations package is declared and defined within file my_abbreviations.ads, whose code follows:

with Ada.Text_IO;
with Ada.Text_IO.Unbounded_IO;
with Ada.Integer_Text_IO;
with Ada.Strings;
with Ada.Strings.Unbounded;
with Ada.Strings.Bounded;
with Ada.Strings.Fixed;
package My_Abbreviations is
   package ATIO renames Ada.Text_IO;
   package AITIO renames Ada.Integer_Text_IO;
   package ATIOUIO renames Ada.Text_IO.Unbounded_IO;
   package AS renames Ada.Strings;
   package ASU renames Ada.Strings.Unbounded;
   package ASB renames Ada.Strings.Bounded;
   package ASF renames Ada.Strings.Fixed;
end My_Abbreviations;

Because the main program file with's and use'es package My_Abbreviations, all the abbreviations are available anywhere inside (not outside) the main file's outer procedure or its nested functions and procedures. Another interesting fact is that the real package names are not available anywhere in the main program's file; the abbreviations must be used. If for some reason you want to use one of the real package names, it must be included near the top of the program file in a with statement. Finally, abbreviations aren't necessary; they're just a convenience. You could just declare the necessary Ada packages using with statements and then use their real names everywhere (or else use a use statement to bring a package's functions, procedures, types and variables into the current namespace).

The preceding package, which was created by me, assigns abbreviations to seven often used Ada provided packages. These abbreviations greatly shorten some lines of code without contaminating the program's namespace. A description of the purpose of these various packages follows:

The Declarations of the Outer Procedure

An Ada program can have exactly one outer procedure or function. Procedures and functions can be nested within the outer procedure, and those nested procedures and functions are simply the outer procedure's declarations. This nesting can go very deep, deeper than you'd ever want to go. The outer procedure in this section's program is called Convert_File. I coded the program to declare three categories of outer procedure declarations in a specific order:

  1. Declarations that must precede nested procedures and functions.
  2. The definitions of all the nested procedures and functions.
  3. Other types and variables.

Note:

In Ada, procedures and functions are a hunks of code that are called and subsequently return to the statement immediately after the calling statement. The compiler enforced rules are that anything that returns any kind of value through a return statement must be a function, and anything that does not return a value through a return statement must be a function. The declaration of a function starts with the word function, whereas the declaration of a procedure starts with the word procedure.

Declarations that must precede nested procedures and functions.

The following declarations come right at the top of the outer procedure, Convert_File:

   use ATIO;
   use ASU;
   Input_File : ATIO.File_Type;
   Output_File : ATIO.File_Type;

The first two lines bring everything public contained in the packages for Ada.Text_IO and Ada.Strings.Unbounded into the current namespace. The the third and fourth lines declare the input file and output file. These are all necessary for all the nested procedures and functions to work.

The definitions of all the nested procedures and functions.

These are described in more detail in later sections.

Other types and variables.

The bottom declarations follow:

   Line: ASU.Unbounded_String;
   Line_Number: Positive := 1;
   More: Boolean;

In the preceding, Line is the unbounded string that transfers lines from the input file through the line-changing Process_Line procedure and then gets written to the output file. Line_Number is the line number that gets incremented and prepended to each output line. More is the loop control variable for the read-process-write loop.

A discussion of the inner procedures and function declarations/definitions follows...

The File Opener Procedures

The following procedure opens the input file:

   procedure Open_Input_File(
         Input_File: out File_Type;
         Filename: String 
         ) is
   begin
      Open(Input_File, In_File, Filename);
   end Open_Input_File;

And the following procedure opens the output file:

   procedure Open_Output_File(
         Output_File: out File_Type;
         Filename: String
         ) is
   begin
      Create(Output_File, Out_File, Filename);
   end Open_Output_File;

In the preceding two procedures, notice the following:

  1. Both procedures set an Ada.Text_IO.File_Type variable in the main routine, yet they're not functions. Ada can change the value of a procedure's arguments such that those changes "stick" after the procedure exits. This is accomplished by placing the word out just before the argument's type. Of course, one can usually return a value through the return value of a function, but in certain circumstances this doesn't work. Notice how safe and easy this is: You don't need to pass an address like you would in C in order to permanently modify an argument outside the scope of the procedure.
  2. The two are identical except one uses open and the second usescreate, with the call to open using type In_File and the call to create using Out_File. open, create, In_File and Out_File are all defined in the Ada.Text_IO package, whose abbreviation is ATIO, and brought into the file's namespace by the use ATIO command near the top of the outer procedure, Convert_File.
  3. Each procedure consists of several lines to replace the one line open and/or create procedure calls that could have been used inline. I sacrificed a little performance for self-documenting code, so that an Ada newbie could understand the main procedure executable statements.
  4. The two procedures are almost identical, so I could have programmed them as one procedure by simply adding an Input_Or_Output argument. I chose not to do this because the procedure names exactly name the function of the procedures. Unless performance, RAM, stack or heap are at an extreme premium, I'd rather take a 1/1,000,000 performance hit, especially for something that gets done only once in the program. It might be different if the opens were in a very tight, fast loop.

So that's it. Into each procedure is passed a file variable that needs to be appropriately opened, and the name of the file to open.

Procedure Get_Next_Line

The code for this procedure follows:

   procedure Get_Next_Line(
         File: in File_Type;
         Line: out ASU.Unbounded_String;
         More: out Boolean
         ) is
      subtype Buffer_String is String(1..512);
      Last: Natural;
      Buffer: Buffer_String;
      begin
         Line := ASU.Null_Unbounded_String;
         if End_Of_File(File) then
            More := False;
            return;
         end if;
         loop
            Get_Line(File, Buffer, Last);
            ASU.Append(
                  Line,
                  ASU.To_Unbounded_String(
                     Buffer(1 .. Last)
                  )
            );
            exit when Last < Buffer'Length;
         end loop;
         More := not End_Of_File(File);
      end Get_Next_Line;

My procedure Get_Next_Line takes three arguments:

  1. An Ada.Text_IO.File_Type variable representing the input file.
  2. An Ada.Text_IO.Unbounded.Unbounded_String variable through which Get_Next_Line delivers the newly read line. Note that because this argument is actually an output, it's defined using the keyword out.
  3. A Boolean variable through which Get_Next_Line delivers whether there's more to read from the input file or not. Note that because this argument is actually an output, it's defined using the keyword out.

Procedure Get_Next_Line has three local declarations:

  1. Buffer_String is a subtype of String, specifically a string with elements 1 to 512. I picked 512 out of habit, but it could have been any reasonable number. This is discussed a little later in this section.
  2. Local variable Last is declared to be of type Natural (integers zero or greater) to contain the number of characters read.
  3. Local variable Buffer is declared to be of type Buffer_String. Its job is to store characters read from the input file.

Note:

There's noting magical about type Buffer_String's length of 512. It could have just as easily been 18405 or 11. I set it to a power of two by habit. The length is best set to a length slightly longer than your expectation for most lines, so it loops only once per call on most lines. On the other hand, for files expected to have extremely long lines, you might want to set it to a more manageable size rather than set it to 100,000 or something like that. It's a tradeoff between RAM and performance.

The executable statements for Get_Next_Line start by setting output argument Line to an empty unbounded string. Then there's a check for an EOF (End Of File) condition on the input file, and if it's EOF then output argument More is set to false and Get_Next_Line returns.

Next comes a loop that repeatedly reads characters from the input file and appends them onto the Buffer output argument, which is an unbounded string. The loop terminates when the number of characters read is less than the length of Buffer, which is declared by Buffer_Type to be 512 characters. If the number of characters read is less than the size of Buffer there are only two explanations:

  1. The entire line was read.
  2. The read encountered EOF.

Because of possibility #2, EOF is checked at the very bottom. This time, instead of using an if statement, I used the following nice short one-liner:

More := not End_Of_File(File);

Before going on to the next section of this document, let's go over Ada's Get_Line procedure.

Ada's Get_Line(File,Buffer,Last) behaves similarly to C's fgets(Buffer,sizeof(Buffer),File) because it reads up to the end of the line or the size of Buffer, whichever comes first. It does not necessarily read a whole line, regardless of line length.

The Get_Line procedure, when used with an input file, takes three arguments:

Get_Line(File, Buffer, Last)

Obviously File is a file opened for input. Buffer is a subtype of a string, in this case a 512 byte string. Please notice that Buffer is of subtype Buffer_String which is defined as follows:

subtype Buffer_String is String(1..512);

The preceding starts at 1, not as 0. This is important for the way I define the third argument, Last. Because Buffer_String starts at 1, the Last matches the number of characters actually read. Getting the entire line is the job of procedure Get_Next_Line, which was written by me.

Procedure Write_The_Line

My Write_The_Line procedure is extremely simple. As is obvious from its name, it writes a line to the output file. In this case the line is of type Ada.Text_IO.Unbounded.Unbounded_String to accommodate any length line. This procedure's arguments are the output file's file object and the unbounded string containing the line to be written.

Procedure Process_Line

Procedure Process_Line simply puts a right justified line number, colon and space before every line:

   Procedure Process_Line(
         Line: in out ASU.Unbounded_String;
         Line_Number: in out Positive) is
      use ASU; --Required for & function

   begin
      Line := Right_Justify_Number(
            Line_Number, 7) &
            ": " & Line;
      Line_Number := Line_Number + 1;
   end;

As you can see from the preceding code, procedure Process_Line has arguments of types ASU.Unbounded_String and the Positive subtype of Integer. Both are in out, meaning the function uses them as input, changes them, and makes them available to the calling procedure. The Line argument is changed by the prepending of the line number, a colon and a space. The Line_Number argument is changed by incrementing it. This is all very simple. The only complicating factor is that the number must be right justified. This is accomplished by calling my function Right_Justify_Number.

Function Right_Justify_Number

Procedure Right_Justify_Number is defined as follows:

   function Right_Justify_Number(
          Number : Integer;
          Width : Natural)
          return String is
      Num_Str : constant String :=
            Integer'Image(Number);
      Len     : constant Natural :=
            Num_Str'Length;
      Padding : constant Integer :=
            1 + Width - Len;
   begin
        if Padding > 0 then
           return
                 (1 .. Padding => ' ')
                 & Num_Str;
        else
           return Num_Str;
        end if;
   end Right_Justify_Number;

The preceding function is a hairy monster with gigantic teeth, because it's so chocked full of Ada'isms. Its purpose is to return a standard string representing a right justified representation of its Number argument. The length of the string is determined by its Width argument, except that if the string representation of the number is longer than Width, the string will be big enough to accommodate the number. In considering function Right_Justify_Number, let's start with its declarations:

Num_Str : constant String := Integer'Image(Number);

Consider the following declaration:

Num_Str : constant String := Integer'Image(Number);

The preceding declares Num_Str to be of type String and unchangeable, and then initializes Num_Str to be the string representation of the number, via Integer'Image. Now you have a string representation of the number argument.

Notice that the initialization contains executable statements. Normally executable statements aren't allowed in the declarations, but they're allowed when you're initializing an argument. Although this initially appears to be a little bit of a contradiction, it turns out to be very handy, so Kudos to Ada for making the language work this way.

Len: constant Natural := Num_Str'Length;

Now that Num_Str is defined and initialized, the following declaration line defines and initializes its length:

Len: constant Natural := Num_Str'Length;

Once again notice that the initialization contains an executable statement. Once again, it's define as constant because there's no reason to change it. Contemplate the fact that this declaration/initialization line depends on Num_Str, so switching places between the declaration/definitions of Num_Str) and Len errors out.

Padding : constant Integer := 1 + Width - Len;

Padding is an integer constant corresponding to the number of spaces to prepend to the right of the number's string representation, Num_Str, in order to have the returned string's length match argument Width in cases where the number's digits plus a possible minus sign is less than or equal to the Width argument. Let's examine the declaration line for Padding:

Padding : constant Integer := 1 + Width - Len;

Obviously, if Width is 5 and the number is a 2 digit number, 3 spaces should be prepended, right? So why are we adding 1? The addition of 1 is to make room for the minus sign that precedes any negative number.

The Executable Statements

As you can see, most of the math was done during the declarations. The thing remaining to do is to set Padding to 0 if it's negative, because you'd prefer too-long numbers be left justified instead of creating an error. Then you append the number representation with a colon and space and return that string, as follows:

      Put_Line("dia:" & Padding'Image);
        if Padding > 0 then
           return
                 (1 .. Padding => ' ')
                 & Num_Str;
        else
           return Num_Str;
        end if;

Making Your Own Ada Packages

Examples so far show an outer function calling inner functions it contains. This can become very hard to read and maintain as the number of procedures and functions increase. To solve this problem, Ada gives us packages. A package is a basket of procedures, functions and variables in a separate file. A package is imported using the with command. Packages can get pretty complicated, so this section showcases a dead-bang simple use of a package.

What we're going to do is create the following three files:

Start with say_hello.ads, which follows:

package Say_Hello is
    procedure Say_Hello;
end Say_Hello;

All the preceding file did was declare procedure Say_Hello, for inclusion in other files. Note that the package name doesn't need to be the same as the procedure name, but the package name does need to be the same as the filename (minus extension) in order to avoid warnings, and warnings should always be avoided. Also, it will be the package name that is used in the with statements in other files that include the package. Please reread this paragraph until you understand it: It's important.

Next take a look at say_hello.adb (.adb stands for ADa Body). It contains definitions (implementation):

with Ada.Text_IO;
with Say_Hello;
package body Say_Hello is
    procedure Say_hello is
    begin
        Ada.Text_IO.Put_Line("Hello, World!");
    end Say_hello;
end Say_Hello;

The preceding program file defines procedure Say_Hello in pretty much the same way as it was defined in the Hello World program, except that it's in a separate file and contains with Say_hello to include the declaration from the say_hello.ads package specification file.

Finally, consider the main program, with_package.adb:

with Ada.Text_IO;
with Say_Hello;

procedure With_Package is
begin
    Say_Hello.Say_Hello;
end With_Package;

The preceding with_package.adb is similar to the original Hello World program except:

You compile it with the following command:

gnatmake with_package.adb say_hello.adb

In the preceding command, notice that nowhere is the .ads file mentioned. It's pulled in by reference while compiling the .adb files.

My research indicates that the order of the files on the command line doesn't matter, but I could be wrong about this.

Two Procedures In the Package

It would be silly to require a package for every single procedure. In fact, packages are meant to house together several procedures contributing to a common goal. This section adds procedure Say_Goodbye to the package specification, the say-_hello_pkg.adb source file, and a call to Say_Goodbye is added to the main program, with_two_procedures.ads. say_hello_pkg.ads follows:

package Say_Hello_Pkg is
    procedure Say_Hello;
    procedure Say_Goodbye;
end Say_Hello_Pkg;

In the preceding, notice that this time we chose to have the package name different from the procedure name. It could have been the same, but for clarity we made it different. Also, notice the declaration of procedure Say_Goodbye. Remember that because the package name is Say_Hello_Pkg, the filename must be say_hello_pkg.ads.

File say_hello_pkg.adb is the source code for the package, not an individual procedure. So its filename must be say_hello_pkg.adb. This file's source code follows:

with Ada.Text_IO;
with Say_Hello_Pkg;
package body Say_Hello_Pkg is
    procedure Say_Hello is
    begin
        Ada.Text_IO.Put_Line("Hello, World!");
    end Say_Hello;

    procedure Say_Goodbye is
    begin
        Ada.Text_IO.Put_Line("Goodbye!");
    end Say_Goodbye;
end Say_Hello_Pkg;

In the preceding code, we defined additional procedure Say_Goodbye. Now lets look at the main program file, with_two_procedures.adb:

with Ada.Text_IO;
with Say_Hello_Pkg;

procedure With_Two_Procedures is
begin
    Say_Hello_Pkg.Say_Hello;
    Say_Hello_Pkg.Say_Goodbye;
end With_Two_Procedures;

The obvious change is that the main routine now calls procedure Say_Goodbye from package Say_Hello_Pkg.

To compile issue the following command:

gnatmake with_two_procedures.adb say_hello_pkg.adb

Packages and Naming

I guarantee you that as a new Ada user you'll get messed up with packages and naming. When you get errors or warnings while using packages, always investigate your naming. Some rules for naming things while using packages follows:

This naming stuff is tough, so please go through this section whenever you get errors or warnings you suspect come from naming problems.

Don't Forget You Can Nest Procedures and Functions

Packages are wonderful, especially when grouping procedures, functions and types that work together to bestow a specific capability. But remember, in simple programs you can do it all in one file by nesting functions and procedures like you did in the A Simple Procedure section.

Package Name Abbreviations.

A beautiful part of Ada is having the full Ada package hierarchy of a function revealed in code, like Ada.Directories.Hierarchical_File_Names, so when you call Ada.Directories.Hierarchical_File_Names.Is_Simple_Name("/etc/fstab") you know exactly what you're calling and where it came from. At the same time, how would you like to have thirty calls to Ada.Directories.Hierarchical_File_Names.Is_Simple_Name("/etc/fstab"), sometimes occurring on the same line and sending your code way past 80 columns? Sometimes the benefits of this explicitness don't offset the over-verbosity of the code it creates. This tradeoff is addressed in this section.

Let's address the simple case first. If you know that only one or two instances of procedures, functions, types or variables from Ada.Directories.Hierarchical_File_Names will appear in your code, it's by far best to spell the whole thing out.

Next, let's address the opposite case: Common packages whose functions and procedures are expected to be used time after time in your code. The easiest way, but I think is usually a bad way, is to use the use command, as follows:

with Ada.Text_IO;
use Ada.Text_IO;
procedure Say_Hello is
begin
   Put_Line("Hello World!");
end Say_Hello;

The use command in the preceding code put every non-private part of the Ada.Text_IO package in the file's namespace, so "Put_Line" without its package name is sufficient. Trouble is, your file's namespace is getting overpopulated and confusing.

The namespace overpopulation can be reduced a little bit by putting the use clause only in procedures and functions that actually need it, as in the following example:

with Ada.Text_IO;
procedure Say_Hello is
   procedure Say_String(My_String: String) is
      use Ada.Text_IO;
   begin
      Put_Line(My_String);
   end Say_String;
begin
   Say_String("Hello World!");
end Say_Hello;

In the preceding code, the entire public namespace for package Ada.Text_IO is imported into the namespace of procedure Say_String() only, reducing confusion and conflicts outside of Say_String().

An approach I like for often used packages is to create your own package containing your abbreviations for all the packages you commonly use. For example, check out the following my_abbreviations.ads file:

package My_Abbreviations is
   package ATIO renames Ada.Text_IO;
   package AITIO renames Ada.Integer_Text_IO;
   package ATIOUIO renames Ada.Text_IO.Unbounded_IO;
   package ASU renames Ada.Strings.Unbounded;
   package ASB renames Ada.Strings.Bounded;
   package ASF renames Ada.Strings.Fixed;
end My_Abbreviations;

The following code, which is contrived to show the usefulness of abbreviations, follows:

with My_Abbreviations;
with Ada.Strings.Fixed;
with Ada.Text_IO;
procedure Speak_Your_Mind is
   use My_Abbreviations;
   St1, St2, St3, St4: String(1..100);
   I: Integer;
begin
   ASF.Move("Iteration number ", St1);
   ASF.Move(" completed", St3);
   for I in integer range 1..4 loop
      ASF.Move(Integer'image(i), St2);
      ASF.Move(ASF.Trim(St1, AS.Right) & ASF.Trim(St2, AS.Right) & ASF.Trim(St3, AS.Right), St4);
      ATIO.Put(ASF.Trim(St4, AS.Right));
      ATIO.Put_Line("!");
   end loop;
end Speak_Your_Mind;

After compiling with gnatmake speak_your_mind.adb, the preceding code outputs the following:

[slitt@mydesk ada]$ ./speak_your_mind
Iteration number 1 completed!
Iteration number 2 completed!
Iteration number 3 completed!
Iteration number 4 completed!
[slitt@mydesk ada]$

Now, looking back in the code, imagine if I'd used Ada.Strings.Fixed everywhere I instead abbreviated with ASF. Imagine I'd used Ada.Strings instead of AS, and Ada.Text_IO instead of ATIO. The string concatenation line, the one with the & characters, is already quite long, but imagine its length if everything were spelled out. What a mess it would be.

my_abbreviations.ads is meant to put in most of your Ada code. You can look at your my_abbreviations.ads for reference, and in time you'll memorize your abbreviations. One abbreviation file can even be used for a whole team. Once again, only packages used often should be included in the my_abbreviations.ads.

A Handy Compile Script

When I'm developing software or performing Rapid Learning, I create a compile/link/run shellscript to recompile and rerun on each change. I usually call it ./jj so it's quick and easy to run. The following is the ./jj script I'm using for this Ada page:

#!/bin/ksh
rm testt
rm testt.o
rm testt.ali
2>&1 gnatmake testt.adb | \
   grep -v "file name does not match unit name"
./testt

Note:

The mumbo-jumbo on the gnatmake line of the preceding shellscript disables a warning that, in this particular use case, is harmless.

The preceding shellscript depends on my Ada file being named testt.adb. It first deletes the old executable, object file and ./ali file so on failure to compile there's no misunderstanding. Then it compiles testt.adb, creating a binary executable file called testt, and then runs testt. If the compile fails, testt doesn't exist, giving a "command not found" error.

The following session shows the use of ./jj on a testt.adb file containing a Hello World program:

[slitt@mydesk ada]$ ./jj
gcc -c testt.adb
gnatbind -x testt.ali
gnatlink testt.ali
Hello World!
[slitt@mydesk ada]$

This compile script is used frequently in the rest of this document.

Strings in Ada

In Perl, Python, Lua, Ruby, C++, BASIC, Cobol, Javascript, to a lesser extent C, and some versions of Pascal, strings are magic. You can assign new and different strings to string variables. In some of these languages, you don't even need to worry about overrunning the capacity of the string variable because the language takes care of it for you. Using strings in these languages is simple and intuitive.

The preceding paragraph does not describe string usage in Ada. In order to succeed in Ada this fact must be accepted. Once you accept it, Ada strings start making sense.

Ada strings are manipulated and translated almost exclusively with functions from commonly used Ada packages Ada.Strings, Ada.Strings.Fixed, Ada.Strings.Unbounded and Ada.Strings.Bounded.

Ada has three kinds of strings:

There are functions to convert between these three types of strings.

Intro to Fixed Length Strings

Ada's type String is a fixed length string that doesn't need any special packages to declare. Consider the following code:

with Ada.Text_IO;
procedure Showstring is
   My_String: String := "abcde";
begin
   Ada.Text_IO.Put_Line(My_String);
end Showstring;

In the preceding code, variable My_String is declared type String and then initialized to "abcde", after which this string is printed. The preceding code proves that Ada's String type doesn't require libraries to declare and set. Now let's add a line that changes the value of My_String just before it is printed:

with Ada.Text_IO;
procedure Showstring is
   My_String: String := "abcde";
begin
   My_String := "edcba";
   Ada.Text_IO.Put_Line(My_String);
end Showstring;

Compile and run:

[slitt@mydesk ada]$ ./jj
gcc -c testt.adb
gnatbind -x testt.ali
gnatlink testt.ali
edcba
[slitt@mydesk ada]$

The compile/run script shows that it worked, with the "edcba" assignment being the value printed.

Cool! So is Ada just like Python in assigning new string values to string variables? NO!!! Consider the following program, where the second assignment assigns "abc" instead of "edcba":

with Ada.Text_IO;
procedure Showstring is
   My_String: String := "abcde";
begin
   My_String := "abc";
   Ada.Text_IO.Put_Line(My_String);
end Showstring;

Compile and run the program that assigns a shorter string:

[slitt@mydesk ada]$ ./jj
gcc -c testt.adb
testt.adb:6:17: warning: wrong length for array of subtype of "Standard.String" defined at line 4 [enabled by default]
testt.adb:6:17: warning: Constraint_Error will be raised at run time [enabled by default]
gnatbind -x testt.ali
gnatlink testt.ali

raised CONSTRAINT_ERROR : testt.adb:6 length check failed
[slitt@mydesk ada]$ 

On compilation it gave two warnings, and when run it aborted with a constraint error. You can't re-assign a shorter or longer value to a fixed string variable.

Fixed Length String Move Procedure and 'length Operator

You can get past the inability to assign various length strings to a given String variable using functions and procedures, using the Ada.Strings Ada.Strings.Fixed packages. Let's start with a little program that initializes a String variable with "12345678", which is a length of 8, and then moves the string "Tiny" to it:

with Ada.Strings.Fixed;
with Ada.Text_IO;
procedure Showstring is
   My_String: String := "12345678";
begin
   Ada.Strings.Fixed.Move("Tiny", My_String);
   Ada.Text_IO.Put(My_String);
   Ada.Text_IO.Put_Line(":::");
end Showstring;

Compilation and output of the preceding program follows:

[slitt@mydesk ada]$ ./jj
gcc -c testt.adb
gnatbind -x testt.ali
gnatlink testt.ali
Tiny    :::
[slitt@mydesk ada]$

As you can see from the output, the Ada.Strings.Fixed.Move() procedure actually moved the string "Tiny" to variable My_String, but also moved enough trailing spaces to overwrite the "5678" that previously ended the string. This is why the Ada.Text_IO.Put_Line(":::"); line put ":::" four spaces to the right of the word "Tiny", in the output.

Assuming what was desired was an output of "Tiny:::", the following code accomplishes this using the Ada.Strings.Fixed.Trim(). The following code uses the abbreviations in the earlier described abbreviations in my_abbreviations.ads file:

with My_Abbreviations;
use My_Abbreviations;
with Ada.Strings.Fixed;
with Ada.Text_IO;
procedure Showstring is
   My_String: String := "12345678";
begin
   ASF.Move("Tiny", My_String);
   ATIO.Put(ASF.Trim(My_String, AS.Right));
   Ada.Text_IO.Put_Line(":::");
end Showstring;

The preceding code produced the desired output, "Tiny:::", as you can see in the following compile/run session:

[slitt@mydesk ada]$ ./jj
gcc -c testt.adb
gnatbind -x testt.ali
gnatlink testt.ali
Tiny:::
[slitt@mydesk ada]$

The preceding output is exactly what we wanted, but there's still trouble in paradise. If the string we move into the variable is longer than the length of the original string assigned the variable, a runtime except terminates the program. Consider the following program:

with My_Abbreviations;
use My_Abbreviations;
with Ada.Strings.Fixed;
with Ada.Text_IO;
procedure Showstring is
   My_String: String := "12345678";
begin
   ASF.Move("abcdefghabcdefgh", My_String);
   ATIO.Put(My_String);
   ––ATIO.Put(ASF.Trim(My_String, AS.Right));
   Ada.Text_IO.Put_Line(":::");
end Showstring;

In the preceding code, no matter whether you use the Ada.Strings.Fixed.Trim() function is irrelevant: Either way you get the exception, as follows:

[slitt@mydesk ada]$ ./jj
gcc -c testt.adb
gnatbind -x testt.ali
gnatlink testt.ali

raised ADA.STRINGS.LENGTH_ERROR : a-strfix.adb:475
[slitt@mydesk ada]$

The original String variable must be declared long enough to accommodate the new string. The following code declares the string to be 10000 characters long, moves an 8 character string to it and prints it trimmed and followed by ":::", and then does the same thing with a 16 character string:

with My_Abbreviations;
use My_Abbreviations;
with Ada.Strings.Fixed;
with Ada.Text_IO;
procedure Showstring is
   My_String: String (1..10000);
begin
   ASF.Move("12345678", My_String);
   ATIO.Put(ASF.Trim(My_String, AS.Right));
   Ada.Text_IO.Put_Line(":::");
   ASF.Move("abcdefghabcdefgh", My_String);
   ATIO.Put(ASF.Trim(My_String, AS.Right));
   Ada.Text_IO.Put_Line(":::");
end Showstring;

The preceding code produces the following output for compilation and run:

[slitt@mydesk ada]$ ./jj
gcc -c testt.adb
gnatbind -x testt.ali
gnatlink testt.ali
12345678:::
abcdefghabcdefgh:::
[slitt@mydesk ada]$

In the preceding, if one were to assign a 10001 length string to My_String, there would again be a runtime length error. Oppositely, if most values of My_String were only 20 characters long, the use of 10000 characters to hold the variable name would be very wasteful, especially if there were several such String variables. In such situations, ADA's Unbounded Strings are often a better choice.

Unbounded Strings

Unbounded strings can take any size string up to a very high number of characters. Unlike fixed strings, there's no need to declare huge strings that probably won't be used, just to (hopefully) guarantee there will be no runtime string length error. The following is a simple program to demonstrate the use of unbounded strings, once again using the my_abbreviations.ads file:

with My_Abbreviations;
use My_Abbreviations;
with Ada.Text_IO;
with Ada.Strings.Unbounded;
procedure Unbounded_String_Example is
   My_String: ASU.Unbounded_String;

begin
   My_String := ASU.To_Unbounded_String("Unbounded");
   ATIO.Put_Line(Ada.Strings.Unbounded.To_String(My_String));
   ATIOUIO.Put_Line(My_String);
end Unbounded_String_Example;

The preceding code outputs the word "Unbounded". What's tricky is that outstring must be declared, then assigned to the unbounded string via To_Unbounded_String(), and then must be converted back to a string via To_String(). Yes, it's a mess.

For Loops in Ada

The simplest loop in Ada is the for loop. Use for loops when looping through a range of consecutive integers, either increasing or decreasing. See the following example called show_for_loops.adb:

with Ada.Text_IO;

procedure show_for_loops is
begin
   for mynumber in integer range 1 .. 9 loop
      Ada.Text_IO.Put_Line("Increasing " & integer'Image(mynumber));
   end loop;

   for mynumber in reverse integer range 1..9 loop
      Ada.Text_IO.Put_Line("Decreasing " & integer'Image(mynumber));
   end loop;
end show_for_loops;

Yes, the preceding syntax is kind of hairy, so keep referring back to it until you've memorized it. Note that to go in reverse order you just add the word reverse directly after the in. There's a rumor out there that you can reverse the order by reversing the range (e.g. 9..1). This doesn't work on my compiler, it simply issues a warning about a null range and does nothing upon execution.

WARNING:

Do not change the value of mynumber inside of the loop. This is a bad habit from K&R C, and it throws a "error: assignment to loop parameter not allowed" hard error on compilation. If you need to tamper with the loop variable, use one of Ada's other loop constructs, which are discussed later in this document.

If Statements

Like other structured languages, Ada uses if statements. The following program, called show_if.adb, demonstrates a complete if structure including if, elsif and else, all controlled by a for loop:

with Ada.Text_IO;

procedure show_ifs is

begin
   for mynumber in integer range 1 .. 9 loop
      Ada.Text_IO.put("Number is " & integer'Image(mynumber) & " ");
      if mynumber = 2 then
          Ada.Text_IO.put_line("(two)");
      elsif mynumber = 5 then
          Ada.Text_IO.put_line("(five)");
      elsif mynumber = 8 then
          Ada.Text_IO.put_line("(eight)");
      else
          Ada.Text_IO.put_line("");
      end if;
   end loop;
end show_ifs;

The preceding code outputs the following output:

[slitt@mydesk ada]$ ./show_ifs
Number is  1 
Number is  2 (two)
Number is  3 
Number is  4 
Number is  5 (five)
Number is  6 
Number is  7 
Number is  8 (eight)
Number is  9 
[slitt@mydesk ada]$

Building an Unbounded String

This section demonstrates the value of an Ada Unbounded String by continually appending to the Unbounded String. The code once again uses the my_abbreviations.ads file, as shown in the following code:

following is a simple program to demonstrate the use of unbounded strings, once again using the my_abbreviations.ads file:

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

procedure Unbounded_String_Example is
   My_Unbounded_String: ASU.Unbounded_String;
   type Text_Arr is array(1..10) of ASU.Unbounded_String;
   Words: Text_Arr;

   procedure Load_Words_Array is
   begin
      Words(1) := ASU.To_Unbounded_String("One"); 
      Words(2) := ASU.To_Unbounded_String("Two"); 
      Words(3) := ASU.To_Unbounded_String("Three"); 
      Words(4) := ASU.To_Unbounded_String("Four"); 
      Words(5) := ASU.To_Unbounded_String("DONE"); 
   end Load_Words_Array;

begin
   Load_Words_Array;
   for Subscript in Words'range loop
      if ASU.To_String(Words(Subscript)) = "DONE" then
         Exit;
      end if;
      ATIOUIO.Put_Line(Words(Subscript));
      ASU.Append(My_Unbounded_String, Words(Subscript));
      ASU.Append(My_Unbounded_String, ", ");
   end loop;
   ATIOUIO.Put_Line(My_Unbounded_String);
end Unbounded_String_Example;

The preceding code outputs the words each on its own line, and then outputs a line with the words concatenated with commas, as shown in the following compile/link/run output:

[slitt@mydesk ada]$ ./jj
gcc -c testt.adb
gnatbind -x testt.ali
gnatlink testt.ali
One
Two
Three
Four
One, Two, Three, Four, 
[slitt@mydesk ada]$

A few comments about the preceding code and output. First, it loops through every element in the array, but exits the loop upon finding an array element with the text "DONE". Also, notice that each element of the array of unbounded strings is initialized with the function ASU.To_Unbounded_String();, where ASU is an abbreviation for Ada.Strings.Unbounded. They cannot be initialized with :=. Observe the user of ATIOUIO.Put_Line to output unbounded strings, where ATIOUIO is an abbreviation for Ada.Text_IO.Unbounded_IO. Every type of string has its own Put() and Put_Line() procedure.

You might notice that a comma, rather than a period, follows the final word in the concatenation. I'll leave it as an optional exercise to the reader to add a Boolean variable, and if/then/else, and a statement following the loop showing the period. I didn't add those things because the job of this section is to demonstrate unbounded strings as simply as possible.

This program declares a too-large array, which both runs the risk of creating a runtime constraint error, and otherwise is wasteful of RAM. The correct way to create an expandable array is to use vectors instead, but you haven't learned about this yet.

Defensive Typing

I'm sure if you've gotten this far, you're cursing Ada's over-pedantic use of data types. After all, you just want to get the job done, and Ada consistently and frustratingly stops you with insanely strict typing. At this point you just might be longing for the good old days of C, Python or Assembler. Funny thing is, though, the longer you use Ada, the more this perceived hassle starts looking like your friend.

It's no accident that Ada is pedantic about types. It's there by design, to help you prevent runtime errors, buffer and array overflows, logic errors, and all those other things that plague you in C long after your program compiles. You'll start noticing that once you get the typing right, the program usually runs as intended. Ada's pedantic typing starts becoming your friend.

This section showcases a ridiculously simple example of how typing prevents a logic error. Specifically, you'll see a function divide() that returns the top number divided by the bottom number. But first, examine the code without defensive typing:

with Ada.Text_IO;
with Ada.Float_Text_IO;

procedure Without_Defensive_Typing is
   Top_Num: Float;
   Bot_Num: Float;
   Result:  Float;

   function Divide(T: Float; B: Float) return Float is
   begin
      return T/B;
   end;

   procedure Put_Float(F: Float) is
   begin
      Ada.Float_Text_IO.Put(
         F,
         Fore=>4,
         Aft=>4,
         Exp=>0
         );
   end;

begin
   Top_Num := 1.0;
   Bot_Num := 5.0;
   Ada.Text_IO.put("The right answer is ");
   Result := Divide(Top_Num, Bot_Num);
   Put_Float(Result);
   Ada.Text_IO.New_Line;

   Ada.Text_IO.Put("Whoops, mixed up the numbers: ");
   Result := Divide(Bot_Num, Top_Num);
   Put_Float(Result);
   Ada.Text_IO.New_Line;

   Ada.Text_IO.Put_Line("End of program.");

end Without_Defensive_Typing;

The preceding code has the following output:

[slitt@mydesk ada]$ ./jj
gcc -c testt.adb
gnatbind -x testt.ali
gnatlink testt.ali
The right answer is    0.2000
Whoops, mixed up the numbers:    5.0000
End of program.
[slitt@mydesk ada]$

The programmer using function Divide() is free to mix up the two arguments, which creates a logical error that silently produces false results. Now let's use some defensive typing to prevent these errors at compile time, using slight modifications of the preceding program, with those modifications marked in red italic:

with Ada.Text_IO;
with Ada.Float_Text_IO;

procedure With_Defensive_Typing is
   type Top_Type is new Float;
   type Bot_Type is new Float;
   Top_Num: Top_Type;
   Bot_Num: Bot_Type;
   Result:  Float;

   function Divide(T: Top_Type; B: Bot_Type) return Float is
   begin
      return Float(T)/Float(B);
   end;

   procedure Put_Float(F: Float) is
   begin
      Ada.Float_Text_IO.Put(
         F,
         Fore=>4,
         Aft=>4,
         Exp=>0
         );
   end;

begin
   Top_Num := 1.0;
   Bot_Num := 5.0;
   Ada.Text_IO.put("The right answer is ");
   Result := Divide(Top_Num, Bot_Num);
   Put_Float(Result);
   Ada.Text_IO.New_Line;

   Ada.Text_IO.Put("Whoops, mixed up the numbers: ");
   Result := Divide(Bot_Num, Top_Num);
   Put_Float(Result);
   Ada.Text_IO.New_Line;

   Ada.Text_IO.Put_Line("End of program.");

end With_Defensive_Typing;

On compile and run, the preceding code produces the following output:

[slitt@mydesk ada]$ ./jj
rm: cannot remove 'testt': No such file or directory
rm: cannot remove 'testt.o': No such file or directory
rm: cannot remove 'testt.ali': No such file or directory
gcc -c testt.adb
testt.adb:36:21: error: expected type "Top_Type" defined at line 6
testt.adb:36:21: error: found type "Bot_Type" defined at line 7
gnatmake: "testt.adb" compilation error
./jj[7]: ./testt: not found
[slitt@mydesk ada]$ 

As shown in the preceding output, the program failed to compile. Specifically, line 36 errored out with a type error. Line 36 is the call to Divide() with the arguments in the wrong order.

Explanations

Function Divide() divides its first Float argument by its second Float argument. Things get ugly if the order of the arguments gets switched; the function returns the reciprocal of what you wanted. By setting a specific type for the numerator and a different type for the denominator, your program requires the right types for each argument, so reversing them produces a compiler error, preventing this type of mistake. This is defensive typing.

Take a look at function Divide(), noting how it converts T and B both to Floats before performing the division operation. This is necessary because the divide operator requires both its operands to be of type Float, not Top_Type or Bot_Type. This type conversion (called type cast in C) is performed by the Float() function. But be careful, because the Float() function compiles without error only in certain circumstances. One of those circumstances is when the type being converted is the same as a Float in every way except its name. This sameness is accomplished by the following line of the preceding code:

type Top_Type is new Float;

The preceding creates a new type, called Top_Type, that is identical to type Float, but with a different name.

Note:

Later in this document you'll learn about Ada subtypes. A subtype is the same type as its parent type except it's a subset (has greater constraints) than the parent type. At compilation time, a subtype is the same as its parent time, but at run time the subtype aborts if assigned something outside its range, even if it's in the range of its parent type. Contrast this with an independent type, which could be identical to an existing type, but still fails at compile time if used in an operation or function argument requiring that existing type.

By Default, Ada Allows Divide by Float Zero

Ada doesn't abort when you divide by 0.0.

Dividing by zero is a fundamental mathematical error. 1 divided by 0 is infinity, which is not a number. -1 divided by 0 is negative infinity, once again not a number. 0 divided by 0 is just plain weird, and not a number. Computers should never divide by zero without throwing a significant error. Unfortunately, Ada compiles and runs without error on divide by 0.0. Consider the following code:

with Ada.Text_IO;
with Ada.Float_Text_IO;

procedure Zero is
   Top: Float := 5.0;
   Bot: Float := 0.0;
   Result:  Float;

begin
   Result := Top/Bot;
   Ada.Float_Text_IO.put(Result);
   Ada.Text_IO.Put_Line(" Is the result of the division.");
end Zero;

On compile and run, the preceding program outputs the following:

[slitt@mydesk ada]$ ./jj
gcc -c junk.adb
gnatbind -x junk.ali
gnatlink junk.ali
+Inf******** Is the result of the division.
+Inf******** Is the result of the subsequent multiply.
[slitt@mydesk ada]$

The following shows the string representation of the result of divide by 0.0 depending on the numerator:

The preceding output shows it doesn't abort on compile or runtime, and instead of aborting at run time the result is some sort of positive or negative infinitay, or NaN (Not A Number).

Protecting From Divide by Float Zero

As discussed in the preceding section, Ada doesn't abort when you divide by 0.0, but instead produces a special result. I appreciate that sometimes you might want to actually do some work with positive or negative infinity, or maybe in some corner case NaN. But most of the time, if you're anything like me, you want your program to either abort or do something very special the instance the division with a 0.0 denominator is attempted. One way to do this is by creating a special subtype containing only 0.0 and nothing else, and testing for whether the divisor is in that special subtype. In effect, you're using the subtype as a one number set. Consider the following code:

with Ada.Text_IO;
with Ada.Float_Text_IO;
procedure Prevent_Divide_By_Zero is
   function Divide(T: Float; B: Float) return Float is
      subtype Floating_Zero_Type is Float range 0.0 .. 0.0;
   begin
      if B in Floating_Zero_Type then
         Ada.Text_IO.Put_Line("Aborting because divide by zero:");
         raise Constraint_Error;
      end if;
      return T/B;
   end;

   procedure Put_Float(F: Float) is
   begin
      Ada.Float_Text_IO.put(F,
         5,
         5,
         0
      );
   end Put_Float;

   procedure Put_Message(T,B: Float) is
   begin
      Put_Float(Divide(T,B));
      Ada.Text_IO.Put_Line(" is the number.");
   end;


begin
   Ada.Text_IO.Put_Line("Begin program here.");
   Put_Message(10.0, -1.5);
   Put_Message(1.0, 0.0001);
   Put_Message(10.0, 0.0);
   Ada.Text_IO.Put_Line("End of program.");
end Prevent_Divide_By_Zero;
[slitt@mydesk ada]$ ./prevent_divide_by_zero 
Begin program here.
   -6.66667 is the number.
10000.00000 is the number.
Aborting because divide by zero:

raised CONSTRAINT_ERROR : prevent_divide_by_zero.adb:9 explicit raise
[slitt@mydesk ada]$

Explanation

First, there's a difference between a type and a subtype. A subtype is a subset of its parent type. Usually the subtype is simply a constrained subset of the parent type. In this case the parent is of type Float, while the subset is only the interval 0.0 to 0.0 of the Float type. You can do anything with a variable in the subtype that you can with its parent type except assign it a value outside of its range. In this program's case, it means you can use it as a type Float in the division operation. Note this distinction while contemplating the division operation

When to Use Types and When to Use Subtypes

The simplest way to explain when to use which is when do you want it to fail: Compile time or run time? If you want it to fail at compile time, use a type. If you want it to fail at run time, use a subtype. But beware, it's tricky, because type conversion can be done on a new type only if several rules are adhered to. A type example and a subtype example follow:

Loops in Ada

Besides the for loop that was already mentioned, Ada supports the following types of loops:

While loops (Test At Top)

The following while loop changes the counter variable such that the counter doesn't process the printing of lines for loop counter counts 2 and 3:

with Ada.Text_IO;
with Ada.Integer_Text_IO;

procedure Test_At_Top is
   My_Number: Integer := 0;
   My_Square: Integer := 0;
begin
   While My_Number < 6 loop
      if My_Number = 2 then
         My_Number := 4;
      end if;
      Ada.Integer_Text_IO.put(My_Number);
      Ada.Text_IO.New_Line;
      My_Number := My_Number + 1;
   end loop;
end Test_At_Top; 

The preceding code produces the following output:

[slitt@mydesk ada]$ ./jj
gcc -c testt.adb
gnatbind -x testt.ali
gnatlink testt.ali
          0
          1
          4
          5
[slitt@mydesk ada]$ 

Counter variable changing is used a lot in state machines.

Another reason to use the while statement instead of the for statement is if you don't know ahead of times how many times you want to repeat the body, instead only knowing the state at which you want to stop. For instance, if you wanted all the prime numbers below a million, you'd use while. However, if you wanted the first 1000 prime numbers, you'd use for.

Test At Bottom Loops

Unlike Pascal, C, Javascript, and Java, Ada has no built-in facility to test at the bottom. Instead you simply create an infinite loop, test at the bottom of the loop body, and exit the loop if the test is true.

Note:

With regards to loops, Ada's Exit command is the same as C's break command.

Bottom testing is done when you want to guarantee the loop body is performed at least once. One frequent use is acquiring correct input from the user. The following code, which tests at the bottom (almost), repeatedly asks the user to type the word "fox", until the user correctly types the word:

with My_Abbreviations;
use My_Abbreviations;
with Ada.Text_IO;
with Ada.Strings.Unbounded;

Procedure Get_Word is
   Wanted_Word: ASU.Unbounded_String
      := ASU.To_Unbounded_String("fox");
   Gotten_Word: ASU.Unbounded_String;

   Function Strings_Equal(
         L,R: in ASU.Unbounded_String)
         return boolean is
      use ASU;
   begin
      return L = R;
   end;

   function Get_User_Input return ASU.Unbounded_String is
      The_Word: ASU.Unbounded_String;
   begin
      The_Word := ATIOUIO.Get_Line;
      return The_Word;
   end Get_User_Input;

   procedure Prompt_User is
   begin
      ATIO.Put("Type the word ");
      ATIOUIO.Put(Wanted_Word);
      ATIO.Put(" : ");
   end;

begin
   loop
      Prompt_User;
      Gotten_Word := Get_User_Input;
      if Strings_Equal(Gotten_Word, Wanted_Word) then
         Exit;
      end if;
      ATIO.Put_Line("You misspelled it, try again!");
   end loop;
   ATIO.Put_Line("Correct!");
end Get_Word; 

The preceding code produces the following output, assuming you enter the wrong word the first time and the correct word the second:

[slitt@mydesk ada]$ ./jj
gcc -c testt.adb
gnatbind -x testt.ali
gnatlink testt.ali
Type the word fox : faux
You misspelled it, try again!
Type the word fox : fox
Correct!
[slitt@mydesk ada]$

The preceding code is a little too complicated for teaching, so let's consider only the main routine:

   loop
      Prompt_User;
      Gotten_Word := Get_User_Input;
      if Strings_Equal(Gotten_Word, Wanted_Word) then
         Exit;
      end if;
      ATIO.Put_Line("You misspelled it, try again!");
   end loop;
   ATIO.Put_Line("Correct!");

The preceding code section would be a forever loop if not for the call to Exit if the strings are equal. Because the test and Exit is (almost) at the bottom, the loop body is guaranteed to execute at least once, which is just what you need for tested user input. The reason I call this almost test at the bottom is because an error message follows the test. Because a successful test exits the loop immediately, the only time the error message displays is when wrong data is entered. And of course, after the loop terminates due to correct input, a success message is printed.

Test Wherever Loops

As you saw in the Test at bottom loops section, you can put calls to Exit anywhere in the loop body, as many times as you want. You can do this in while loops, for loops, and just plain loop loops. This section has no sample code, because you can use the code in the the Test at bottom loops section as a guide.

Skip Remainder Of Loop Loop

In most languages, skip remainder of loop algorithms are accomplished with a continue statement. Unfortunately, Ada has no continue statement, so it must either rely on nested if statements or a goto with label. In absolutely any language, goto statements are a spectacular opportunity for logical errors and day-long debugs, so think long and hard before using a goto even in this limited capacity. You put one in for the purpose of skipping the remainder, and the next guy maintaining your code figures if you used goto statements in less harmless capacities, maybe he can get a little more adventurous with them. I sincerely wish Ada had a continue statement, but it doesn't.

Skipping to the top of a loop isn't ideal structured programming, but it's easier, simpler, and more readable for a loop that rules various things out, and not necessarily consecutively. It's typically used to save two or more nested if statements, sometimes a lot more than 2. The following code, which prints numbers 20, 40, 60 and 80 is comprised of a while loop, a goto, and a label:

with Ada.Text_IO;
with Ada.Integer_Text_IO;

procedure Continue_Kludge is
   My_Number: Integer;
begin
   My_Number := 0;
   While My_Number < 8 loop
      <<Top>>
      My_Number := My_Number + 1;
      if My_Number mod 2 = 1 then
         goto Top;
      end if;
      Ada.Integer_Text_IO.put(My_Number * 10);
      Ada.Text_IO.New_Line;
   end loop;
end Continue_Kludge;

The preceding code produces the following output:

gcc -c testt.adb
gnatbind -x testt.ali
gnatlink testt.ali
         20
         40
         60
         80
[slitt@mydesk ada]$

As you can see, if My_Number is odd, it skips the rest of the loop. If it's not odd, then it prints My_Number multiplied by 10. This is nothing more than a dead-bang easy example of skipping the rest of the loop. It's not practical. There are many better ways of creating the same output. The only value of this section is, if you find yourself really in need of a continue statement like they have in C and Python, you can kludge one together with a goto and a label.

Command Line Arguments

Command line arguments are pretty easy in Ada, as the following code, which outputs all its command line arguments shows.

with Ada.Text_IO; 
with Ada.Command_Line;

procedure Command_Line_Example is
begin
    Ada.Text_IO.Put("Number of arguments: ");
    Ada.Text_IO.Put_Line(Integer'Image(Ada.Command_Line.Argument_Count));

    for I in 1 .. Ada.Command_Line.Argument_Count loop
        Ada.Text_IO.Put("Argument "); 
       	Ada.Text_IO.Put(Integer'Image(I) & ": ");
       	Ada.Text_IO.Put_line(Ada.Command_Line.Argument(I));
    end loop;
end Command_Line_Example;

The preceding code produces the following output.

[slitt@mydesk ada]$ ./command_line_example one two three
Number of arguments:  3
Argument  1: one
Argument  2: two
Argument  3: three
[slitt@mydesk ada]$

As you can see, the action happens with Ada.Command_Line.Argument_Count and Ada.Command_Line.Argument(I).

Note also that I wrote this section before most of the other sections, and didn't ye t know about Ada.Integer_Text_IO.Put(My_Number), so instead I converted the number to a string with Integer'image(My_Number). I left it this way so you could start getting acquainted with conversion of integer to string.

Returning a Return Code

The following program, return_code_example demonstrates how an Ada program can return a return code, in this case 21:

function return_code_example return integer is
begin
   return(21);
end return_code_example;

So when we run the preceding program and test its return (in Linux), it returns 21:

[slitt@mydesk ada]$ ./return_code_example; echo $?
21
[slitt@mydesk ada]$

Running Another Program In Ada

Random Numbers in Ada

with ada.numerics.discrete_random;
with Ada.Text_IO;
with Ada.Integer_Text_IO;

procedure Random_Demo is
   subtype My_Random_Range_Type is
         Integer range 1..9;
   package Rand_Int is new
         ada.numerics.discrete_random(
         My_Random_Range_Type);
   My_Generator: Rand_Int.Generator;
   Rand: My_Random_Range_Type;

   procedure Print_One_Line(
         SS: Integer;
         Rand: My_Random_Range_Type
         ) is
      use Ada.Text_IO; -- local use
      use Ada.Integer_Text_IO; -- local use
   begin
      put(SS);
      put(Rand);
      New_Line;
   end;

begin
   Rand_Int.reset(My_Generator);
   for ss in 1..12 loop
      Rand := Rand_Int.Random(My_Generator);
      print_One_Line(ss, Rand);
   end loop;
end Random_Demo; 

The parts of the preceding code important to random numbers are in red italics. Consider the following facts:

  1. with ada.numerics.discrete_random; brings in the random number generator package for integers.
  2. subtype My_Random_Range_Type is Integer range 1..9; declares an integer range from 1 through 9. All generated random numbers will be integers in this range.
  3. package Rand_Int is new ada.numerics.discrete_random( My_Random_Range_Type); declares a new package that brings the facilities of Ada's random integer facility to integers 1 through 9.
  4. My_Generator: Rand_Int.Generator; creates a random integer generator, as specified in package with ada.numerics.discrete_random;, over integers 1 through 9.
  5. Rand: My_Random_Range_Type; declares a variable can take integer values from 1 through 9.
  6. Rand_Int.reset(My_Generator);: This initializes the generator and seeds it with the current time. This reset procedure has an optional second argument, a Standard Integer, that, if there, sets the seed instead of the time setting the seed. This can be used to make the random number generator much more secure by making the seed much less guessable. Oppositely, for debugging, this second argument could be an integer constant, 7, for instance, so that the same sequence of "random" numbers comes up every time the program is run. This command is one of two commands in this list that is executable code rather than a declaration.
  7. Rand := Rand_Int.Random(My_Generator);: This function delivers the next integer value 1 through 9. It's the other important part of the code that is executable code rather than a declaration.

The code displayed earlier produces the following output:

[slitt@mydesk ada]$ ./jj
gcc -c testt.adb
gnatbind -x testt.ali
gnatlink testt.ali
          1          6
          2          2
          3          1
          4          5
          5          9
          6          8
          7          1
          8          7
          9          7
         10          4
         11          2
         12          6
[slitt@mydesk ada]$

Naturally, the second column is different every time the program is run, because the second column prints random numbers.

Introduction to Ada Arrays

Arrays in Ada, like arrays in other languages, has lots of moving parts. This section is merely an introduction to arrays. Let's start with a simple program that iterates through a custom type:

with Ada.Text_IO;
with My_Abbreviations;
use My_Abbreviations;

procedure Iterate_Languages is
   type Languages is (Ada, C, Rust, Python);
   SS: Positive;

begin
   for Language in Languages loop
      ATIO.Put_Line(Language'Image);
   end loop;
end Iterate_Languages;

The preceding code outputs the following:

[slitt@mydesk ada]$ ./jj
gcc -c testt.adb
gnatbind -x testt.ali
gnatlink testt.ali
ADA
C
RUST
PYTHON
[slitt@mydesk ada]$

The only problem is that all the output is entirely uppercase. That's what 'imagedoes. To show the correct capitalization, we must add and use a parallel array that looks like the following:

with Ada.Text_IO;
with Ada.Strings.Fixed;

procedure Iterate_Languages is
   subtype String6 is String (1..6);
   type Languages is (Ada_Language, C, Rust, Python);
   Language_Names: constant array (Languages)
      of String6 :=
         ("Ada   ", "C     ", "Rust  ", "Python");

begin
   for Language in Languages loop
      Ada.Text_IO.Put(Language_Names(Language));
      Ada.Text_IO.Put_Line("!");
   end loop;
end Iterate_Languages;

The preceding code outputs the following:

[slitt@mydesk ada]$ ./jj
gcc -c testt.adb
gnatbind -x testt.ali
gnatlink testt.ali
Ada   !
C     !
Rust  !
Python!
[slitt@mydesk ada]$

Several points about the preceding code:

Many times we don't initially know how many elements an array will have. The right way to handle this situation would be to use vectors, but that's not a beginner's topic. Another way is to have the array big enough to hold the number of array entries we would (we think) maximally have, load a certain number of entries, and then have a sentinel entry indicating end of data. In the following example of an array of 20 unbounded strings, the sentinel value is "DONE, and this example uses the abbreviations in the earlier described abbreviations in my_abbreviations.ads file:

with My_Abbreviations;
with Ada.Text_IO;
with Ada.Strings.Fixed;

procedure Iterate_Languages_Sentinel is
   use My_Abbreviations;
   Language_Names: array (1..20)
      of ASU.Unbounded_String;
   SS: Positive;
   procedure Load_Language_Names is
   begin
      Language_Names(1) := ASU.To_Unbounded_String ("Ada");
      Language_Names(2) := ASU.To_Unbounded_String ("C");
      Language_Names(3) := ASU.To_Unbounded_String ("Rust");
      Language_Names(4) := ASU.To_Unbounded_String ("Python");
      Language_Names(5) := ASU.To_Unbounded_String ("DONE");
   end;

begin
   Load_Language_Names;
   for SS in Language_Names'Range loop
      if ASU.To_String(Language_Names(SS)) = "DONE" then
         exit;
      end if;
      ATIOUIO.Put(Language_Names(SS));
      Ada.Text_IO.Put_Line("!");
   end loop;
   ATIO.New_Line;
   ATIO.Put_Line("Finished!");
end Iterate_Languages_Sentinel;

As expected, the preceding code outputs the following:

[slitt@mydesk ada]$ ./jj
gcc -c testt.adb
gnatbind -x testt.ali
gnatlink testt.ali
Ada!
C!
Rust!
Python!

Finished!
[slitt@mydesk ada]$ 

Intro to File Handling

File handling for text files is provided by the Ada.Text_IO package. For binary files you use the Ada.Streams.Stream_IO package. This section explores only text file handling, because binary file handling is similar enough that you can figure it out yourself.

A Typical File Handling Program

The following code reads the /etc/fstab file line by line, capitalizes all letters, and writes it back to my_output.txt.

with Ada.Text_IO;
with Ada.Text_IO.Unbounded_IO;
with Ada.Strings.Unbounded;

with My_Abbreviations;

-- ATIOUIO rnms Ada.Text_IO.Unbounded_IO;
-- ASU renames Ada.Strings.Unbounded;
-- ATIO has a use clause;

procedure Convert_File is
   use Ada.Text_IO;
   use My_Abbreviations;
   Input_File : File_Type;
   Output_File : File_Type;
   subtype Buffer_String is String(1..512);

   procedure Open_Input_File(
         Input_File: out File_Type;
         Filename: String 
         ) is
   begin
      Open(Input_File, In_File, Filename);
   end Open_Input_File;

   procedure Open_Output_File(
         Output_File: out File_Type;
         Filename: String
         ) is
   begin
      Create(Output_File, Out_File, Filename);
   end Open_Output_File;


   procedure Get_Next_Line(
         File: in File_Type;
         Line: out ASU.Unbounded_String;
         More: out Boolean
         ) is
      Last: Natural;
      Buffer: Buffer_String;
      begin
         Line := ASU.Null_Unbounded_String;
         if End_Of_File(File) then
            More := False;
            return;
         end if;
         loop
            Get_Line(File, Buffer, Last);
            ASU.Append(
                  Line,
                  ASU.To_Unbounded_String(
                     Buffer(1 .. Last)
                  )
            );
            exit when Last < Buffer'Length;
         end loop;
         More := not End_Of_File(File);
      end Get_Next_Line;

   function Right_Justify_Number(
          Number : Integer;
          Width : Natural)
          return String is
      Num_Str : constant String :=
            Integer'Image(Number);
      Len     : constant Natural :=
            Num_Str'Length;
      Padding : constant Integer :=
            Width - Len + 1;
   begin
        if Padding > 0 then
           return
                 (1 .. Padding => ' ')
                 & Num_Str;
        else
           return Num_Str;
        end if;
   end Right_Justify_Number;


   Procedure Process_Line(
         Line: in out ASU.Unbounded_String;
         Line_Number: in out Positive) is
      use ASU; --Required for &

   begin
      Line := Right_Justify_Number(
            Line_Number, 7) &
            ": " & Line;
      Line_Number := Line_Number + 1;
   end;

   Procedure Write_The_Line(
         Output_File: File_Type;
         Line: ASU.Unbounded_String
      ) is
   begin
      ATIOUIO.Put(Output_File, Line);
      New_Line(Output_File);
   end;


   Buffer : Buffer_String;
   Last : Natural;
   Line: ASU.Unbounded_String;
   Line_Number: Positive := 1;
   More: Boolean;
begin
   Open_Input_File(Input_File, "/usr/bin/Xorg");
      -- or gunzip, ldd, startx, dracut,
      -- update-grub, xdg-open, Xorg,
      -- xzless, zcat, zless
   Open_Output_File(Output_File, "./out.txt");
   More := True;
   While More loop
      Get_Next_Line(Input_File, Line, More);
      Process_Line(Line, Line_Number);
      Write_The_Line(Output_File, Line);
   end loop;
   
   Close(Input_File);
   Close(Output_File);
end Convert_File;
[slitt@mydesk ada]$ cat my_output.txt 
       1: #!/bin/sh
       2: #
       3: # Execute Xorg.wrap if it exists otherwise execute Xorg directly.
       4: # This allows distros to put the suid wrapper in a separate package.
       5: 
       6: basedir="/usr/libexec"
       7: if [ -x "$basedir"/Xorg.wrap ]; then
       8: 	exec "$basedir"/Xorg.wrap "$@"
       9: else
      10: 	exec "$basedir"/Xorg "$@"
      11: fi
[slitt@mydesk ada]$

Best Practices for Naming

Please ignore this section until you're comfortable with all the previous sections.

Once you're familiar with all the previous sections, it's time to unlearn the few bad habits you've acquired and learn best practices for naming and other things. You can study these best practices at https://en.wikibooks.org/wiki/Ada_Style_Guide/Readability.

As you advance in your Ada journey, you'll need to read the entire style guide at https://en.wikibooks.org/wiki/Ada_Style_Guide. This book involves a lot of study, so you might not to do it right away. Just keep in mind that until you read and follow it, you'll later need to change some of your habits to follow Ada best practices.

Wrapup

If you've read and studied this document straight through, you now know how to compile and run a Hello World program in Ada, how to use a procedure, how to add arguments to a procedure, how to program and use a function, how to separate out functions into packages and use those packages. You've learned how to implement if statements, loops, and how to handle command line arguments and how to return a return code from your executable.


[ Training | Troubleshooters.Com | Email Steve Litt ]