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:
- The clause
with Ada.Text_IO;
is a lot likeuse std::io;
in Rust. That's where the text I/O operations live. - The main program can be called whatever you like in Ada. It doesn't have to be
main
. Ada.Text_IO.Put_Line
is the counterpart for Rust'sprintln!
macro, but it is a simple procedure call.- I'm sidestepping the issue of Ada string handling, which can get complicated: you have fixed-size, bounded, and unbounded
strings. Usually you have to read in exactly the amount of characters in the fixed string (in this case three), but with this
particular version of
Get_Line
you can read anything up to the maximum length, while the rest is left alone. That is why I initializeGuess
to three blank spaces. Once we start actually reading numbers this will become a non-issue (or a different issue). - The
Length
variable is actually unused here. BothGuess
andLength
get their values filled in by theGet_Line
call, because they are defined asout
parameters. - In the last
Put_Line
statement the output is constructed by concatenating the literal string and the value ofGuess
with the&
operator.
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:
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:
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:
- The equality comparison in Ada is
=
, a single equals sign. This is in stark contrast with languages in the C family, where the equality operator is==
because the single equals sign was taken by the assignment statement. Ada follows the Pascal tradition, where assignment is expressed as:=
. - Statements in Ada are not separated by curly braces, but instead keywords are used as block delimiters:
if
...end if
,loop
...end loop
, you get the idea. - The spelling
elsif
is intentional. I remember reading the rationale for this in some Ada book and thinking it was brilliant, but I have forgotten it! Anyway, it was something that actually saved the programmer from errors down the road.
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:
- In Ada, you can start a new block with a new scope using the
begin
keyword. This corresponds to the way you start a new scope in Rust with the left curly brace. - Ada does not favor graphic notation, but instead prefers spelled-out keywords
like
begin
andend
.
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.