So far, we have talked about aggregates quite a bit and have seen a number of
examples. Now we will revisit this feature in some more detail.

An Ada aggregate is, in effect, a literal value for a composite type. It's a
very powerful notation that helps you to avoid writing procedural code for the
initialization of your data structures in many cases.

A basic rule when writing aggregates is that every component of the array or
record has to be specified, even components that have a default value.

with Ada.Text_IO; use Ada.Text_IO;
with Pkg; use Pkg;
procedure Main is
S : String := Convert (123_145_299);
-- ^ Valid, will choose the proper Convert
begin
Put_Line (S);
end Main;

Attention

Note that overload resolution based on the type is allowed for both
functions and enumeration literals in Ada - which is why you can have
multiple enumeration literals with the same name. Semantically, an
enumeration literal is treated like a function that has no parameters.

However, sometimes an ambiguity makes it impossible to resolve which
declaration of an overloaded name a given occurrence of the name refers to.
This is where a qualified expression becomes useful.

with Ada.Text_IO; use Ada.Text_IO;
with Pkg; use Pkg;
procedure Main is
S : String := Convert (123_145_299);
-- ^ Invalid, which convert should we call?
S2 : String := Convert (SSID'(123_145_299));
-- ^ We specify that the type of the expression is
-- SSID.
-- We could also have declared a temporary
I : SSID := 123_145_299;
S3 : String := Convert (I);
begin
Put_Line (S);
end Main;

Syntactically the target of a qualified expression can be either any expression
in parentheses, or an aggregate:

package Qual_Expr is
type Point is record
A, B : Integer;
end record;
P : Point := Point'(12, 15);
A : Integer := Integer'(12);
end Qual_Expr;

This illustrates that qualified expressions are a convenient (and sometimes
necessary) way for the programmer to make the type of an expression explicit,
for the compiler of course, but also for other programmers.

Attention

While they look and feel similar, type conversions and qualified
expressions are not the same.

A qualified expression specifies the exact type that the target expression
will be resolved to, whereas a type conversion will try to convert the
target and issue a run-time error if the target value cannot be so
converted.

Note that you can use a qualified expression to convert from one subtype to
another, with an exception raised if a constraint is violated.

Pointers are a potentially dangerous construct, which conflicts with Ada's
underlying philosophy.

There are two ways in which Ada helps shield programmers from the dangers of
pointers:

One approach, which we have already seen, is to provide alternative features
so that the programmer does not need to use pointers. Parameter modes,
arrays, and varying size types are all constructs that can replace typical
pointer usages in C.

Second, Ada has made pointers as safe and restricted as possible, but allows
"escape hatches" when the programmer explicitly requests them and presumably
will be exercising such features with appropriate care.

Here is how you declare a simple pointer type, or access type, in Ada:

Declare an access type whose values point to ("designate") objects from a
specific type

Declare a variable (access value) from this access type

Give it a value of null

In line with Ada's strong typing philosophy, if you declare a second access
type whose designated type is Date, the two access types will be incompatible
with each other, and you will need an explicit type conversion to convert from
one to the other:

In most other languages, pointer types are structurally, not nominally
typed, like they are in Ada, which means that two pointer types will be the
same as long as they share the same target type and accessibility rules.

Not so in Ada, which takes some time getting used to. A seemingly simple
problem is, if you want to have a canonical access to a type, where should
it be declared? A commonly used pattern is that if you need an access type
to a specific type you "own", you will declare it along with the type:

The last important piece of Ada's access type facility is how to get from an
access value to the object that is pointed to, that is, how to dereference the
pointer. Dereferencing a pointer uses the .all syntax in Ada, but is
often not needed - in many cases, the access value will be implicitly
dereferenced for you:

As you might know if you have used pointers in C or C++, we are still missing
features that are considered fundamental to the use of pointers, such as:

Pointer arithmetic (being able to increment or decrement a pointer in order
to point to the next or previous object)

Manual deallocation - what is called free or delete in C. This is
a potentially unsafe operation. To keep within the realm of safe
Ada, you need to never deallocate manually.

Those features exist in Ada, but are only available through specific standard
library APIs.

Attention

The guideline in Ada is that most of the time you can avoid manual
allocation, and you should.

There are many ways to avoid manual allocation, some of which have been
covered (such as parameter modes). The language also provides library
abstractions to avoid pointers:

One is the use of containers. Containers help users
avoid pointers, because container memory is automatically managed.

A container to note in this context is the
Indefinite holder.
This container allows you to store a value of an indefinite type such as
String.

GNATCOLL has a library for smart pointers, called
Refcount
Those pointers' memory is automatically managed, so that when an
allocated object has no more references to it, the memory is
automatically deallocated.

The linked list is a common idiom in data structures; in Ada this would be most
naturally defined through two types, a record type and an access type, that are
mutually dependent. To declare mutually dependent types, you can use an
incomplete type declaration:

package Simple_List is
type Node;
-- This is an incomplete type declaration, which is
-- completed in the same declarative region.
type Node_Acc is access Node;
type Node is record
Content : Natural;
Prev, Next : Node_Acc;
end record;
end Simple_List;

In the example above, the size of the Items field is determined once, at
run-time, but every Growable_Stack instance will be exactly the same size.
But maybe that's not what you want to do. We saw that arrays in general offer
this flexibility: for an unconstrained array type, different objects can have
different sizes.

You can get analogous functionality for records, too, using a special kind of
field that is called a discriminant:

Discriminants, in their simple forms, are constant: You cannot modify them once
you have initialized the object. This intuitively makes sense since they
determine the size of the object.

Also, they make a type indefinite: Whether or not the discriminant is used to
specify the size of an object, a type with a discriminant will be indefinite if
the discriminant is not declared with an initialization:

package Test_Discriminants is
type Point (X, Y : Natural) is record
null;
end record;
P : Point;
-- ERROR: Point is indefinite, so you need to specify the discriminants
-- or give a default value
P2 : Point (1, 2);
P3 : Point := (1, 2);
-- Those two declarations are equivalent.
end Test_Discriminants;

package Test_Discriminants is
type Point (X, Y : Natural) is record
null;
end record;
P : Point;
-- ERROR: Point is indefinite, so you need to specify the discriminants
-- or give a default value
P2 : Point (1, 2);
P3 : Point := (1, 2);
-- Those two declarations are equivalent.
end Test_Discriminants;

The examples of discriminants thus far have illustrated the declaration of
records of varying size, by having components whose size depends on the
discriminant.

However, discriminants can also be used to obtain the functionality of what are
sometimes called "variant records": records that can contain different sets of
fields.

package Variant_Record is
type Expr; -- Forward declaration of Expr
type Expr_Access is access Expr; -- Access to a Expr
type Expr_Kind_Type is (Bin_Op_Plus, Bin_Op_Minus, Num);
-- A regular enumeration type
type Expr (Kind : Expr_Kind_Type) is record
-- ^ The discriminant is an enumeration value
case Kind is
when Bin_Op_Plus | Bin_Op_Minus =>
Left, Right : Expr_Access;
when Num =>
Val : Integer;
end case;
-- Variant part. Only one, at the end of the record
-- definition, but can be nested
end record;
end Variant_Record;

The fields that are in a when branch will be only available when the
value of the discriminant is covered by the branch. In the example above, you
will only be able to access the fields Left and Right when the
Kind is Bin_Op_Plus or Bin_Op_Minus.

If you try to access a field that is not valid for your record, a
Constraint_Error will be raised.

with Variant_Record; use Variant_Record;
procedure Main is
E : Expr := (Num, 12);
begin
E.Left := new Expr'(Num, 15);
-- Will compile but fail at runtime
end Main;

package Variant_Record is
type Expr; -- Forward declaration of Expr
type Expr_Access is access Expr; -- Access to a Expr
type Expr_Kind_Type is (Bin_Op_Plus, Bin_Op_Minus, Num);
-- A regular enumeration type
type Expr (Kind : Expr_Kind_Type) is record
-- ^ The discriminant is an enumeration value
case Kind is
when Bin_Op_Plus | Bin_Op_Minus =>
Left, Right : Expr_Access;
when Num =>
Val : Integer;
end case;
-- Variant part. Only one, at the end of the record
-- definition, but can be nested
end record;
end Variant_Record;

package Variant_Record is
type Expr; -- Forward declaration of Expr
type Expr_Access is access Expr; -- Access to a Expr
type Expr_Kind_Type is (Bin_Op_Plus, Bin_Op_Minus, Num);
-- A regular enumeration type
type Expr (Kind : Expr_Kind_Type) is record
-- ^ The discriminant is an enumeration value
case Kind is
when Bin_Op_Plus | Bin_Op_Minus =>
Left, Right : Expr_Access;
when Num =>
Val : Integer;
end case;
-- Variant part. Only one, at the end of the record
-- definition, but can be nested
end record;
end Variant_Record;

In other languages

Ada's variant records are very similar to Sum types in functional languages
such as OCaml or Haskell. A major difference is that the discriminant is a
separate field in Ada, whereas the 'tag' of
a Sum type is kind of built in, and only accessible with pattern matching.

There are other differences (you can have several discriminants in a
variant record in Ada). Nevertheless, they allow the same kind of type
modeling as sum types in functional languages.

Compared to C/C++ unions, Ada variant records are more powerful in what
they allow, and are also checked at run time, which makes them safer.

We have already seen how to specify floating-point types. However, in some
applications floating-point is not appropriate since, for example, the roundoff
error from binary arithmetic may be unacceptable or perhaps the hardware does
not support floating-point instructions. Ada provides a category of types, the
decimal fixed-point types, that allows the programmer to specify the required
decimal precision (number of digits) as well as the scalaing factor (a power of
ten) and, optionally, a range. In effect the values will be represented as
integers implicitly scaled by the specified power of 10. This is useful, for
example, for financial applications.

The syntax for a simple decimal fixed-point type is

type<type-name>isdelta<delta-value>digits<digits-value>;

In this case, the delta and the digits will be used by the
compiler to derive a range.

Several attributes are useful for dealing with decimal types:

Attribute Name

Meaning

First

The first value of the type

Last

The last value of the type

Delta

The delta value of the type

In the example below, we declare two data types: T3_D3 and T6_D3.
For both types, the delta value is the same: 0.001.

When running the application, we see that the delta value of both
types is indeed the same: 0.001. However, because T3_D3 is restricted
to 3 digits, its range is -0.999 to 0.999. For the T6_D3, we have
defined a precision of 6 digits, so the range is -999.999 to 999.999.

Similar to the type definition using the range syntax, because we
have an implicit range, the compiled code will check that the variables
contain values that are not out-of-range. Also, if the result of a
multiplication or division on decimal fixed-point types is smaller than
the delta value required for the context, the actual result will be
zero. For example:

In this example, the result of the operation \(0.001 * 0.5\) is
0.0005. Since this value is not representable for the T3_D3 type
because the delta value is 0.001, the actual value stored in variable
A is zero. However, accuracy is preserved during the arithmetic
operations if the target has sufficient precision, and the value
displayed for C is 0.000500.

Ordinary fixed-point types are similar to decimal fixed-point types in that the
values are, in effect, scaled integers. The difference between them is in the
scale factor: for a decimal fixed-point type, the scaling, given explicitly by
the type's delta, is always a power of ten.

In contrast, for an ordinary fixed-point type, the scaling is defined by the
type's small, which is derived from the specified delta and, by
default, is a power of two. Therefore, ordinary fixed-point types are sometimes
called binary fixed-point types.

Note

Ordinary fixed-point types can be thought of being closer to the actual
representation on the machine, since hardware support for decimal
fixed-point arithmetic is not widespread (rescalings by a power of ten),
while ordinary fixed-point types make use of the available integer shift
instructions.

The syntax for an ordinary fixed-point type is

type<type-name>isdelta<delta-value>range<lower-bound>..<upper-bound>;

By default the compiler will choose a scale factor, or small, that is a
power of 2 no greater than <delta-value>.

For example, we may define a normalized range between -1.0 and 1.0 as
following:

In this example, we are defining a 32-bit fixed-point data type for our
normalized range. When running the application, we notice that the upper
bound is close to one, but not exact one. This is a typical effect of
fixed-point data types --- you can find more details in this discussion
about the Q format.
We may also rewrite this code with an exact type definition:

In fact the language is more general that these examples imply, since in
practice it is typical to need to multiply or divide values from different
fixed-point types, and obtain a result that may be of a third fixed-point type.
The details are outside the scope of this introductory course.

It is also worth noting, although again the details are outside the scope of
this course, that you can explicitly specify a value for an ordinary
fixed-point type's small. This allows non-binary scaling, for example:

As noted earlier, each enumeration type is distinct and
incompatible with every other enumeration type. However, what we did not
mention previously is that character literals are permitted as
enumeration literals. This means that in addition to the language's
strongly typed character types,
user-defined character types are also permitted: