Two Wrongs

(don't make a right)

Guessing Game: Ada Style!

by ~kqr

The Rust book starts with a tutorial on how to write a simple guessing game in Rust. I liked the tutorial, so I'm going to adapt it to Ada. If you're interested in "safer alternatives to C" (like D, Rust or Go) I think you should definitely give Ada a try before dismissing it. It has a rich history and these days, a good open-source implementation.

Like the Rust tutorial, we'll make a simple number guessing game. The program will generate a random number between 1 and 100, and repeatedly ask the user to guess which it is, while giving the user hints (like "too low, try again"). When the user guesses the number correctly, the program will print a congratulatory message and exit.

Setup

To start an Ada project, there is only one optional (but recommended) setup step: simply create a directory for your project. Then you need to create a file that is called the same thing as your main procedure, and open it in your favourite editor.

$ cd ~/projects
$ mkdir tutorial
$ cd tutorial/
$ touch guessing_game.adb
$ emacs guessing_game.adb &

Take note of the file extension: .adb is for Ada code (implementing a package or procedure), and .ads is for an Ada specification declaring a package. If you're used to C-based languages, you can view the specification a little bit like a header file. So normally, your Ada project consists of a bunch of packages, where each have one specification and one implementation.

In our simple program, we don't need a full package, so we don't need a specification either. We'll just implement the main procedure directly.

Hello, world!

To get us going quickly, we'll just type in a "hello, world" program in our main module. It may look like this:

with Ada.Text_IO;

procedure Guessing_Game is
   use Ada.Text_IO;
begin
   Put_Line ("Hello, world!");
end Guessing_Game;

To compile this code, run gnatmake -gnatyy guessing_game (the -gnatyy flag enables all style checks, which ensures your code looks like people expect Ada code to look like – think of it a little like gofmt except built into the compiler), and then run it like you would any other binary.

$ gnatmake -gnatyy guessing_game
gcc-6 -c -gnatyy guessing_game.adb
gnatbind-6 -x guessing_game.ali
gnatlink-6 guessing_game.ali
$ ./guessing_game
Hello, world!

I'll quickly walk through the code line by line.

with Ada.Text_IO;

Any packages you want to use (import, include) in your module, you have to list at the top of your file in with statements. Statements are terminated with semicolons in Ada.

procedure Guessing_Game is

Procedures are functions that don't return anything. If a procedure does not take any arguments, you don't write the parentheses either. The main procedure in Ada does not take any arguments. The keyword is indicates that the implementation of the procedure is coming next.

   use Ada.Text_IO;

This is the place for local declarations. Between is and begin you can declare local variables, define local types and even write local functions and procedures! In this case, we're saying we want to import all contents of Ada.Text_IO into the local scope, so we don't have to type Ada.Text_IO.Put_Line when we want to print something.

begin

Start of procedure code (and thus also end of local declarations.)

   Put_Line ("Hello, world!");

Ada style guides suggest a weird combination of CamelCase and snake_case for names and I'm not even going to try to argue. It wants a space before parentheses. In general, you'll find Ada code to be very "airy". This is by design, albeit controversial.

Oh, and Ada is case-insensitive! So pUT_LinE and put_line refer to the same thing. And yes, Ada is indented 3 spaces. Of course, all of this is stylistic choice and you can ignore it if you want to but I don't see why.

end Guessing_Game;

When you end a procedure, you also type the name of it. This makes it easier to debug mistakes of the kind "one closing brace too much" and gives the compiler more information to help you.

Processing a Guess

As part of our soft start, we will now read a string (representing a guess) from the user and print it back to them. Most of this will be fairly obvious.

with Ada.Text_IO;

procedure Guessing_Game is
   use Ada.Text_IO;
begin
   Put_Line ("Guess the number!");
   Put_Line ("Please input your guess.");

   declare
      Input : constant String := Get_Line;
   begin
      Put_Line ("You guessed: " & Input);
   end;
end Guessing_Game;

There's not that much new stuff here, but we'll look at the few pieces that are new. First of all, we have a declare block. This lets us introduce new variables in the middle of a procedure, and those variables will be local to just that block. We could have defined the variables as local variables in the procedure instead, but it will be obvious very shortly why we didn't.

We define a variable called Input, which is constant (i.e. we're not allowed to change it once it is initialised) and of type String. We assign it the return value of the Get_Line function, which reads a line of input from the user.

This means, as you may have guessed, that in Ada we declare variables by saying

Variable_Name : Type_Name;

And we assign values to variables by saying

Variable_Name := New_Value;

As you've seen, we can of course do both at the same time – and this is often a good idea, especially if it allows us to declare our variables constant to prevent accidental changes to them.

I should mention here that the String type is a little like C strings, or Pascal strings. It's fixed-size and very limited in how you can use it. It's just a low-level array of characters. If you want something similar to strings in high-level languages (and for example std::string in C++), look up Unbounded_String. That's a RAII-managed high-level string type in standard Ada.

As a good rule of thumb, the String type may be simpler and more efficient if we are dealing with constant strings (either as literals or as constants), but if we want to change or just in general... do things... with our strings, then Unbounded_String is probably the better option.

You might still be wondering about

      Put_Line ("You guessed: " & Input);

and rest assured, nothing weird is happening here. The ampersand (&) is the string concatenation operator in Ada, so something like "Jona" & "than" returns the string "Jonathan".

Guess a Number

You probably realise at this point that we don't really want a String when the user is supposed to guess a number. So this is where we get into the real goodness of Ada.

with Ada.Text_IO;

procedure Guessing_Game is
   subtype Small_Number is Integer range 1 .. 100;

   use Ada.Text_IO;
begin
   Put_Line ("Guess the number!");
   Put_Line ("Please input your guess.");

   declare
      Input : constant String := Get_Line;
      Guess : constant Small_Number := Small_Number'Value (Input);
   begin
      Put_Line ("You guessed: " & Small_Number'Image (Guess));
   end;
end Guessing_Game;

Let's look at the new stuff!

   subtype Small_Number is Integer range 1 .. 100;

We said guesses are going to be a number from 1 to 100. So we define a type specifically for these numbers. This is a recurring theme in Ada. We have general types (like Integer) which allow almost any value, but then we specialise them to only allow the values that make sense for our program. In this case, we define a type called Small_Number which is a subtype of Integer and limited to the specified range.

      Guess : constant Small_Number := Small_Number'Value (Input);

We declare another variable called Guess. It's also immutable (constant) but it is of type Small_Number. We assign to it the return value of Small_Number'Value (Input) which requires an explanation.

Types in Ada have something called attributes, which are basically built-in primitive functions that operate on that type. We use two attributes in this code: Value and Image. The Value attribute takes a String and tries to parse it into whatever type it belongs to – in our case Small_Number. The Image attribute does the opposite: it's a primitive way to convert a value to a String.

So, all in all, this line takes our guess and attempts to convert it to a value of the Small_Number type.

Finally, we convert the number back to a string to be able to print the guess.

      Put_Line ("You guessed: " & Small_Number'Image (Guess));

I encourage you to run this and try to enter things which are not numbers, or invalid numbers. If you enter something like "tablecloth" it will crash with a CONSTRAINT_ERROR exception and say it's a bad value. If you enter "529" the same exception will be thrown, but the reason will be "range check failed". Ada was one of the first languages to use exceptions the way they are used today, so we'll get back to this!

Generating a Secret Number

If the user is supposed to guess which number the program is thinking of, the program first needs to pick a number to think of!

Ada provides a generic package called Discrete_Random, which can be used to generate random values of, among other things, integer types.

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

procedure Guessing_Game is
   subtype Small_Number is Integer range 1 .. 100;

   package Random_Secret is new Ada.Numerics.Discrete_Random (Small_Number);
   use Ada.Text_IO;

   Seed : Random_Secret.Generator;
   Secret_Number : Small_Number;
begin
   Random_Secret.Reset (Seed);
   Secret_Number := Random_Secret.Random (Seed);

   Put_Line ("Guess the number!");
   Put_Line ("Please input your guess.");

   declare
      Input : constant String := Get_Line;
      Guess : constant Small_Number := Small_Number'Value (Input);
   begin
      Put_Line ("You guessed: " & Small_Number'Image (Guess));
   end;

   Put_Line ("The secret number was: " & Small_Number'Image (Secret_Number));
end Guessing_Game;

Boy, do we have some new stuff here! Fortunately, most of it should be familiar. I'll walk you through it either way.

with Ada.Numerics.Discrete_Random;

Probably no surprise, but we need to import the standard package for generating random values if we intend to use it.

   package Random_Secret is new Ada.Numerics.Discrete_Random (Small_Number);

This one is cool. Now we declare a local package! The Ada standard library comes with a package to generate random values of arbitrary types, but we want to generate random values of our specific Small_Number. To do that, we instantiate a new package that is a specialised version of the general one. If you've used C++, you can loosely think of this a little like templates for generics, except better.

Within our main method, Random_Secret now refers to a package that is specialised to generate random Small_Number values.

Could we also write use Random_Secret; to avoid extra typing? Yes. But in this case I think the package name adds to the readability to the code, and since you read much more often than you write it, it's worth conserving readability.


   Seed : Random_Secret.Generator;
   Secret_Number : Small_Number;
begin
   Random_Secret.Reset (Seed);
   Secret_Number := Random_Secret.Random (Seed);

First we create a variable called Seed to hold our generator, which is the thing that actually creates random-looking values from thin air. We also need a variable to store the secret number. The generator (or seed) is reset to a unique value when the program starts, and then we call the Random function from the Random_Secret package and pass in the seed/generator to get us a secret number.

Well... that's it, really, for generating random values: create a specialised local package from the Discrete_Random template and then use it like any other random generation library you're used to.

Comparing Guesses

In order to know whether the user guessed correctly or not, we need to compare the guess to the secret number. We'll do this with a regular old if/elsif/else construct.

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

procedure Guessing_Game is
   subtype Small_Number is Integer range 1 .. 100;

   package Random_Secret is new Ada.Numerics.Discrete_Random (Small_Number);
   use Ada.Text_IO;

   Seed : Random_Secret.Generator;
   Secret_Number : Small_Number;
begin
   Random_Secret.Reset (Seed);
   Secret_Number := Random_Secret.Random (Seed);

   Put_Line ("Guess the number");
   Put_Line ("Please input your guess.");

   declare
      Input : constant String := Get_Line;
      Guess : constant Small_Number := Small_Number'Value (Input);
   begin
      Put_Line ("You guessed: " & Small_Number'Image (Guess));

      if Guess < Secret_Number then
         Put_Line ("Your guess was too low!");
      elsif Guess > Secret_Number then
         Put_Line ("Your guess was too high!");
      else
         Put_Line ("Wow, you guessed right!");
      end if;
   end;

   Put_Line ("The secret number was: " & Small_Number'Image (Secret_Number));
end Guessing_Game;

You'll note that we terminate the construct with end if, rather than just end. Again, this is to reduce the number of logic mistakes where the code doesn't quite do what you expected it to because you've put an end at the wrong place.

Looping

Giving the user only once chance to guess isn't really fair, so we'll put the central guessing code in a loop, which we exit if the user guesses correctly.

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

procedure Guessing_Game is
   subtype Small_Number is Integer range 1 .. 100;

   package Random_Secret is new Ada.Numerics.Discrete_Random (Small_Number);
   use Ada.Text_IO;

   Seed : Random_Secret.Generator;
   Secret_Number : Small_Number;
begin
   Random_Secret.Reset (Seed);
   Secret_Number := Random_Secret.Random (Seed);

   Put_Line ("Guess the number");

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

      declare
         Input : constant String := Get_Line;
         Guess : constant Small_Number := Small_Number'Value (Input);
      begin
         Put_Line ("You guessed: " & Small_Number'Image (Guess));
   
         if Guess < Secret_Number then
            Put_Line ("Your guess was too low!");
         elsif Guess > Secret_Number then
            Put_Line ("Your guess was too high!");
         else
            Put_Line ("Wow, you guessed right!");
            exit;
         end if;
      end;
   end loop;

   Put_Line ("The secret number was: " & Small_Number'Image (Secret_Number));
end Guessing_Game;

The basic loop … end loop; construct introduces an infinite loop. We can use the exit keyword to break out of it. There are other kinds of loops, but for our purpose the basic infinite loop is good enough.

A cool feature in Ada is that we can label our loops. The change in the code below is small, but significant. Notice the Game label in relation to the loop.

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

procedure Guessing_Game is
   subtype Small_Number is Integer range 1 .. 100;

   package Random_Secret is new Ada.Numerics.Discrete_Random (Small_Number);
   use Ada.Text_IO;

   Seed : Random_Secret.Generator;
   Secret_Number : Small_Number;
begin
   Random_Secret.Reset (Seed);
   Secret_Number := Random_Secret.Random (Seed);

   Put_Line ("Guess the number");

   Game : loop
      Put_Line ("Please input your guess.");

      declare
         Input : constant String := Get_Line;
         Guess : constant Small_Number := Small_Number'Value (Input);
      begin
         Put_Line ("You guessed: " & Small_Number'Image (Guess));
   
         if Guess < Secret_Number then
            Put_Line ("Your guess was too low!");
         elsif Guess > Secret_Number then
            Put_Line ("Your guess was too high!");
         else
            Put_Line ("Wow, you guessed right!");
            exit Game;
         end if;
      end;
   end loop Game;

   Put_Line ("The secret number was: " & Small_Number'Image (Secret_Number));
end Guessing_Game;

You see how loop turned into Game : loop, which in turn made it so that end loop; is written end loop Game;, increasing code readability. More importantly, though, our break statement becomes exit Game;. Yes, this means we can break out of nested loops as well.

Exceptional Input

We still have a problem in our code, which is that the program will crash any time someone enters an invalid number (or no number at all!) To deal with this, we use the exception system in Ada.

Almost any begin … end; block in Ada can have an exception handler attached to it, so the intuitive thing to do might be to try to attach an exception handler to the declare block that takes the input. If you do this, however, you'll notice that the exception handler does not catch exceptions inside the "declare part" of the declare block – only exceptions that are raised between begin and end.

Thus, we have to wrap the declare block in an anonymous begin … end; block with an exception handler. See the code below.

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

procedure Guessing_Game is
   subtype Small_Number is Integer range 1 .. 100;

   package Random_Secret is new Ada.Numerics.Discrete_Random (Small_Number);
   use Ada.Text_IO;

   Seed : Random_Secret.Generator;
   Secret_Number : Small_Number;
begin
   Random_Secret.Reset (Seed);
   Secret_Number := Random_Secret.Random (Seed);

   Put_Line ("Guess the number");

   Game : loop
      Put_Line ("Please input your guess.");

      begin
         declare
            Input : constant String := Get_Line;
            Guess : constant Small_Number := Small_Number'Value (Input);
         begin
            Put_Line ("You guessed: " & Small_Number'Image (Guess));
   
            if Guess < Secret_Number then
               Put_Line ("Your guess was too low!");
            elsif Guess > Secret_Number then
               Put_Line ("Your guess was too high!");
            else
               Put_Line ("Wow, you guessed right!");
               exit Game;
            end if;
         end;
      exception
         when CONSTRAINT_ERROR =>
            null;
      end;
   end loop Game;

   Put_Line ("The secret number was: " & Small_Number'Image (Secret_Number));
end Guessing_Game;

The interesting bit is the following:

      exception
         when CONSTRAINT_ERROR =>
            null;

Any exceptions raised in that block should be checked if they are CONSTRAINT_ERROR exceptions. If they are, they should be ignored. The null statement means "do nothing". (And the loop will ensure the user is asked for their guess again.)

Note here that Ada does not allow empty statements, the way C does. In C, if you want to "do nothing", you just write an empty statement rather than null. That means it's easy to accidentally write code like

Person *anna;
if (get_person("Anna Bates", &anna) == MEMORY_VALID_PERSON);
   anna->age += 1;

This code will sometimes work, and sometimes be really, really broken. Struggling to see why? Check the stray semicolon after the if condition. The next line will be executed whether or not the address anna points to is valid.

In Ada, you don't get that sort of error, because if you want a "noop if statement", you'd have to write

declare
   Anna : access Person;
begin
   if Get_Person("Anna Bates", Anna) = Memory_Valid_Person then null; end if;
   Person.Increase_Age(Anna.all)
end;

The null just makes it over the top incredibly clear that something isn't quite right. Of course, you could argue that "with good coding style, that can't happen in C either", and you'd be right. The point is that even with bad coding style the error is incredibly easy to spot in Ada.

(If you're used to C-like pointers and a bit confused about the notation in the above Ada code, don't worry. In Ada, "pointers" are called "access types" and they are slightly safer than C pointers. The language is also designed such that you don't have to use pointers very often. Either way, a type access Person is a "pointer to person", and when Anna is of such a type, Anna.all is a the Person object it points to.)

Finishing Up

This is where the Rust tutorial leaves off, and this is where I will leave off.

You have in a short time learned a lot about Ada. You know about creating custom types, importing packages and specialising generic packages, creating local variables and constants, printing things to the user and reading user input, basic control structures such as loops and conditionals, and you even dipped your toes in exception handling.

These basics will get you far, but Ada is a well-developed, old language with a long track record. There's a lot to it we haven't even touched yet. I recommend getting into it; make your next hobby project in Ada, for example! Even if you don't, now you know it exists.

If you enjoyed this article, you might like others tagged with