Containers are based on generic packages, so we can't simply declare
a vector as we would declare an array of a specific type:

A:array(1..10)ofInteger;

Instead, we first need to instantiate one of those packages. We
with the container package (Ada.Containers.Vectors in this
case) and instantiate it to create an instance of the generic package for
the desired type. Only then can we declare the vector using the type from
the instantiated package. This instantiation needs to be done for any
container type from the standard library.

In the instantiation of Integer_Vectors, we indicate that the vector
contains elements of Integer type by specifying it as the
Element_Type. By setting Index_Type to Natural, we specify
that the allowed range includes all natural numbers. We could have used a
more restrictive range if desired.

We specify useInteger_Vectors, so we have direct access to the
types and operations from the instantiated package. Also, the example
introduces another operation on the vector: Length, which
retrieves the number of elements in the vector. We can use the dot
notation because Vector is a tagged type, allowing us to write
either V.Length or Length(V).

You can swap elements by calling the procedure Swap and retrieving a
reference (a cursor) to the first and last elements of the vector by
calling First and Last. A cursor allows us to iterate over a
container and process individual elements from it.

With these operations, we're able to write code to swap the first and last
elements of a vector:

Because we're given a reference, we can display not only the value of an
element but also modify it. For example, we could easily write a loop to
add one to each element of vector V:

forEofVloopE:=E+1;endloop;

We can also use indices to access vector elements. The format is
similar to a loop over array elements: we use a forIin<range> loop. The range is provided by V.First_Index and
V.Last_Index. We can access the current element by using it as an
array index: V(I). For example:

with Ada.Containers.Vectors;
with Ada.Text_IO; use Ada.Text_IO;
procedure Show_Vector_Index_Iteration is
package Integer_Vectors is new Ada.Containers.Vectors
(Index_Type => Natural,
Element_Type => Integer);
use Integer_Vectors;
V : Vector := 20 & 10 & 0 & 13;
begin
Put_Line ("Vector elements are: ");
--
-- Using indices in a "for I in ..." loop to iterate:
--
for I in V.First_Index .. V.Last_Index loop
-- Displaying current index I
Put ("- ["
& Extended_Index'Image (I)
& "] ");
Put (Integer'Image (V (I)));
-- We could also use the V.Element (I) function to retrieve the
-- element at the current index I
New_Line;
end loop;
end Show_Vector_Index_Iteration;

Here, in addition to displaying the vector elements, we're also
displaying each index, I, just like what we can do for array
indices. Also, we can access the element by using either the short
form V(I) or the longer form V.Element(I) but not V.I.

As mentioned in the previous section, you can use cursors to iterate over
containers. For this, use the function Iterate, which retrieves a
cursor for each position in the vector. The corresponding loop has the
format forCinV.Iterateloop. Like the previous example using
indices, you can again access the current element by using the cursor as an
array index: V(C). For example:

Instead of accessing an element in the loop using V(C), we could
also have used the longer form Element(C). In this example, we're
using the function To_Index to retrieve the index corresponding to
the current cursor.

As shown in the comments after the loop, we could also use a
while...loop to iterate over the vector. In this case, we
would start with a cursor for the first element (retrieved by calling
V.First) and then call Next(C) to retrieve a cursor for
subsequent elements. Next(C) returns No_Element when the
cursor reaches the end of the vector.

You can directly modify the elements using a reference. This is what it
looks like when using both indices and cursors:

The Reference Manual requires that the worst-case complexity for
accessing an element be O(\(log N\)).

Another way of modifing elements of a vector is using a process
procedure, which takes an individual element and does some processing on
it. You can call Update_Element and pass both a cursor and an access
to the process procedure. For example:

You can locate a specific element in a vector by retrieving its index.
Find_Index retrieves the index of the first element matching the value
you're looking for. Alternatively, you can use Find to retrieve a
cursor referencing that element. For example:

As we saw in the previous section, we can directly access vector elements
by using either an index or cursor. However, an exception is raised if we
try to access an element with an invalid index or cursor, so we must check
whether the index or cursor is valid before using it to access an element.
In our example, Find_Index or Find might not have found the element
in the vector. We check for this possibility by comparing the index to
No_Index or the cursor to No_Element. For example:

You can remove elements from a vector by passing either a valid index or
cursor to the Delete procedure. If we combine this with the functions
Find_Index and Find from the previous section, we can write a
program that searches for a specific element and deletes it, if found:

We've seen some operations on vector elements. Here, we'll see operations
on the vector as a whole. The most prominent is the concatenation of
multiple vectors, but we'll also see operations on vectors, such as sorting
and sorted merging operations, that view the vector as a sequence of
elements and operate on the vector considering the element's relations to
each other.

We do vector concatenation using the & operator on vectors. Let's
consider two vectors V1 and V2. We can concatenate them by doing
V:=V1&V2. V contains the resulting vector.

The generic package Generic_Sorting is a child package of
Ada.Containers.Vectors. It contains sorting and merging operations.
Because it's a generic package, you can't use it directly, but have to
instantiate it. In order to use these operations on a vector of integer
values (Integer_Vectors, in our example), you need to instantiate it
directly as a child of Integer_Vectors. The next example makes it clear
how to do this.

After instantiating Generic_Sorting, we make all the operations
available to us with the use statement. We can then call Sort to
sort the vector and Merge to merge one vector into another.

The following example presents code that manipulates three vectors (V1,
V2, V3) using the concatenation, sorting and merging operations:

Sets are another class of containers. While vectors allow duplicated
elements to be inserted, sets ensure that no duplicated elements exist.

In the following sections, we'll see operations you can perform on
sets. However, since many of the operations on vectors are similar to the
ones used for sets, we'll cover them more quickly here. Please refer back
to the section on vectors for a more detailed discussion.

To initialize a set, you can call the Insert procedure. However, if
you do, you need to ensure no duplicate elements are being inserted: if you
try to insert a duplicate, you'll get an exception. If you have less
control over the elements to be inserted so that there may be duplicates,
you can use another option instead:

a version of Insert that returns a Boolean value
indicating whether the insertion was successful;

the Include procedure, which silently ignores any attempt to
insert a duplicated element.

To iterate over a set, you can use a forEofS loop, as you saw for
vectors. This gives you a reference to each element in the set.

Let's see an example:

with Ada.Containers; use Ada.Containers;
with Ada.Containers.Ordered_Sets;
with Ada.Text_IO; use Ada.Text_IO;
procedure Show_Set_Init is
package Integer_Sets is new Ada.Containers.Ordered_Sets
(Element_Type => Integer);
use Integer_Sets;
S : Set;
-- Same as: S : Integer_Sets.Set;
C : Cursor;
Ins : Boolean;
begin
S.Insert (20);
S.Insert (10);
S.Insert (0);
S.Insert (13);
-- Calling S.Insert(0) now would raise Constraint_Error
-- because this element is already in the set.
-- We instead call a version of Insert that doesn't raise an
-- exception but instead returns a Boolean indicating the status
S.Insert (0, C, Ins);
if not Ins then
Put_Line ("Inserting 0 into set was not successful");
end if;
-- We can also call S.Include instead
-- If the element is already present, the set remains unchanged
S.Include (0);
S.Include (13);
S.Include (14);
Put_Line ("Set has " & Count_Type'Image (S.Length) & " elements");
--
-- Iterate over set using for .. of loop
--
Put_Line ("Elements:");
for E of S loop
Put_Line ("- " & Integer'Image (E));
end loop;
end Show_Set_Init;

To delete elements, you call the procedure Delete. However,
analogously to the Insert procedure above, Delete raises an
exception if the element to be deleted isn't present in the set. If you
want to permit the case where an element might not exist, you can call
Exclude, which silently ignores any attempt to delete a non-existent
element.

Contains returns a Boolean value indicating whether a value is
contained in the set. Find also looks for an element in a set, but
returns a cursor to the element or No_Element if the element doesn't
exist. You can use either function to search for elements in a set.

The previous sections mostly dealt with operations on individual elements
of a set. But Ada also provides typical set operations: union,
intersection, difference and symmetric difference. In contrast to some
vector operations we've seen before (e.g. Merge), here you can use
built-in operators, such as -. The following table lists the
operations and its associated operator:

The previous sections presented containers for elements of definite
types. Although most examples in those sections presented Integer
types as element type of the containers, containers can also be used with
indefinite types, an example of which is the String type. However,
indefinite types require a different kind of containers designed specially
for them.

We'll also be exploring a different class of containers: maps. They
associate a key with a specific value. An example of a map is the
one-to-one association between a person and their age. If we consider a
person's name to be the key, the value is the person's age.

Hashed maps are maps that make use of a hash as a key. The hash itself is
calculated by a function you provide.

In other languages

Hashed maps are similar to dictionaries in Python and hashes in Perl.
One of the main differences is that these scripting languages allow
using different types for the values contained in a single map, while
in Ada, both the type of key and value are specified in the package
instantiation and remains constant for that specific map. You can't
have a map where two elements are of different types or two keys are of
different types. If you want to use multiple types, you must create a
different map for each and use only one type in each map.

When instantiating a hashed map from
Ada.Containers.Indefinite_Hashed_Maps, we specify following elements:

Key_Type: type of the key

Element_Type: type of the element

Hash: hash function for the Key_Type

Equivalent_Keys: an equality operator (e.g. =) that indicates
whether two keys are to be considered equal.

If the type specified in Key_Type has a standard operator, you can
use it, which you do by specifing using that operator as the value of
Equivalent_Keys.

In the next example, we'll use a string as a key type. We'll use the
Hash function provided by the standard library for strings (in the
Ada.Strings package) and the standard equality operator.

You add elements to a hashed map by calling Insert. If an element is
already contained in a map M, you can access it directly by using its
key. For example, you can change the value of an element by calling M("My_Key"):=10. If the key is not found, an exception is raised. To
verify if a key is available, use the function Contains (as we've seen
above in the section on sets).

You can see a great similarity between the examples above and from the
previous section. In fact, since both kinds of maps share many operations,
we didn't need to make extensive modifications when we changed our example
to use ordered maps instead of hashed maps. The main difference is seen
when we run the examples: the output of a hashed map is usually unordered,
but the output of a ordered map is always ordered, as implied by its name.

Hashed maps are generally the fastest data structure available to you in
Ada if you need to associate heterogeneous keys to values and search for
them quickly. In most cases, they are slightly faster than ordered maps.
So if you don't need ordering, use hashed maps.

The Reference Manual requires the following average complexity of
operations:

The standard library supports representing and handling dates and
times. This is part of the Ada.Calendar package. Let's look at a
simple example:

with Ada.Calendar; use Ada.Calendar;
with Ada.Calendar.Formatting; use Ada.Calendar.Formatting;
with Ada.Text_IO; use Ada.Text_IO;
procedure Display_Current_Time is
Now : Time := Clock;
begin
Put_Line ("Current time: " & Image (Now));
end Display_Current_Time;

This example displays the current date and time, which is retrieved by a
call to the Clock function. We call the function Image from the
Ada.Calendar.Formatting package to get a String for the current
date and time. We could instead retrieve each component using the Split
function. For example:

In this example, we specify the date and time by initializing Next
using a call to Time_Of, a function taking the various components
of a date (year, month, etc) and returning an element of the Time
type. Because the date specified is in the past, the delayuntil statement won't produce any noticeable effect. However, if we
passed a date in the future, the program would wait until that
specific date and time arrived.

Here we're converting the time to the local timezone. If we don't specify a
timezone, Coordinated Universal Time (abbreviated to UTC) is used by
default. By retrieving the time offset to UTC with a call to
UTC_Time_Offset from the Ada.Calendar.Time_Zones package, we can
initialize TZ and use it in the call to Time_Of. This is all we
need do to make the information provided to Time_Of relative to the
local time zone.

We could achieve a similar result by initializing Next with a
String. We can do this with a call to Value from the
Ada.Calendar.Formatting package. This is the modified code:

In this example, we're again using TZ in the call to Value to
adjust the input time to the current time zone.

In the examples above, we were delaying to a specific date and time.
Just like we saw in the tasking chapter, we could instead specify the
delay relative to the current time. For example, we could delay by 5
seconds, using the current time:

with Ada.Calendar; use Ada.Calendar;
with Ada.Text_IO; use Ada.Text_IO;
procedure Display_Delay_Next is
D : Duration := 5.0; -- seconds
Now : Time := Clock;
Next : Time := Now + D; -- use duration to
-- specify next point in time
begin
Put_Line ("Let's wait "
& Duration'Image (D) & " seconds...");
delay until Next;
Put_Line ("Enough waiting!");
end Display_Delay_Next;

Here, we're specifying a duration of 5 seconds in D, adding it to the
current time from Now, and storing the sum in Next. We then use it
in the delayuntil statement.

In addition to Ada.Calendar, the standard library also supports time
operations for real-time applications. These are included in the
Ada.Real_Time package. This package also include a Time type.
However, in the Ada.Real_Time package, the Time type is used to
represent an absolute clock and handle a time span. This contrasts with the
Ada.Calendar, which uses the Time type to represent dates and
times.

In the previous section, we used the Time type from the
Ada.Calendar and the delayuntil statement to delay an
application by 5 seconds. We could have used the Ada.Real_Time
package instead. Let's modify that example:

The main difference is that D is now a variable of type Time_Span,
defined in the Ada.Real_Time package. We call the function
Seconds to initialize D, but could have gotten a finer granularity
by calling Nanoseconds instead. Also, we need to first convert D to
the Duration type using To_Duration before we can display it.

This example defines a dummy Computational_Intensive_App implemented
using a simple delay statement. We initialize Start_Time and
Stop_Time from the then-current clock and calculate the elapsed
time. By running this program, we see that the time is roughly 5 seconds,
which is expected due to the delay statement.

A similar application is benchmarking of CPU time. We can implement this
using the Execution_Time package. Let's modify the previous example
to measure CPU time:

In this example, Start_Time and Stop_Time are of type CPU_Time
instead of Time. However, we still call the Clock function to
initialize both variables and calculate the elapsed time in the same way as
before. By running this program, we see that the CPU time is significantly
lower than the 5 seconds we've seen before. This is because the
delay statement doesn't require much CPU time. The results will be
different if we change the implementation of
Computational_Intensive_App to use a mathematical functions in a long
loop. For example:

Operations on standard strings are available in the
Ada.Strings.Fixed package. As mentioned previously, standard strings
are arrays of elements of Character type with a
fixed-length. That's why this child package is called Fixed.

One of the simplest operations provided is counting the number of
substrings available in a string (Count) and finding their
corresponding indices (Index). Let's look at an example:

We initialize the string S using a multiplication. Writing
"Hello"&3*"World" creates the string HelloWorldWorldWorld. We then call the function Count to get the number of instances
of the word World in S. Next we call the function Index in a
loop to find the index of each instance of World in S.

That example looked for instances of a specific substring. In the next
example, we retrieve all the words in the string. We do this using
Find_Token and specifying whitespaces as separators. For example:

We pass a set of characters to be used as delimitators to the procedure
Find_Token. This set is a member of the Character_Set type from the
Ada.Strings.Maps package. We call the To_Set function (from the
same package) to initialize the set to Whitespace and then call
Find_Token to loop over each valid index and find the starting index of
each word. We pass Outside to the Test parameter of the
Find_Token procedure to indicate that we're looking for indices that
are outside the Whitespace set, i.e. actual words. The First and
Last parameters of Find_Token are output parameters that indicate
the valid range of the substring. We use this information to display the
string (S(F..L)).

The operations we've looked at so far read strings, but don't modify
them. We next discuss operations that change the content of strings:

Operation

Description

Insert

Insert substring in a string

Overwrite

Overwrite a string with a substring

Delete

Delete a substring

Trim

Remove whitespaces from a string

All these operations are available both as functions or procedures.
Functions create a new string but procedures perform the operations in
place. The procedure will raise an exception if the constraints of the
string are not satisfied. For example, if we have a string S containing
10 characters, inserting a string with two characters (e.g. "!!") into
it produces a string containing 12 characters. Since it has a fixed length,
we can't increase its size. One possible solution in this case is to
specify that truncation should be applied while inserting the substring.
This keeps the length of S fixed. Let's see an example that makes use
of both function and procedure versions of Insert, Overwrite, and
Delete:

In this example, we look for the index of the substring World and
perform operations on this substring within the outer string. The procedure
Display_Adapted_String uses both versions of the operations. For the
procedural version of Insert and Overwrite, we apply truncation to
the right side of the string (Right). For the Delete procedure, we
specify the range of the substring, which is replaced by whitespaces. For
the function version of Delete, we also call Trim which trims the
trailing whitespace.

Using fixed-length strings is usually good enough for strings that are
initialized when they are declared. However, as seen in the previous
section, procedural operations on strings cause difficulties when done on
fixed-length strings because fixed-length strings are arrays of
characters. The following example shows how cumbersome the initialization
of fixed-length strings can be when it's not performed in the declaration:

In this case, we can't simply write S:="Hello" because the
resulting array of characters for the Hello constant has a different
length than the S string. Therefore, we need to include trailing
whitespaces to match the length of S. As shown in the example, we could
use an exact range for the initialization ( S(1..5)) or use an
explicit array of individual characters.

When strings are initialized or manipulated at run-time, it's usually
better to use bounded or unbounded strings. An important feature of these
types is that they aren't arrays, so the difficulties presented above don't
apply. Let's start with bounded strings.

Bounded strings are defined in the
Ada.Strings.Bounded.Generic_Bounded_Length package. Because
this is a generic package, you need to instantiate it and set the
maximum length of the bounded string. You can then declare bounded
strings of the Bounded_String type.

Both bounded and fixed-length strings have a maximum length that they
can hold. However, bounded strings are not arrays, so initializing
them at run-time is much easier. For example:

By using bounded strings, we can easily assign to S1 and S2
multiple times during execution. We use the To_Bounded_String and
To_String functions to convert, in the respective direction, between
fixed-length and bounded strings. A call to To_Bounded_String raises an
exception if the length of the input string is greater than the maximum
capacity of the bounded string. To avoid this, we can use the truncation
parameter (Right in our example).

Bounded strings are not arrays, so we can't use the 'Length
attribute as we did for fixed-length strings. Instead, we call the
Length function, which returns the length of the bounded string. The
Max_Length constant represents the maximum length of the bounded string
that we set when we instantiated the package.

After initializing a bounded string, we can manipulate it. For example, we
can append a string to a bounded string using Append or concatenate
bounded strings using the & operator. Like so:

We can initialize a bounded string with an empty string using the
Null_Bounded_String constant. Also, we can use the Append procedure
and specify the truncation mode like we do with the To_Bounded_String
function.

Unbounded strings are defined in the Ada.Strings.Unbounded package.
This is not a generic package, so we don't need to instantiate it before
using the Unbounded_String type. As you may recall from the previous
section, bounded strings require a package instantiation.

Unbounded strings are similar to bounded strings. The main difference is
that they can hold strings of any size and adjust according to the input
string: if we assign, e.g., a 10-character string to an unbounded string
and later assign a 50-character string, internal operations in the
container ensure that memory is allocated to store the new string. In most
cases, developers don't need to worry about these operations. Also, no
truncation is necessary.

Initialization of unbounded strings is very similar to bounded strings.
Let's look at an example:

Like bounded strings, we can assign to S1 and S2 multiple times
during execution and use the To_Unbounded_String and To_String
functions to convert back-and-forth between fixed-length strings and
unbounded strings. However, in this case, truncation is not needed.

And, just like for bounded strings, you can use the Append function and
the & operator for unbounded strings. For example:

In most parts of this course, we used the Put_Line procedure to display
information on the console. However, this procedure also accepts a
File_Type parameter. For example, you can select between standard
output and standard error by setting this parameter explicitly:

with Ada.Text_IO; use Ada.Text_IO;
procedure Show_Std_Text_Out is
begin
Put_Line (Standard_Output, "Hello World #1");
Put_Line (Standard_Error, "Hello World #2");
end Show_Std_Text_Out;

You can also use this parameter to write information to any text file. To
create a new file for writing, use the Create procedure, which
initializes a File_Type element that you can later pass to Put_Line
(instead of, e.g., Standard_Output). After you finish writing
information, you can close the file by calling the Close procedure.

You use a similar method to read information from a text file. However,
when opening the file, you must specify that it's an input file
(In_File) instead of an output file. Also, instead of calling the
Put_Line procedure, you call the Get_Line function to read
information from the file.

Let's see an example that writes information into a new text file and then
reads it back from the same file:

withAda.Text_IO;useAda.Text_IO;procedureShow_Simple_Text_File_IOisF:File_Type;File_Name:constantString:="simple.txt";beginCreate(F,Out_File,File_Name);Put_Line(F,"Hello World #1");Put_Line(F,"Hello World #2");Put_Line(F,"Hello World #3");Close(F);Open(F,In_File,File_Name);whilenotEnd_Of_File(F)loopPut_Line(Get_Line(F));endloop;Close(F);endShow_Simple_Text_File_IO;

In addition to the Create and Close procedures, the standard
library also includes a Reset procedure, which, as the name implies,
resets (erases) all the information from the file. For example:

withAda.Text_IO;useAda.Text_IO;procedureShow_Text_File_ResetisF:File_Type;File_Name:constantString:="simple.txt";beginCreate(F,Out_File,File_Name);Put_Line(F,"Hello World #1");Reset(F);Put_Line(F,"Hello World #2");Close(F);Open(F,In_File,File_Name);whilenotEnd_Of_File(F)loopPut_Line(Get_Line(F));endloop;Close(F);endShow_Text_File_Reset;

By running this program, we notice that, although we've written the first
string (HelloWorld#1) to the file, it has been erased because of the
call to Reset.

In addition to opening a file for reading or writing, you can also open an
existing file and append to it. Do this by calling the Open procedure
with the Append_File option.

When calling the Open procedure, an exception is raised if the
specified file isn't found. Therefore, you should handle exceptions in
that context. The following example deletes a file and then tries to open
the same file for reading:

withAda.Text_IO;useAda.Text_IO;procedureShow_Text_File_Input_ExceptisF:File_Type;File_Name:constantString:="simple.txt";begin-- Open output file and delete itCreate(F,Out_File,File_Name);Delete(F);-- Try to open deleted fileOpen(F,In_File,File_Name);Close(F);exceptionwhenName_Error=>Put_Line("File does not exist");whenothers=>Put_Line("Error while processing input file");endShow_Text_File_Input_Except;

In this example, we create the file by calling Create and then
delete it by calling Delete. After the call to Delete, we can
no longer use the File_Type element`. After deleting the file, we
try to open the non-existent file, which raises a Name_Error
exception.

The previous section presented details about text file I/O. Here, we
discuss doing file I/O in binary format. The first package we'll explore is
the Ada.Sequential_IO package. Because this package is a generic
package, you need to instantiate it for the data type you want to use for
file I/O. Once you've done that, you can use the same procedures we've seen
in the previous section: Create, Open, Close, Reset and
Delete. However, instead of calling the Get_Line and Put_Line
procedures, you'd call the Read and Write procedures.

In the following example, we instantiate the Ada.Sequential_IO
package for floating-point types:

As the example shows, we can use the same approach we used for
floating-point types to perform file I/O for this record. Once we
instantiate the Ada.Sequential_IO package for the record type, file
I/O operations are performed the same way.

Direct I/O is available in the Ada.Direct_IO package. This mechanism
is similar to the sequential I/O approach just presented, but allows us to
access any position in the file. The package instantiation and most
operations are very similar to sequential I/O. To rewrite the
Show_Seq_Float_IO application presented in the previous section to use
the Ada.Direct_IO package, we just need to replace the instances of
the Ada.Sequential_IO package by the Ada.Direct_IO
package. This is the new source code:

Unlike sequential I/O, direct I/O allows you to access any position in
the file. However, it doesn't offer an option to append information to
a file. Instead, it provides an Inout_File mode allowing reading
and writing to a file via the same File_Type element.

To access any position in the file, call the Set_Index procedure to set
the new position / index. You can use the Index function to retrieve
the current index. Let's see an example:

withAda.Text_IO;withAda.Direct_IO;procedureShow_Dir_Float_In_Out_FileispackageFloat_IOis newAda.Direct_IO(Float);useFloat_IO;F:Float_IO.File_Type;File_Name:constantString:="float_file.bin";begin-- Open file for input / outputCreate(F,Inout_File,File_Name);Write(F,1.5);Write(F,2.4);Write(F,6.7);-- Set index to previous position and overwrite valueSet_Index(F,Index(F)-1);Write(F,7.7);declareValue:Float;begin-- Set index to start of fileSet_Index(F,1);whilenotEnd_Of_File(F)loopRead(F,Value);Ada.Text_IO.Put_Line(Float'Image(Value));endloop;Close(F);end;endShow_Dir_Float_In_Out_File;

By running this example, we see that the file contains 7.7, rather than
the previous 6.7 that we wrote. We overwrote the value by changing the
index to the previous position before doing another write.

In this example we used the Inout_File mode. Using that mode, we just
changed the index back to the initial position before reading from the file
(Set_Index(F,1)) instead of closing the file and reopening it for
reading.

All the previous approaches for file I/O in binary format (sequential and
direct I/O) are specific for a single data type (the one we instantiate
them with). You can use these approaches to write objects of a single data
type that may be an array or record (potentially with many fields), but if
you need to create and process files that include different data types, or
any objects of an unbounded type, these approaches are not
sufficient. Instead, you should use stream I/O.

Stream I/O shares some similarities with the previous approaches. We still
use the Create, Open and Close procedures. However, instead of
accessing the file directly via a File_Type element, you use a
Stream_Access element. To read and write information, you use the
'Read or 'Write attributes of the data types you're reading
or writing.

Let's look at a version of the Show_Dir_Float_IO procedure from the
previous section that makes use of stream I/O instead of direct I/O:

After the call to Create, we retrieve the corresponding
Stream_Access element by calling the Stream function. We then
use this stream to write information to the file via the 'Write
attribute of the Float type. After closing the file and
reopening it for reading, we again retrieve the corresponding
Stream_Access element and processed to read information from the
file via the 'Read attribute of the Float type.

You can use streams to create and process files containing different data
types within the same file. You can also read and write unbounded data
types such as strings. However, when using unbounded data types you must
call the 'Input and 'Output attributes of the unbounded data
type: these attributes write information about bounds or discriminants in
addition to the object's actual data.

The following example shows file I/O that mixes both strings of
different lengths and floating-point values:

When you use Stream I/O, no information is written into the file indicating
the type of the data that you wrote. If a file contains data from
different types, you must reference types in the same order when reading a
file as when you wrote it. If not, the information you get will be
corrupted. Unfortunately, strong data typing doesn't help you in this
case. Writing simple procedures for file I/O (as in the example above) may
help ensuring that the file format is consistent.

Like direct I/O, stream I/O supports also allows you to access any location
in the file. However, when doing so, you need to be extremely careful that
the position of the new index is consistent with the data types you're
expecting.

Here we use the standard e and Pi constants from the
Ada.Numerics package.

The Ada.Numerics.Elementary_Functions package provides operations
for the Float type. Similar packages are available for
Long_Float and Long_Long_Float types. For example, the
Ada.Numerics.Long_Elementary_Functions package offers the same set
of operations for the Long_Float type. In addition, the
Ada.Numerics.Generic_Elementary_Functions package is a generic
version of the package that you can instantiate for custom floating-point
types. In fact, the Elementary_Functions package can be defined as
follows:

The standard library also includes a random number generator for discrete
numbers, which is part of the Ada.Numerics.Discrete_Random package.
Since it's a generic package, you have to instantiate it for the desired
discrete type. This allows you to specify a range for the generator. In the
following example, we create an application that displays random integers
between 1 and 10:

Here, package R is instantiated with the Random_Range type,
which has a constrained range between 1 and 10. This allows us to
control the range used for the random numbers. We could easily modify
the application to display random integers between 0 and 20 by
changing the specification of the Random_Range type. We can also
use floating-point or fixed-point types.

The Ada.Numerics.Complex_Types package provides support for complex
number types and the Ada.Numerics.Complex_Elementary_Functions
package provides support for common operations on complex number types,
similar to the Ada.Numerics.Elementary_Functions package. Finally,
you can use the Ada.Text_IO.Complex_IO package to perform I/O
operations on complex numbers. In the following example, we declare
variables of the Complex type and initialize them using an aggregate:

As we can see from this example, all the common operators, such as *
and +, are available for complex types. You also have typical
operations on complex numbers, such as Argument and Exp. In
addition to initializing complex numbers in the cartesian form using
aggregates, you can do so from the polar form by calling the
Compose_From_Polar function.

The Ada.Numerics.Complex_Types and
Ada.Numerics.Complex_Elementary_Functions packages provide
operations for the Float type. Similar packages are available for
Long_Float and Long_Long_Float types. In addition, the
Ada.Numerics.Generic_Complex_Types and
Ada.Numerics.Generic_Complex_Elementary_Functions packages are
generic versions that you can instantiate for custom or pre-defined
floating-point types. For example:

The Ada.Numerics.Real_Arrays package provides support for
vectors and matrices. It includes common matrix operations such as
inverse, determinant, eigenvalues in addition to simpler operators
such as matrix addition and multiplication. You can declare vectors
and matrices using the Real_Vector and Real_Matrix types,
respectively.

The following example uses some of the operations from the
Ada.Numerics.Real_Arrays package:

Matrix dimensions are automatically determined from the aggregate used for
initialization when you don't specify them. You can, however, also use
explicit ranges. For example:

M1:Real_Matrix(1..2,1..3):=((1.0,5.0,1.0),(2.0,2.0,1.0));

The Ada.Numerics.Real_Arrays package implements operations for the
Float type. Similar packages are available for Long_Float and
Long_Long_Float types. In addition, the
Ada.Numerics.Generic_Real_Arrays package is a generic version that
you can instantiate with custom floating-point types. For example, the
Real_Arrays package can be defined as follows: