Getting Started With Ada by Way of Rust

This post is the result of a crazy idea I had one day: how to make Ada programming more accessible to developers who already know Rust (making the total audience for this post something like seven people).

Ada and Rust are often mentioned in the same sentence, because they share some of the same goals, even though they are rather different. Ada has not been explicitly mentioned as an influence to Rust, but neither has C++. Many similar good ideas appear (modules / packages being the primary example, also appearing in modern C++).

Whereas Rust is the relative newcomer that some people think will take the place of C++ in systems programming (please!), Ada is an old industry stalwart that has been around the block for a few times and managed to embed itself (sic) in embedded systems.

Inspired by Chapter 2 in the book The Rust Programming Language, I decided to rewrite the guessing game example in Ada, just to highlight some of the differences (and maybe also similarities) between Rust and Ada.

If you haven't programmed in Rust before, you might still get something out of the discussion. But if you have, I'm going to compare and contrast features of Rust and Ada in detail.

The Guessing Game

Chapter 2, "Programming a Guessing Game", in the book "The Rust Programming Language", Second Edition, introduces Rust with a little program that lets the user try their luck in guessing a secret number that the computer is "thinking" about. The final Rust program is about 30 significant lines of code, and manages to present text input and output, generating random numbers, comparing numeric values, and pattern matching.

For the Ada version, I'm going to start from a situation where you have Ada tools already installed. In practice this most likely means GNAT Community Edition. Nowadays Ada also has a package repository, Alire, and its associated alr tool, which are not part of the language standard but have been heavily influenced by Cargo, the Rust package manager / Swiss army knife, and the crates.io repository. Here I'm not going to use Alire or alr, but instead I'll build the program with gnatmake, which is part of the GNAT tools.

For editing Ada source code I prefer to use Visual Studio Code with the "Language Support for Ada" extension by AdaCore.

To create the Ada version of the guessing game, start up the editor and create a new file called guessing_game.adb. The file extension .adb stands for "Ada (package) body". This is the only source code file we will need for this program.

The first version of the guessing game (found in the section "Processing a Guess" in The Book), converted into idiomatic Ada, looks like this:

with Ada.Text_IO;

procedure Guessing_Game is
    Guess : String (1..3) := "   ";
    Length : Natural;
begin
    Ada.Text_IO.Put_Line ("Guess the number!");
    Ada.Text_IO.Put_Line ("Please input your guess.");
    Ada.Text_IO.Get_Line (Item => Guess, Last => Length);
    Ada.Text_IO.Put_Line ("You guessed " & Guess);
end Guessing_Game;

Points of note:

You can compile the program into an executable file with the command:

gnatmake guessing_game.adb

gnatmake is going to compile the code with gcc, bind compiled objects with gnatbind, and finally link with any necessary libraries using gnatlink.

Generating Random Numbers

The secret number that the computer is "thinking" about is generated at random. In Rust you need the rand crate for that, and you include it in your Cargo.toml file. In Ada the random number generation is part of the standard library, but we are going to take a different approach.

In the section "Generating a Random Number" in The Book, you will see that it is almost an afterthought that the secret number is an integer from 1 to 100. This fact is mentioned in passing at the beginning of Chapter 2, but there is no special provision for this number. It is just an integer.

In Ada, you would start by defining a data type for the secret number. We know that it is supposed to be an integer between 1 and 100 inclusive. That happens to be a range that is included in Positive, which is an Ada standard type for integers starting from 1 and monotonically increasing. Hence a suitable type definition in Ada is:

subtype Guess_Type is Positive range 1..100;

As we now have a proper data type for the user's guess and the secret number, we can change the definition of the Guess variable accordingly:

Guess : Guess_Type;

Any time we want to generate discrete random numbers we need to bring in the standard package Ada.Numerics.Discrete_Random using a with clause. This is a generic package, so we need to instantiate it with the correct discrete data type:

with Ada.Numerics.Discrete_Random;

package Random_Guess is new Ada.Numerics.Discrete_Random (Result_Subtype => Guess_Type);

The services in this newly instantiated Random_Guess package are going to give us random numbers that fall in the range of Guess_Type.

We will need to declare a generator that will give us the numbers, and another variable where we can save the latest secret number:

Gen : Random_Guess.Generator;
Secret_Number : Guess_Type;

We also need to initialize the random number generator. I can't ensure you that the generator is cryptographically sound, so look it up in the Ada documentation if that matters for your chosen field. We pass in the generator:

Random_Guess.Reset (Gen);

Finally, we can generate a secret number using the generator:

Secret_Number := Random_Guess.Random (Gen);

At this point we also reveal the secret number, just for debugging purposes. However, at this point we introduce another Ada text I/O package, namely that for integer values. Like string handling, text I/O in Ada can get a bit involved, but basically you have two choices: either you output an "image" of the number, or you assume more control over the output using the services of a dedicated package. In this case we opt to use the Ada.Integer_Text_IO package to output the secret number.

The Ada.Integer_Text_IO package also gives us a way of reading input that can be interpreted as an integer (or its subtype). Putting both input and output of integers to use:

with Ada.Integer_Text_IO;

Ada.Integer_Text_IO.Get (Item => Guess);
Ada.Text_IO.Put ("You guessed: ");
Ada.Integer_Text_IO.Put (Guess);

The second version of the guessing game program looks like this:

with Ada.Text_IO;
with Ada.Integer_Text_IO;
with Ada.Numerics.Discrete_Random;

procedure Guessing_Game is
    subtype Guess_Type is Positive range 1..100;
    Guess : Guess_Type;

    package Random_Guess is new Ada.Numerics.Discrete_Random (Result_Subtype => Guess_Type);
    Gen : Random_Guess.Generator;
    Secret_Number : Guess_Type;

begin
    Ada.Text_IO.Put_Line ("Guess the number!");

    Random_Guess.Reset (Gen);
    Secret_Number := Random_Guess.Random (Gen);
    Ada.Text_IO.Put ("The secret number is ");
    Ada.Integer_Text_IO.Put (Secret_Number);
    Ada.Text_IO.New_Line;

    Ada.Text_IO.Put_Line ("Please input your guess.");

    Ada.Integer_Text_IO.Get (Item => Guess);
    Ada.Text_IO.Put ("You guessed: ");
    Ada.Integer_Text_IO.Put (Guess);
    Ada.Text_IO.New_Line;
end Guessing_Game;

Comparing the Guess to the Secret Number

The title of this section is identical to the one in The Book. However, because Ada does not have a match statement like Rust to perform pattern matching, we are going to use a regular if statement to compare the user's guess with the secret number.

In the original Rust program there was a mismatch between types: the input from the user was read as a string. There is no type inference in Ada, so we had to specify the type of Guess and Secret_Number. They both are of type Guess_Type, so there is no need to convert a string into a number, as is done in the original Rust program. There, the guess string is trimmed and parsed into a u32.

In Ada, this parsing and converting is done by Ada.Integer_Text_IO.Get, so we get a value of Guess_Type right away. So, all that is left to do is to actually compare the value to the secret number:

if Guess < Secret_Number then
    Ada.Text_IO.Put_Line ("Too small!");
elsif Guess > Secret_Number then
    Ada.Text_IO.Put_Line ("Too big!");
else
    Ada.Text_IO.Put_Line ("You win!");
end if;

For this we don't need pattern matching. However, for more elaborate situations it could be quite useful, and actually there is a proposal to add pattern matching to Ada.

At this point it might be a good idea to introduce the use clause, which is similar to the statement with the same name in Rust. In Ada you always need to "with" any packages you want to use, but you still need to refer to them by their fully qualified name. So even if you have the clause

with Ada.Text_IO;

at the top of your program, you still need to refer to the Put_Line procedure in the Ada.Text_IO package as Ada.Text_IO.Put_Line, unless you add the clause

use Ada.Text_IO;

at the top. The use clause is frowned upon a little bit by many Ada developers, because it will obscure the origin of the subprograms. If saving some typing seems to be worth the risk of naming conflicts, then why not use use along with with (see what I did there?).

You can also rename packages to give them more descriptive names and save typing at the same time. For example, it is not uncommon to see this or similar in an Ada program:

package TIO renames Ada.Text_IO;

Then you can say TIO.Put_Line instead of Ada.Text_IO.Put_Line.

The complete Ada version of the guessing game program so far looks like this:

with Ada.Text_IO;
with Ada.Integer_Text_IO;
with Ada.Numerics.Discrete_Random;

procedure Guessing_Game is
    subtype Guess_Type is Positive range 1..100;
    Guess : Guess_Type;

    package Random_Guess is new Ada.Numerics.Discrete_Random (Result_Subtype => Guess_Type);
    Gen : Random_Guess.Generator;
    Secret_Number : Guess_Type;

begin
    Ada.Text_IO.Put_Line ("Guess the number!");

    Random_Guess.Reset (Gen);
    Secret_Number := Random_Guess.Random (Gen);
    Ada.Text_IO.Put ("The secret number is ");
    Ada.Integer_Text_IO.Put (Secret_Number);
    Ada.Text_IO.New_Line;

    Ada.Text_IO.Put_Line ("Please input your guess.");

    Ada.Integer_Text_IO.Get (Item => Guess);
    Ada.Text_IO.Put ("You guessed: ");
    Ada.Integer_Text_IO.Put (Guess);
    Ada.Text_IO.New_Line;

    if Guess < Secret_Number then
        Ada.Text_IO.Put_Line ("Too small!");
    elsif Guess > Secret_Number then
        Ada.Text_IO.Put_Line ("Too big!");
    else
        Ada.Text_IO.Put_Line ("You win!");
    end if;
end Guessing_Game;

Allowing Multiple Guesses With Looping

This section introduces a similar looping mechanism as found in the original Rust program. Just like in Rust, you use the loop statement to make a loop, and you need to have some kind of exit condition, so that the loop doesn't run forever. The obvious condition is if the guess matches the secret number:

loop
    Ada.Text_IO.Put_Line ("Please input your guess.");

    Ada.Integer_Text_IO.Get (Item => Guess);
    Ada.Text_IO.Put ("You guessed: ");
    Ada.Integer_Text_IO.Put (Guess);
    Ada.Text_IO.New_Line;

    if Guess < Secret_Number then
        Ada.Text_IO.Put_Line ("Too small!");
    elsif Guess > Secret_Number then
        Ada.Text_IO.Put_Line ("Too big!");
    else
        Ada.Text_IO.Put_Line ("You win!");
    end if;

    exit when Guess = Secret_Number;
end loop;

In Ada, the way to exit the loop actually literally says "exit when this condition is true", which is really elegant.

Points to note:

Handling Invalid Input

One thing we haven't dealt with in the Ada version is invalid input. The user has free reign on the keyboard, so we could receive just about anything as a guess. Now that we use Ada.Integer_Text_IO.Get to read the input, all the valid inputs get parsed, but the invalid inputs are still our responsibility.

For example, if you type "xx" for the guess and press Enter, the program crashes with an error message that looks something like this:

raised ADA.IO_EXCEPTIONS.DATA_ERROR : a-tiinio.adb:86 instantiated at a-inteio.ads:18

From this we can make out that the exception that is being raised is Data_Error. Since Rust doesn't have exceptions, we didn't have this problem. Then again, Rust has optional types, but Ada doesn't. In the original Rust version of the guessing game invalid inputs were dealt by examining the optional type returned by the parse function, and then either using the result, or continuing to the next round of the loop. However, we cannot do quite the same in Ada, because it doesn't have a continue statement.

Well, at least we can deal with the exception. It turns out that in this case it's just a matter of cleaning up the input buffer (i.e. the rest of the line after the first erroneous character) when we get a Data_Error. For that we can use the Skip_Line subprogram in Ada.Text_IO.

To actually handle the exception we need a begin...exception...end structure inside the loop. In the exception branch we examine the error we get, and act accordinly:

with Ada.IO.Exceptions;

loop
    begin
        Ada.Text_IO.Put_Line ("Please input your guess.");

        Ada.Integer_Text_IO.Get (Item => Guess);
        Ada.Text_IO.Put ("You guessed: ");
        Ada.Integer_Text_IO.Put (Guess);
        Ada.Text_IO.New_Line;

        if Guess < Secret_Number then
            Ada.Text_IO.Put_Line ("Too small!");
        elsif Guess > Secret_Number then
            Ada.Text_IO.Put_Line ("Too big!");
        else
            Ada.Text_IO.Put_Line ("You win!");
        end if;

        exit when Guess = Secret_Number;
    exception
        when Error : Ada.IO_Exceptions.Data_Error =>
            Ada.Text_IO.Skip_Line; -- discard the erroneous output
    end;
end loop;

Points of note:

One final tweak: remove the display of the secret number. It's much more fun to guess the number if you don't see what it is beforehand. But otherwise, that's it! The final version of the Ada guessing game is here:

with Ada.Strings;
with Ada.Text_IO;
with Ada.Integer_Text_IO;
with Ada.Numerics.Discrete_Random;
with Ada.IO_Exceptions;

procedure Guessing_Game is
    subtype Guess_Type is Positive range 1..100;
    Guess : Guess_Type;

    package Random_Guess is new Ada.Numerics.Discrete_Random (Result_Subtype => Guess_Type);
    Gen : Random_Guess.Generator;
    Secret_Number : Guess_Type;

begin
    Ada.Text_IO.Put_Line ("Guess the number!");

    Random_Guess.Reset (Gen);
    Secret_Number := Random_Guess.Random (Gen);

    loop
        begin
            Ada.Text_IO.Put_Line ("Please input your guess.");

            Ada.Integer_Text_IO.Get (Item => Guess);
            Ada.Text_IO.Put ("You guessed: ");
            Ada.Integer_Text_IO.Put (Guess);
            Ada.Text_IO.New_Line;

            if Guess < Secret_Number then
                Ada.Text_IO.Put_Line ("Too small!");
            elsif Guess > Secret_Number then
                Ada.Text_IO.Put_Line ("Too big!");
            else
                Ada.Text_IO.Put_Line ("You win!");
            end if;

            exit when Guess = Secret_Number;
        exception
            when Error : Ada.IO_Exceptions.Data_Error =>
                Ada.Text_IO.Skip_Line;
        end;
    end loop;
end Guessing_Game;

You can build the final version with gnatmake using the command

gnatmake guessing_game.adb

This will result in the executable file guessing_game (or guessing_game.exe in Windows), which you can then run with ./guessing_game (or guessing_game.exe or .\guessing_game.exe) in Windows.

Here is an actual run in macOS:

% ./guessing_game
Guess the number!
Please input your guess.
36
You guessed:          36
Too small!
Please input your guess.
56
You guessed:          56
Too small!
Please input your guess.
76
You guessed:          76
Too big!
Please input your guess.
66
You guessed:          66
Too small!
Please input your guess.
70
You guessed:          70
Too small!
Please input your guess.
73
You guessed:          73
Too big!
Please input your guess.
72
You guessed:          72
Too big!
Please input your guess.
71
You guessed:          71
You win!

Conclusion

Hopefully this example was useful if you know Rust but are curious about Ada. It was an attempt to do something of an idiomatic translation of the intent of the Rust code into Ada. The thing I struggled most with was actually the loop statement combined with exception handling, i.e. how to discard invalid input and move on.

This mechanical translation does not expose the benefits of careful design, which is emphasized in large-scale Ada programs. Also, it does not utilize any external libraries from Alire or the alr tool.

An interesting coincidence is that this guessing game program appears almost in the same form in the book Ada 95: The Craft of Object-Oriented Programming by John English (Prentice-Hall, 1997) as Exercise 5.1 (on page 86 of the printed book). The only differences are that the Ada 95 version specifies guesses from 1 to 1000, and the user can guess at most 10 times. You may wish to try modifying the program in this post to match that specification.

I'm available for consulting for both Rust and Ada, but also Swift, C#, Python, and Java.