The Ada language - personal scratchpad

This is a mostly un-organized collection of notes I took while learning the language. Or rather, re-learning, because I’ve learned Ada many years ago, but the knowledge faded away over many years of not using it.

Case insensitive language

Everything, identifiers, package names, keywords is case-insensitive in Ada. This leads to a common practice of using long and often descriptive names for identifiers. While this makes the language often appear as verbose, it also means that Ada code is usually well readable and avoids cryptic names for identifiers or functions.

Mixed snake case

Most style guides encourage to use mixed snake case for identifiers. Instead of move_point or movePoint (camel case) in Ada you’ll very often see Move_Point for the same identifier. Since the language is case insensitive, the capitalized parts are purely for aesthetics.

It is also good practice to avoid single character identifiers like i or f, except for very short-lived identifiers (e.g. in loops). Identifiers starting or ending with an underscore are not allowed in Ada.

Variable shadowing

Ada allows variable shadowing in most cases. Local variables shadow variables of the same name declared in higher level blocks. However, the compiler organizes variables in name spaces and it is possible to access variables using fully qualified names when blocks are labeled. The name of the current subprogram is always an implicit namespace.

Shadowing of variable names
 1procedure Shadow_Test is
 2  var : Integer := 0;
 3begin
 4  Ada.Text_IO.Put_Line("The var is: " & Shadow_Test.var'Image);
 5  inner: declare
 6    -- this shadows the variable declared in line 2
 7    var : Float := 10.0;
 8  begin
 9    -- but you can still access the outer variable by using its fully qualified name
10    Ada.Text_IO.Put_Line("The outer var is: " & Shadow_Test.var'Image);
11    Ada.Text_IO.Put_Line("The inner var is: " & var'Image);
12    Shadow_Test.var := 20;
13  end inner;
14  Ada.Text_IO.Put_Line("The outer var is: " & Shadow_Test.var'Image);
15end Shadow_Test;

By default, the GNAT compiler will warn you when a variable gets shadowed by a local declaration.

Difference WITH vs USE

In Ada, importing modules (informally known as packages) is a two step process. The WITH keyword makes a package available to the current module. USE integrates it into the top level namespace. USE is therefore optional and should not always be used, particularly to avoid name conflicts.

Suppose, you have a package Lib that defines a method Foo. To use it you need to do WITH Lib and you can then use the method in your code by specifying the fully qualified name Lib.Foo. If you, however, USE Lib you can use Foo without prefixing it with its package name. An often seen construct in Ada are therefore statements like: WITH Ada.Text_IO; USE Ada.Text_IO;.

  • When you WITH and/or USE a package in the spec (.ads file), you do not need to do the same in the implementation (.adb) file of your module.

  • The compiler will warn you about unused packages when the -gnatu switch has been selected. You can silence this warnings (which are usually harmless) by using -gnatU (note the capital U). However, importing too man packages can lead to name conflicts which can result in compilation errors.

Classes, methods, functions

Ada allows object-oriented design and offers all the tools necessary. You can have classes (records in Ada) and methods working on instances of such records. The syntax is, however, different from known OO langauges like C++ or Java. Ada does not know the concept of member functions, but it does know the concept of dynamic dispatching, inheritance and interfaces. Member functions (method) are, however, defined in a different way and are not part of the class (record) itself.

Example
1type Point is tagged record
2  x, y, z : Coord;
3end Point;
4
5procedure Move_PointX(Self : in Point; delta : Coord) is
6begin
7  Self.x := Self.x + delta;
8end Move_PointX;

Here we have a record describing a point in 3d space with 3 coordinates (assume Coord is a custom floating point type that can only take values valid in our coordinate system). The method Move_PointX shall translate the x-coordinate of our point. Its signature has two arguments, the first is of type Point and THIS is what makes the procedure a quasi-member of Point. Such methods are called primitives in Ada slang. The compiler then allows objects of type Point to use that procedure like a normal member method. You can use p.Move_PointX(3.0); (called dot-notation) to increase x by 3 and this would be exactly the same as calling Move_PointX(p, 3.0); in which case you must pass a value for the object to work on. When using the dot notation, you do NOT need to specify a value for Self, the compiler does that for you and this works like the implicit this pointer in C++. The method definition must include it, but the compiler will automatically translate method calls. In C++ or Java, the method definition is part of the class definition and the compiler will call class methods with an implicit this.

Note also that Self is not a mandatory name, it’s just convention to use names like Self or This but you can use whatever you want. In Ada, this is called the dispatching parameter and you can use dot notation ONLY if the dispatching parameter is the first in the list of arguments.

Floating point arithmetics

How to express NaN (not a number)?

Use T'Invalid_Value where T is any floating point data type.

How to check for NaN / infinity?

Use x /= x where x is any floating point data type.

How to express Double.MIN_NORMAL? [1]

Use T'Model_Small for any floating point type T.

How to find ULP?

Use T'Model_Epsilon to find the number with the minimum precision for any floating point type T.

What aliased means

You may have seen a compiler error stating

main.adb:52:20: error: prefix of "Access" attribute must be aliased

so what does this mean? It means you must tell the compiler explicitly by using the aliased keyword that a variable must have a memory location. The Ada compiler is free to decide whether a variable is stored at a memory address (like on the stack or the heap) or just kept in a CPU register. The latter is often better and more performant for short-lived variables, but has a significant limitation: Such a variable cannot be accessed with an access type, because an access type needs a memory address. Internally, access types are pointers and there is no way to have a pointer addressing a CPU register.

So, declare your variable like so: foo : aliased Long_Float := 0.0;

Terminology in Ada

When learning Ada, it’s easy to get confused by terminology which can be quite a bit different from other languages.

Methods or functions vs subprograms

In Ada, the term method is rarely, if ever, used. We use the term subprogram which is either a procedure or a function. The difference is that a function must return a value and a procedure can not. A procedure is basically what a void function is in C++ or Java.

Primitives

Primitives is the term that is used for class methods, i.E. methods that work on objects instantiated from tagged records. Ada does not know the concept of implicit this and primitives must always specify the controlling parameter in explicit form.

A typical declaration of a primitive would look like:

procedure Foo(Self : in out T; v : Integer);

It declares a subprogram that takes 2 parameters. The first (Self) is of type T which must be a tagged record. It’s the controlling parameter and its presence in the first place of the formal arguments enables you to use the dot notation with primitives. Assume Bar is of type T then you can call either Bar.Foo(10); or Foo(Bar, 10);. Either way, the 10 is passed to the formal argument v, the dot-notation automatically passes Bar to T.

Access Types

This is the term we use for pointers which are really pointer-like data types in Ada. An access type can be used to access anything – a simple scalar type like Integer, a variable holding a record, an array or even allows access to a function. Internally, access types are implemented as pointers and that’s why the target object needs to be declared as aliased.