Guessing Game: Ada Style!
Table of Contents
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” 11 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 a procedure, and .ads
is for an Ada
specification declaring a package.22 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
33 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 use44 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
names55 I’m not even going to try to argue and it wants a space before
parentheses. In general, you’ll find Ada code to be very “airy”. This is by
design, albeit controversial. And yes, Ada is indented with three spaces per
level.66 Oh, and Ada is case-insensitive! So pUT_LinE
and put_line
refer
to the same thing. 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 languages77 such as std::string
in C++, look up
Unbounded_String
. That’s a RAII-managed high-level string type in standard
Ada. 88 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"
and nothing else.
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”.99 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.1010 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.1111 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
construct1212 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..
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;
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;
.1313 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”.1414 Then 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.1515 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 won’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.1616 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.