Aggregates
Container Aggregates
Note
This feature was introduced in Ada 2022.
A container aggregate is a list of elements — such as [1, 2, 3]
— that we use to initialize or assign to a container. For example:
pragma Ada_2022; with Ada.Containers.Vectors; procedure Show_Container_Aggregate is package Float_Vec is new Ada.Containers.Vectors (Positive, Float); V : constant Float_Vec.Vector := [1.0, 2.0, 3.0]; pragma Unreferenced (V); begin null; end Show_Container_Aggregate;
In this example, [1.0, 2.0, 3.0]
is a container aggregate that we use
to initialize a vector V
.
We can specify container aggregates in three forms:
as a null container aggregate, which indicates a container without any elements and is represented by the
[]
syntax;as a positional container aggregate, where the elements are simply listed in a sequence (such as
[1, 2]
);as a named container aggregate, where a key is indicated for each element of the list (such as
[1 => 10, 2 => 15]
).
Let's look at a complete example:
pragma Ada_2022; with Ada.Containers.Vectors; procedure Show_Container_Aggregate is package Float_Vec is new Ada.Containers.Vectors (Positive, Float); -- Null container aggregate Null_V : constant Float_Vec.Vector := []; -- Positional container aggregate Pos_V : constant Float_Vec.Vector := [1.0, 2.0, 3.0]; -- Named container aggregate Named_V : constant Float_Vec.Vector := [1 => 1.0, 2 => 2.0, 3 => 3.0]; pragma Unreferenced (Null_V, Pos_V, Named_V); begin null; end Show_Container_Aggregate;
In this example, we see the three forms of container aggregates. The difference between positional and named container aggregates is that:
for positional container aggregates, the vector index is implied by its position;
while
for named container aggregates, the index (or key) of each element is explicitly indicated.
Also, the named container aggregate in this example (Named_V
) is using
an index as the name (i.e. it's an indexed aggregate). Another option is to use
non-indexed aggregates, where we use actual keys — as we do in maps.
For example:
pragma Ada_2022; with Ada.Containers.Vectors; with Ada.Containers.Indefinite_Hashed_Maps; with Ada.Strings.Hash; procedure Show_Named_Container_Aggregate is package Float_Vec is new Ada.Containers.Vectors (Positive, Float); package Float_Hashed_Maps is new Ada.Containers.Indefinite_Hashed_Maps (Key_Type => String, Element_Type => Float, Hash => Ada.Strings.Hash, Equivalent_Keys => "="); -- Named container aggregate -- using an index Indexed_Named_V : constant Float_Vec.Vector := [1 => 1.0, 2 => 2.0, 3 => 3.0]; -- Named container aggregate -- using a key Keyed_Named_V : constant Float_Hashed_Maps.Map := ["Key_1" => 1.0, "Key_2" => 2.0, "Key_3" => 3.0]; pragma Unreferenced (Indexed_Named_V, Keyed_Named_V); begin null; end Show_Named_Container_Aggregate;
In this example, Indexed_Named_V
and Keyed_Named_V
are both
initialized with a named container aggregate. However:
the container aggregate for
Indexed_Named_V
is an indexed aggregate, so we use an index for each element;
while
the container aggregate for
Keyed_Named_V
has a key for each element.
Later on, we'll talk about the Aggregate aspect, which allows for defining custom container aggregates for any record type.
In the Ada Reference Manual
Record aggregates
We've already seen record aggregates in the Introduction to Ada course, so this is just a brief overview on the topic.
As we already know, record aggregates can have positional and named component associations. For example, consider this package:
package Points is type Point_3D is record X, Y, Z : Integer; end record; procedure Display (P : Point_3D); end Points;with Ada.Text_IO; use Ada.Text_IO; package body Points is procedure Display (P : Point_3D) is begin Put_Line ("(X => " & Integer'Image (P.X) & ","); Put_Line (" Y => " & Integer'Image (P.Y) & ","); Put_Line (" Z => " & Integer'Image (P.Z) & ")"); end Display; end Points;
We can use positional or named record aggregates when assigning to an object
P
of Point_3D
type:
with Points; use Points; procedure Show_Record_Aggregates is P : Point_3D; begin -- Positional component association P := (0, 1, 2); Display (P); -- Named component association P := (X => 3, Y => 4, Z => 5); Display (P); end Show_Record_Aggregates;
Also, we can have a mixture of both:
with Points; use Points; procedure Show_Record_Aggregates is P : Point_3D; begin -- Positional and named component associations P := (3, 4, Z => 5); Display (P); end Show_Record_Aggregates;
In this case, only the Z
component has a named association, while the
other components have a positional association.
Note that a positional association cannot follow a named association, so we
cannot write P := (3, Y => 4, 5);
, for example. Once we start using a
named association for a component, we have to continue using it for the
remaining components.
In addition, we can choose multiple components at once and assign the same value
to them. For that, we use the |
syntax:
with Points; use Points; procedure Show_Record_Aggregates is P : Point_3D; begin -- Multiple component selection P := (X | Y => 5, Z => 6); Display (P); end Show_Record_Aggregates;
Here, we assign 5 to both X
and Y
.
In the Ada Reference Manual
<>
We can use the <>
syntax to tell the compiler to use the default value
for specific components. However, if there's no default value for specific
components, that component isn't initialized to a known value. For example:
with Points; use Points; procedure Show_Record_Aggregates is P : Point_3D; begin P := (0, 1, 2); Display (P); -- Specifying X component. P := (X => 42, Y => <>, Z => <>); Display (P); -- Specifying Y and Z components. P := (X => <>, Y => 10, Z => 20); Display (P); end Show_Record_Aggregates;
Here, as the components of Point_3D
don't have a default value, those
components that have <>
are not initialized:
when we write
(X => 42, Y => <>, Z => <>)
, onlyX
is initialized;when we write
(X => <>, Y => 10, Z => 20)
instead, onlyX
is uninitialized.
For further reading...
As we've just seen, all components that get a <>
are uninitialized
because the components of Point_3D
don't have a default value.
As no initialization is taking place for those components of the aggregate,
the actual value that is assigned to the record is undefined. In other
words, the resulting behavior might dependent on the compiler's
implementation.
When using GNAT, writing (X => 42, Y => <>, Z => <>)
keeps the value
of Y
and Z
intact, while (X => <>, Y => 10, Z => 20)
keeps the value of X
intact.
If the components of Point_3D
had default values, those would have been
used. For example, we may change the type declaration of Point_3D
and use
default values for each component:
package Points is type Point_3D is record X : Integer := 10; Y : Integer := 20; Z : Integer := 30; end record; procedure Display (P : Point_3D); end Points;
Then, writing <>
makes use of those default values we've just specified:
with Points; use Points; procedure Show_Record_Aggregates is P : Point_3D := (0, 0, 0); begin -- Using default value for -- all components P := (X => <>, Y => <>, Z => <>); Display (P); end Show_Record_Aggregates;
Now, as expected, the default values of each component (10, 20 and 30) are used
when we write <>
.
Similarly, we can specify a default value for the type of each component. For
example, let's declare a Point_Value
type with a default value —
using the Default_Value
aspect — and use it in the Point_3D
record type:
package Points is type Point_Value is new Float with Default_Value => 99.9; type Point_3D is record X : Point_Value; Y : Point_Value; Z : Point_Value; end record; procedure Display (P : Point_3D); end Points;with Ada.Text_IO; use Ada.Text_IO; package body Points is procedure Display (P : Point_3D) is begin Put_Line ("(X => " & Point_Value'Image (P.X) & ","); Put_Line (" Y => " & Point_Value'Image (P.Y) & ","); Put_Line (" Z => " & Point_Value'Image (P.Z) & ")"); end Display; end Points;
Then, writing <>
makes use of the default value of the Point_Value
type:
with Points; use Points; procedure Show_Record_Aggregates is P : Point_3D := (0.0, 0.0, 0.0); begin -- Using default value of Point_Value -- for all components P := (X => <>, Y => <>, Z => <>); Display (P); end Show_Record_Aggregates;
In this case, the default value of the Point_Value
type (99.9) is used
for all components when we write <>
.
others
Also, we can use the others
selector to assign a value to all components
that aren't explicitly mentioned in the aggregate. For example:
with Points; use Points; procedure Show_Record_Aggregates is P : Point_3D; begin -- Specifying X component; -- using 42 for all -- other components. P := (X => 42, others => 100); Display (P); -- Specifying all components P := (others => 256); Display (P); end Show_Record_Aggregates;
When we write P := (X => 42, others => 100)
, we're assigning 42 to
X
and 100 to all other components (Y
and Z
in this case).
Also, when we write P := (others => 256)
, all components have the
same value (256).
Note that writing a specific value in others
— such as
(others => 256)
— only works when all components have the same
type. In this example, all components of Point_3D
have the same type:
Integer
. If we had components with different types in the components
selected by others
, say Integer
and Float
, then
(others => 256)
would trigger a compilation error. For example, consider
this package:
package Custom_Records is type Integer_Float is record A, B : Integer := 0; Y, Z : Float := 0.0; end record; end Custom_Records;
If we had written an aggregate such as (others => 256)
for an object of
type Integer_Float
, the value (256) would be OK for components A
and B
, but not for components Y
and Z
:
with Custom_Records; use Custom_Records; procedure Show_Record_Aggregates_Others is Dummy : Integer_Float; begin -- ERROR: components selected by -- others must be of same -- type. Dummy := (others => 256); end Show_Record_Aggregates_Others;
We can fix this compilation error by making sure that others
only refers
to components of the same type:
with Custom_Records; use Custom_Records; procedure Show_Record_Aggregates_Others is Dummy : Integer_Float; begin -- OK: components selected by -- others have Integer type. Dummy := (Y | Z => 256.0, others => 256); end Show_Record_Aggregates_Others;
In any case, writing (others => <>)
is always accepted by the compiler
because it simply selects the default value of each component, so the type of
those values is unambiguous:
with Custom_Records; use Custom_Records; procedure Show_Record_Aggregates_Others is Dummy : Integer_Float; begin Dummy := (others => <>); end Show_Record_Aggregates_Others;
This code compiles because <>
uses the appropriate default value of each
component.
Record discriminants
When a record type has discriminants, they must appear as components of an aggregate of that type. For example, consider this package:
package Points is type Point_Dimension is (Dim_1, Dim_2, Dim_3); type Point (D : Point_Dimension) is record case D is when Dim_1 => X1 : Integer; when Dim_2 => X2, Y2 : Integer; when Dim_3 => X3, Y3, Z3 : Integer; end case; end record; procedure Display (P : Point); end Points;with Ada.Text_IO; use Ada.Text_IO; package body Points is procedure Display (P : Point) is begin Put_Line (Point_Dimension'Image (P.D)); case P.D is when Dim_1 => Put_Line (" (X => " & Integer'Image (P.X1) & ")"); when Dim_2 => Put_Line (" (X => " & Integer'Image (P.X2) & ","); Put_Line (" Y => " & Integer'Image (P.Y2) & ")"); when Dim_3 => Put_Line (" (X => " & Integer'Image (P.X3) & ","); Put_Line (" Y => " & Integer'Image (P.Y3) & ","); Put_Line (" Z => " & Integer'Image (P.Z3) & ")"); end case; end Display; end Points;
To write aggregates of the Point
type, we have to specify the D
discriminant as a component of the aggregate. The discriminant must be included
in the aggregate — and must be static — because the compiler must
be able to examine the aggregate to determine if it is both complete and
consistent. All components must be accounted for one way or another, as usual
— but, in addition, references to those components whose existence
depends on the discriminant's values must be consistent with the actual
discriminant value used in the aggregate. For example, for type Point
,
an aggregate can only reference the X3
, Y3
, and Z3
components when Dim_3
is specified for the discriminant D
;
otherwise, those three components don't exist in that aggregate. Also, the
discriminant D
must be the first one if we use positional component
association. For example:
with Points; use Points; procedure Show_Rec_Aggregate_Discriminant is -- Positional component association P1 : constant Point := (Dim_1, 0); -- Named component association P2 : constant Point := (D => Dim_2, X2 => 3, Y2 => 4); -- Positional / named component association P3 : constant Point := (Dim_3, X3 => 3, Y3 => 4, Z3 => 5); begin Display (P1); Display (P2); Display (P3); end Show_Rec_Aggregate_Discriminant;
As we see in this example, we can use any component association in the aggregate, as long as we make sure that the discriminants of the type appear as components — and are the first components in the case of positional component association.
Null records
A null record is a record that doesn't have any components. Consequently, it
cannot store any information. When declaring a null record, we simply
write null
instead of declaring actual components, as we usually do for
records. For example:
package Null_Recs is type Null_Record is record null; end record; end Null_Recs;
Note that the syntax can be simplified to is null record
, which is much
more common than the previous form:
package Null_Recs is type Null_Record is null record; end Null_Recs;
Although a null record doesn't have components, we can still specify subprograms for it. For example, we could specify an addition operation for it:
package Null_Recs is type Null_Record is null record; function "+" (A, B : Null_Record) return Null_Record; end Null_Recs;package body Null_Recs is function "+" (A, B : Null_Record) return Null_Record is pragma Unreferenced (A, B); begin return (null record); end "+"; end Null_Recs;with Null_Recs; use Null_Recs; procedure Show_Null_Rec is A, B : Null_Record; begin B := A + A; A := A + B; end Show_Null_Rec;
In the Ada Reference Manual
Simple Prototyping
A null record doesn't provide much functionality on itself, as we're not storing any information in it. However, it's far from being useless. For example, we can make use of null records to design an API, which we can then use in an application without having to implement the actual functionality of the API. This allows us to design a prototype without having to think about all the implementation details of the API in the first stage.
Consider this example:
package Devices is type Device is private; function Create (Active : Boolean) return Device; procedure Reset (D : out Device) is null; procedure Process (D : in out Device) is null; procedure Activate (D : in out Device) is null; procedure Deactivate (D : in out Device) is null; private type Device is null record; function Create (Active : Boolean) return Device is (null record); end Devices;with Ada.Text_IO; use Ada.Text_IO; with Devices; use Devices; procedure Show_Device is A : Device; begin Put_Line ("Creating device..."); A := Create (Active => True); Put_Line ("Processing on device..."); Process (A); Put_Line ("Deactivating device..."); Deactivate (A); Put_Line ("Activating device..."); Activate (A); Put_Line ("Resetting device..."); Reset (A); end Show_Device;
In the Devices
package, we're declaring the Device
type and its
primitive subprograms: Create
, Reset
, Process
,
Activate
and Deactivate
. This is the API that we use in our
prototype. Note that, although the Device
type is declared as a private
type, it's still defined as a null record in the full view.
In this example, the Create
function, implemented as an expression
function in the private part, simply returns a null record. As expected, this
null record returned by Create
matches the definition of the
Device
type.
All procedures associated with the Device
type are implemented as null
procedures, which means they don't actually have an implementation nor have any
effect. We'll discuss this topic
later on in the course.
In the Show_Device
procedure — which is an application
that implements our prototype —, we declare an object of Device
type and call all subprograms associated with that type.
Extending the prototype
Because we're either using expression functions or null procedures in the
specification of the Devices
package, we don't have a package body for
it (as there's nothing to be implemented). We could, however, move those user
messages from the Show_Devices
procedure to a dummy implementation of
the Devices
package. This is the adapted code:
package Devices is type Device is null record; function Create (Active : Boolean) return Device; procedure Reset (D : out Device); procedure Process (D : in out Device); procedure Activate (D : in out Device); procedure Deactivate (D : in out Device); end Devices;with Ada.Text_IO; use Ada.Text_IO; package body Devices is function Create (Active : Boolean) return Device is pragma Unreferenced (Active); begin Put_Line ("Creating device..."); return (null record); end Create; procedure Reset (D : out Device) is pragma Unreferenced (D); begin Put_Line ("Processing on device..."); end Reset; procedure Process (D : in out Device) is pragma Unreferenced (D); begin Put_Line ("Deactivating device..."); end Process; procedure Activate (D : in out Device) is pragma Unreferenced (D); begin Put_Line ("Activating device..."); end Activate; procedure Deactivate (D : in out Device) is pragma Unreferenced (D); begin Put_Line ("Resetting device..."); end Deactivate; end Devices;with Devices; use Devices; procedure Show_Device is A : Device; begin A := Create (Active => True); Process (A); Deactivate (A); Activate (A); Reset (A); end Show_Device;
As we changed the specification of the Devices
package to not use null
procedures, we now need a corresponding package body for it. In this package
body, we implement the operations on the Device
type, which actually
just display a user message indicating which operation is being called.
Let's focus on this updated version of the Show_Device
procedure. Now
that we've removed all those calls to Put_Line
from this procedure and
just have the calls to operations associated with the Device
type, it
becomes more apparent that, even though Device
is just a null record, we
can design an application with a sequence of various commands operating on it.
Also, when we just read the source-code of the Show_Device
procedure,
there's no clear indication that the Device
type doesn't actually hold
any information.
More complex applications
As we've just seen, we can use null records like any other type and create complex prototypes with them. We could, for instance, design an application that makes use of many null records, or even have types that depend on or derive from null records. Let's see a simple example:
package Many_Devices is type Device is null record; type Device_Config is null record; function Create (Config : Device_Config) return Device is (null record); type Derived_Device is new Device; procedure Process (D : Derived_Device) is null; end Many_Devices;with Many_Devices; use Many_Devices; procedure Show_Derived_Device is A : Device; B : Derived_Device; C : Device_Config; begin A := Create (Config => C); B := Create (Config => C); Process (B); end Show_Derived_Device;
In this example, the Create
function has a null record parameter
(of Device_Config
type) and returns a null record (of Device
type). Also, we derive the Derived_Device
type from the Device
type. Consequently, Derived_Device
is also a null record (since it's
derived from a null record). In the Show_Derived_Device
procedure, we
declare objects of those types (A
, B
and C
) and call
primitive subprograms to operate on them.
This example shows that, even though the types we've declared are just null records, they can still be used to represent dependencies in our application.
Implementing the API
Let's focus again on the previous example. After we have an initial prototype,
we can start implementing some of the functionality needed for the
Device
type. For example, we can store information about the current
activation state in the record:
package Devices is type Device is private; function Create (Active : Boolean) return Device; procedure Reset (D : out Device); procedure Process (D : in out Device); procedure Activate (D : in out Device); procedure Deactivate (D : in out Device); private type Device is record Active : Boolean; end record; end Devices;with Ada.Text_IO; use Ada.Text_IO; package body Devices is function Create (Active : Boolean) return Device is pragma Unreferenced (Active); begin Put_Line ("Creating device..."); return (Active => Active); end Create; procedure Reset (D : out Device) is pragma Unreferenced (D); begin Put_Line ("Processing on device..."); end Reset; procedure Process (D : in out Device) is pragma Unreferenced (D); begin Put_Line ("Deactivating device..."); end Process; procedure Activate (D : in out Device) is begin Put_Line ("Activating device..."); D.Active := True; end Activate; procedure Deactivate (D : in out Device) is begin Put_Line ("Resetting device..."); D.Active := False; end Deactivate; end Devices;with Ada.Text_IO; use Ada.Text_IO; with Devices; use Devices; procedure Show_Device is A : Device; begin A := Create (Active => True); Process (A); Deactivate (A); Activate (A); Reset (A); end Show_Device;
Now, the Device
record contains an Active
component, which is
used in the updated versions of Create
, Activate
and
Deactivate
.
Note that we haven't done any change to the implementation of the
Show_Device
procedure: it's still the same application as before. As
we've been hinting in the beginning, using null records makes it easy for us to
first create a prototype — as we did in the Show_Device
procedure
— and postpone the API implementation to a later phase of the project.
Tagged null records
A null record may be tagged, as we can see in this example:
package Null_Recs is type Tagged_Null_Record is tagged null record; type Abstract_Tagged_Null_Record is abstract tagged null record; end Null_Recs;
As we see in this example, a type can be tagged
, or even
abstract tagged
. We discuss abstract types
later on in the course.
As expected, in addition to deriving from tagged types, we can also extend them. For example:
package Devices is type Device is private; function Create (Active : Boolean) return Device; type Derived_Device is private; private type Device is tagged null record; function Create (Active : Boolean) return Device is (null record); type Derived_Device is new Device with record Active : Boolean; end record; function Create (Active : Boolean) return Derived_Device is (Active => Active); end Devices;
In this example, we derive Derived_Device
from the Device
type
and extend it with the Active
component. (Because we have a type
extension, we also need to override the Create
function.)
Since we're now introducing elements from object-oriented programming, we could consider using interfaces instead of null records. We'll discuss this topic later on in the course.
Full coverage rules for Aggregates
Note
This section was originally written by Robert A. Duff and published as Gem #1: Limited Types in Ada 2005.
One interesting feature of Ada are the full coverage rules for aggregates. For example, suppose we have a record type:
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; package Persons is type Years is new Natural; type Person is record Name : Unbounded_String; Age : Years; end record; end Persons;
We can create an object of the type using an aggregate:
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; with Persons; use Persons; procedure Show_Aggregate_Init is X : constant Person := (Name => To_Unbounded_String ("John Doe"), Age => 25); begin null; end Show_Aggregate_Init;
The full coverage rules say that every component of Person
must be
accounted for in the aggregate. If we later modify type Person
by
adding a component:
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; package Persons is type Years is new Natural; type Person is record Name : Unbounded_String; Age : Natural; Shoe_Size : Positive; end record; end Persons;
and we forget to modify X
accordingly, the compiler will remind us.
Case statements also have full coverage rules, which serve a similar
purpose.
Of course, we can defeat the full coverage rules by using others
(usually for array aggregates and case
statements, but occasionally useful for
record aggregates):
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; with Persons; use Persons; procedure Show_Aggregate_Init_Others is X : constant Person := (Name => To_Unbounded_String ("John Doe"), others => 25); begin null; end Show_Aggregate_Init_Others;
According to the Ada RM, others
here means precisely the same thing
as Age | Shoe_Size
. But that's wrong: what others
really
means is "all the other components, including the ones we might add next
week or next year". That means you shouldn't use others
unless
you're pretty sure it should apply to all the cases that haven't been
invented yet.
Later on, we'll discuss full coverage rules for limited types.
Array aggregates
We've already discussed array aggregates in the Introduction to Ada course. Therefore, this section just presents some details about this topic.
In the Ada Reference Manual
Positional and named array aggregates
Note
The array aggregate syntax using brackets (e.g.: [1, 2, 3]
), which we
mention in this section, was introduced in Ada 2022.
Similar to record aggregates, array aggregates can be positional or named. Consider this package:
package Points is type Point_3D is array (1 .. 3) of Integer; procedure Display (P : Point_3D); end Points;pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; package body Points is procedure Display (P : Point_3D) is begin Put_Line ("(X => " & Integer'Image (P (1)) & ","); Put_Line (" Y => " & Integer'Image (P (2)) & ","); Put_Line (" Z => " & Integer'Image (P (3)) & ")"); end Display; end Points;
We can write positional or named aggregates when assigning to an object P
of Point_3D
type:
pragma Ada_2022; with Points; use Points; procedure Show_Array_Aggregates is P : Point_3D; begin -- Positional component association P := [0, 1, 2]; Display (P); -- Named component association P := [1 => 3, 2 => 4, 3 => 5]; Display (P); end Show_Array_Aggregates;
In this example, we assign a positional array aggregate ([1, 2, 3]
) to
P
. Then, we assign a named array aggregate
([1 => 3, 2 => 4, 3 => 5]
) to P
. In this case, the names are
the indices of the components we're assigning to.
We can also assign array aggregates to slices:
pragma Ada_2022; with Points; use Points; procedure Show_Array_Aggregates is P : Point_3D := [others => 0]; begin -- Positional component association P (2 .. 3) := [1, 2]; Display (P); -- Named component association P (2 .. 3) := [1 => 3, 2 => 4]; Display (P); end Show_Array_Aggregates;
Note that, when using a named array aggregate, the index (name) that we use
in the aggregate doesn't have to match the slice. In this example, we're
assigning the component from index 1 of the aggregate to the component of index
2 of the array P
(and so on).
Historically
In the first versions of Ada, we could only write array aggregates using parentheses.
pragma Ada_2012; with Points; use Points; procedure Show_Array_Aggregates is P : Point_3D; begin -- Positional component association P := (0, 1, 2); Display (P); -- Named component association P := (1 => 3, 2 => 4, 3 => 5); Display (P); end Show_Array_Aggregates;This syntax is considered obsolescent since Ada 2022: brackets (
[1, 2, 3]
) should be used instead.
Null array aggregate
Note
This feature was introduced in Ada 2022.
We can also write null array aggregates: []
. As the name implies, this
kind of array aggregate doesn't have any components.
Consider this package:
package Integer_Arrays is type Integer_Array is array (Positive range <>) of Integer; procedure Display (A : Integer_Array); end Integer_Arrays;pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; package body Integer_Arrays is procedure Display (A : Integer_Array) is begin Put_Line ("Length = " & A'Length'Image); Put_Line ("("); for I in A'Range loop Put (" " & I'Image & " => " & A (I)'Image); if I /= A'Last then Put_Line (","); else New_Line; end if; end loop; Put_Line (")"); end Display; end Integer_Arrays;
We can initialize an object N
of Integer_Array
type with a null
array aggregate:
pragma Ada_2022; with Integer_Arrays; use Integer_Arrays; procedure Show_Array_Aggregates is N : constant Integer_Array := []; begin Display (N); end Show_Array_Aggregates;
In this example, when we call the Display
procedure, we confirm that
N
doesn't have any components.
|
, <>
, others
We've seen the following syntactic elements when we were discussing
record aggregates: |
, <>
and
others
. We can apply them to array aggregates as well:
pragma Ada_2022; with Points; use Points; procedure Show_Array_Aggregates is P : Point_3D; begin -- All components have a value of zero. P := [others => 0]; Display (P); -- Both first and second components have -- a value of three. P := [1 | 2 => 3, 3 => 4]; Display (P); -- The default value is used for the first -- component, and all other components -- have a value of five. P := [1 => <>, others => 5]; Display (P); end Show_Array_Aggregates;
In this example, we use the |
, <>
and others
elements in a
very similar way as we did with record aggregates. (See the comments in the code
example for more details.)
Note that, as for record aggregates, the <>
makes use of the default
value (if it is available). We discuss this topic in more details
later on.
..
We can also use the range syntax (..
) with array aggregates:
pragma Ada_2022; with Points; use Points; procedure Show_Array_Aggregates is P : Point_3D; begin -- All components have a value of zero. P := [1 .. 3 => 0]; Display (P); -- Both first and second components have -- a value of three. P := [1 .. 2 => 3, 3 => 4]; Display (P); -- The default value is used for the first -- component, and all other components -- have a value of five. P := [1 => <>, 2 .. 3 => 5]; Display (P); end Show_Array_Aggregates;
This example is a variation of the previous one. However, in this case, we're
using ranges instead of the |
and others
syntax.
Missing components
All aggregate components must have an associated value. If we don't specify a value for a certain component, an exception is raised:
pragma Ada_2022; with Points; use Points; procedure Show_Array_Aggregates is P : Point_3D; begin P := [1 => 4]; -- ERROR: value of components at indices -- 2 and 3 are missing Display (P); end Show_Array_Aggregates;
We can use others
to specify a value to all components that
haven't been explicitly mentioned in the aggregate:
pragma Ada_2022; with Points; use Points; procedure Show_Array_Aggregates is P : Point_3D; begin P := [1 => 4, others => 0]; -- OK: unspecified components have a -- value of zero Display (P); end Show_Array_Aggregates;
However, others
can only be used when the range is known —
compilation fails otherwise:
pragma Ada_2022; with Integer_Arrays; use Integer_Arrays; procedure Show_Array_Aggregates is N1 : Integer_Array := [others => 0]; -- ERROR: range is unknown N2 : Integer_Array (1 .. 3) := [others => 0]; -- OK: range is known begin Display (N1); Display (N2); end Show_Array_Aggregates;
Of course, we could fix the declaration of N1
by specifying a range
— e.g. N1 : Integer_Array (1 .. 10) := [others => 0];
.
Iterated component association
Note
This feature was introduced in Ada 2022.
We can use an iterated component association to specify an aggregate. This is the general syntax:
-- All components have a value of zero
P := [for I in 1 .. 3 => 0];
Let's see a complete example:
pragma Ada_2022; with Points; use Points; procedure Show_Array_Aggregates is P : Point_3D; begin -- All components have a value of zero P := [for I in 1 .. 3 => 0]; Display (P); -- Both first and second components have -- a value of three P := [for I in 1 .. 3 => (if I = 1 or I = 2 then 3 else 4)]; Display (P); -- The first component has a value of 99 -- and all other components have a value -- that corresponds to its index P := [1 => 99, for I in 2 .. 3 => I]; Display (P); end Show_Array_Aggregates;
In this example, we use iterated component associations in different ways:
We write a simple iteration (
[for I in 1 .. 3 => 0]
).We use a conditional expression in the iteration:
[for I in 1 .. 3 => (if I = 1 or I = 2 then 3 else 4)]
.We use a named association for the first element, and then iterated component association for the remaining components:
[1 => 99, for I in 2 .. 3 => I]
.
So far, we've used a discrete choice list (in the for I in Range
form) in
the iterated component association. We could use an iterator (in the
for E of
form) instead. For example:
pragma Ada_2022; with Points; use Points; procedure Show_Array_Aggregates is P : Point_3D := [for I in Point_3D'Range => I]; begin -- Each component is doubled P := [for E of P => E * 2]; Display (P); -- Each component is increased -- by one P := [for E of P => E + 1]; Display (P); end Show_Array_Aggregates;
In this example, we use iterators in different ways:
We write
[for E of P => E * 2]
to double the value of each component.We write
[for E of P => E + 1]
to increase the value of each component by one.
Of course, we could write more complex operations on E
in the iterators.
Multidimensional array aggregates
So far, we've discussed one-dimensional array aggregates. We can also use the same constructs when dealing with multidimensional arrays. Consider, for example, this package:
package Matrices is type Matrix is array (Positive range <>, Positive range <>) of Integer; procedure Display (M : Matrix); end Matrices;pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; package body Matrices is procedure Display (M : Matrix) is procedure Display_Row (M : Matrix; I : Integer) is begin Put_Line (" ("); for J in M'Range (2) loop Put (" " & J'Image & " => " & M (I, J)'Image); if J /= M'Last (2) then Put_Line (","); else New_Line; end if; end loop; Put (" )"); end Display_Row; begin Put_Line ("Length (1) = " & M'Length (1)'Image); Put_Line ("Length (2) = " & M'Length (2)'Image); Put_Line ("("); for I in M'Range (1) loop Display_Row (M, I); if I /= M'Last (1) then Put_Line (","); else New_Line; end if; end loop; Put_Line (")"); end Display; end Matrices;
We can assign multidimensional aggregates to a matrix M
using
positional or named component association:
pragma Ada_2022; with Matrices; use Matrices; procedure Show_Array_Aggregates is M : Matrix (1 .. 2, 1 .. 3); begin -- Positional component association M := [[0, 1, 2], [3, 4, 5]]; Display (M); -- Named component association M := [[1 => 3, 2 => 4, 3 => 5], [1 => 6, 2 => 7, 3 => 8]]; Display (M); end Show_Array_Aggregates;
The first aggregate we use in this example is [[0, 1, 2], [3, 4, 5]]
.
Here, [0, 1, 2]
and [3, 4, 5]
are subaggregates of the
multidimensional aggregate. Subaggregates don't have a type themselves, but are
rather just considered part of a multidimensional aggregate (which, of course,
has an array type). In this sense, a subaggregate such as [0, 1, 2]
is
different from a one-dimensional aggregate (such as [0, 1, 2]
), even
though they are written in the same way.
Strings in subaggregates
In the case of matrices using characters, we can use strings in the corresponding array aggregates. Consider this package:
package String_Lists is type String_List is array (Positive range <>, Positive range <>) of Character; procedure Display (SL : String_List); end String_Lists;pragma Ada_2022; with Ada.Text_IO; use Ada.Text_IO; package body String_Lists is procedure Display (SL : String_List) is procedure Display_Row (SL : String_List; I : Integer) is begin Put (" ("); for J in SL'Range (2) loop Put (SL (I, J)); end loop; Put (")"); end Display_Row; begin Put_Line ("Length (1) = " & SL'Length (1)'Image); Put_Line ("Length (2) = " & SL'Length (2)'Image); Put_Line ("("); for I in SL'Range (1) loop Display_Row (SL, I); if I /= SL'Last (1) then Put_Line (","); else New_Line; end if; end loop; Put_Line (")"); end Display; end String_Lists;
Then, when assigning to an object SL
of String_List
type, we can
use strings in the aggregates:
pragma Ada_2022; with String_Lists; use String_Lists; procedure Show_Array_Aggregates is SL : String_List (1 .. 2, 1 .. 3); begin -- Positional component association SL := ["ABC", "DEF"]; Display (SL); -- Named component associations SL := [[1 => 'A', 2 => 'B', 3 => 'C'], [1 => 'D', 2 => 'E', 3 => 'F']]; Display (SL); SL := [[1 => 'X', 2 => 'Y', 3 => 'Z'], [others => ' ']]; Display (SL); end Show_Array_Aggregates;
In the first assignment to SL
, we have the aggregate
["ABC", "DEF"]
, which uses strings as subaggregates. (Of course, we can
use a named aggregate and assign characters to the individual components.)
<>
and default values
As we indicated earlier, the <>
syntax sets a component to its default
value — if such a default value is available. If a default value isn't
defined, however, the component will remain uninitialized, so that the behavior
is undefined. Let's look at more complex example to illustrate this situation.
Consider this package, for example:
package Points is subtype Point_Value is Integer; type Point_3D is record X, Y, Z : Point_Value; end record; procedure Display (P : Point_3D); type Point_3D_Array is array (Positive range <>) of Point_3D; procedure Display (PA : Point_3D_Array); end Points;with Ada.Text_IO; use Ada.Text_IO; package body Points is procedure Display (P : Point_3D) is begin Put (" (X => " & Point_Value'Image (P.X) & ","); New_Line; Put (" Y => " & Point_Value'Image (P.Y) & ","); New_Line; Put (" Z => " & Point_Value'Image (P.Z) & ")"); end Display; procedure Display (PA : Point_3D_Array) is begin Put_Line ("("); for I in PA'Range (1) loop Put_Line (" " & Integer'Image (I) & " =>"); Display (PA (I)); if I /= PA'Last (1) then Put_Line (","); else New_Line; end if; end loop; Put_Line (")"); end Display; end Points;
Then, let's use <>
for the array components:
pragma Ada_2022; with Points; use Points; procedure Show_Record_Aggregates is PA : Point_3D_Array (1 .. 2); begin PA := [ (X => 3, Y => 4, Z => 5), (X => 6, Y => 7, Z => 8) ]; Display (PA); -- Array components are -- uninitialized. PA := [1 => <>, 2 => <>]; Display (PA); end Show_Record_Aggregates;
Because the record components (of the Point_3D
type) don't have default
values, they remain uninitialized when we write [1 => <>, 2 => <>]
.
(In fact, you may see garbage in the values displayed by the Display
procedure.)
When a default value is specified, it is used whenever <>
is
specified. For example, we could use a type that has the Default_Value
aspect in its specification:
package Integer_Arrays is type Value is new Integer with Default_Value => 99; type Integer_Array is array (Positive range <>) of Value; procedure Display (A : Integer_Array); end Integer_Arrays;pragma Ada_2022; with Integer_Arrays; use Integer_Arrays; procedure Show_Array_Aggregates is N : Integer_Array (1 .. 4); begin N := [for I in N'Range => Value (I)]; Display (N); N := [others => <>]; Display (N); end Show_Array_Aggregates;
When writing an aggregate for the Point_3D
type, any component that has
<>
gets the default value of the Point
type (99):
For further reading...
Similarly, we could specify the Default_Component_Value
aspect
(which we discussed earlier on)
in the declaration of the array type:
package Integer_Arrays is type Value is new Integer; type Integer_Array is array (Positive range <>) of Value with Default_Component_Value => 9999; procedure Display (A : Integer_Array); end Integer_Arrays;pragma Ada_2022; with Integer_Arrays; use Integer_Arrays; procedure Show_Array_Aggregates is N : Integer_Array (1 .. 4); begin N := [for I in N'Range => Value (I)]; Display (N); N := [others => <>]; Display (N); end Show_Array_Aggregates;