Ada is a strongly typed language. It is interestingly modern in that
respect: strong static typing has been increasing in popularity in programming
language design, owing to factors such as the growth of statically typed
functional programming, a big push from the research community in the typing
domain, and many practical languages with strong type systems.

In statically typed languages, a type is mainly (but not only) a compile time
construct. It is a construct to enforce invariants about the behavior of a
program. Invariants are unchangeable properties that hold for all variables of
a given type. Enforcing them ensures, for example, that variables of a data
type never have invalid values.

A type is used to reason about the objects a program manipulates (an object
is a variable or a constant). The aim is to classify objects by what you can
accomplish with them (i.e., the operations that are permitted), and this way
you can reason about the correctness of the objects' values.

A nice feature of Ada is that you can define your own integer types, based on
the requirements of your program (i.e., the range of values that makes sense).
In fact, the definitional mechanism that Ada provides forms the semantic basis
for the predefined integer types. There is no "magical" built-in type in that
regard, which is unlike most languages, and arguably very elegant.

with Ada.Text_IO; use Ada.Text_IO;
procedure Integer_Type_Example is
-- Declare a signed integer type, and give the bounds
type My_Int is range -1 .. 20;
-- ^ High bound
-- ^ Low bound
-- Like variables, type declarations can only appear in
-- declarative regions
begin
for I in My_Int loop
Put_Line (My_Int'Image (I));
-- ^ 'Image attribute, converts a value to a
-- String
end loop;
end Integer_Type_Example;

This example illustrates the declaration of a signed integer type, and
several things we can do with them.

Every type declaration in Ada starts with the type keyword (except for task types). After the type, we can see a range that looks a lot like
the ranges that we use in for loops, that defines the low and high bound of the
type. Every integer in the inclusive range of the bounds is a valid value for
the type.

Ada integer types

In Ada, an integer type is not specified in terms of its
machine representation, but rather by its range. The
compiler will then choose the most appropriate representation.

Another point to note in the above example is the My_Int'Image(I)
expression. The Name'Attribute(optionalparams) notation is used for
what is called an attribute in Ada. An attribute is a
built-in operation on a type, a value, or some other program entity. It is
accessed by using a ' symbol (the ASCII apostrophe).

Ada has several types available as "built-ins"; Integer is one of
them. Here is how Integer might be defined for a typical processor:

typeIntegerisrange-(2**31)..+(2**31-1);

** is the exponent operator, which means that the first valid
value for Integer is \(-2^{31}\), and the last valid value is
\(2^{31}-1\).

Ada does not mandate the range of the built-in type Integer. An implementation
for a 16-bit target would likely choose the range \(-2^{15}\) through
\(2^{15}-1\).

Type level overflow will only be checked at specific points in the execution.
The result, as we see above, is that you might have an operation that overflows
in an intermediate computation, but no exception will be raised because the
final result does not overflow.

Ada also features unsigned Integer types. They're called modular types in Ada
parlance. The reason for this designation is due to their behavior in case of
overflow: They simply "wrap around", as if a modulo operation was applied.

For machine sized modular types, for example a modulus of 2**32, this mimics
the most common implementation behavior of unsigned types. However, an
advantage of Ada is that the modulus is more general:

Unlike in C/C++, since this behavior is guaranteed by the Ada specification,
you can rely on it to implement portable code. Also, being able to leverage the
wrapping on arbitrary bounds is very useful -- the modulus does not need to be
a power of 2 -- to implement certain algorithms and data structures, such as
ring buffers.

Enumeration types are another nicety of Ada's type system. Unlike C's enums,
they are not integers, and each new enumeration type is incompatible with
other enumeration types. Enumeration types are part of the bigger family of
discrete types, which makes them usable in certain situations that we will
describe later but one context that we have already seen is a case statement.

with Ada.Text_IO; use Ada.Text_IO;
procedure Enumeration_Example is
type Days is (Monday, Tuesday, Wednesday,
Thursday, Friday, Saturday, Sunday);
-- An enumeration type
begin
for I in Days loop
case I is
when Saturday .. Sunday =>
Put_Line ("Week end!");
when Monday .. Friday =>
Put_Line ("Hello on " & Days'Image (I));
-- 'Image attribute, works on enums too
end case;
end loop;
end Enumeration_Example;

Enumeration types are powerful enough that, unlike in most languages, they're
used to define the standard Boolean type:

typeBooleanis(False,True);

As mentioned previously, every "built-in" type in Ada is defined with facilities
generally available to the user.

The value of A is 2.0 after the first operation and 5.0
after the second operation.

In addition to Float, an Ada implementation may offer data types with
higher precision such as Long_Float and Long_Long_Float. Like
Float, the standard does not indicate the exact precision of these types: it
only guarantees that the type Long_Float, for example, has at least the
precision of Float. In order to guarantee that a certain precision
requirement is met, we can define custom floating-point types, as we will see
in the next section.

Ada allows the user to specify the precision for a floating-point type,
expressed in terms of decimal digits. Operations on these custom types will
then have at least the specified precision. The syntax for a simple
floating-point type declaration is:

typeTisdigits<number_of_decimal_digits>;

The compiler will choose a floating-point representation that supports the
required precision. For example:

In this example, the attribute 'Size is used to retrieve the number of
bits used for the specified data type. As we can see by running this example,
the compiler allocates 32 bits for T3, 64 bits for T15 and 128
bits for T18. This includes both the mantissa and the exponent.

The number of digits specified in the data type is also used in the format
when displaying floating-point variables. For example:

In addition to the precision, a range can also be specified for a
floating-point type. The syntax is similar to the one used for integer data
types --- using the range keyword. This simple example creates a new
floating-point type based on the type Float, for a normalized range
between -1.0 and 1.0:

with Ada.Text_IO; use Ada.Text_IO;
procedure Floating_Point_Range is
type T_Norm is new Float range -1.0 .. 1.0;
A : T_Norm;
begin
A := 1.0;
Put_Line ("The value of A is " & T_Norm'Image (A));
end Floating_Point_Range;

The application is responsible for ensuring that variables of this type stay
within this range; otherwise an exception is raised. In this example, the
exception Constraint_Error is raised when assigning 2.0 to the
variable A:

with Ada.Text_IO; use Ada.Text_IO;
procedure Floating_Point_Range_Exception is
type T_Norm is new Float range -1.0 .. 1.0;
A : T_Norm;
begin
A := 2.0;
Put_Line ("The value of A is " & T_Norm'Image (A));
end Floating_Point_Range_Exception;

Ranges can also be specified for custom floating-point types. For example:

As noted earlier, Ada is strongly typed. As a result, different types of the
same family are incompatible with each other; a value of one type cannot be
assigned to a variable from the other type. For example:

A consequence of these rules is that, in the general case, a "mixed mode"
expression like 2*3.0 will trigger a compilation error. In a language
like C or Python, such expressions are made valid by implicit conversions. In
Ada, such conversions must be made explicit:

Of course, we probably do not want to write the conversion code every time we
convert from meters to miles. The idiomatic Ada way in that case would be to
introduce conversion functions along with the types.

If you write a lot of numeric code, having to explicitly provide such
conversions might seem painful at first. However, this approach brings some
advantages. Notably, you can rely on the absence of implicit conversions, which
will in turn prevent some subtle errors.

In other languages

In C, for example, the rules for implicit conversions may not
always be completely obvious. In Ada, however, the code will always do
exactly what it seems to do. For example:

inta=3,b=2;floatf=a/b;

This code will compile fine, but the result of f will be 1.0 instead
of 1.5, because the compiler will generate an integer division (three
divided by two) that results in one. The software developer must be
aware of data conversion issues and use an appropriate casting:

inta=3,b=2;floatf=(float)a/b;

In the corrected example, the compiler will convert both variables to
their corresponding floating-point representation before performing the
division. This will produce the expected result.

This example is very simple, and experienced C developers will probably
notice and correct it before it creates bigger
problems. However, in more complex applications where the type
declaration is not always visible --- e.g. when referring to elements of
a struct --- this situation might not always be evident and quickly
lead to software defects that can be harder to find.

The Ada compiler, in contrast, will always reject code that
mixes floating-point and integer variables without explicit conversion.
The following Ada code, based on the erroneous example in C, will not
compile:

The offending line must be changed to F:=Float(A)/Float(B);
in order to be accepted by the compiler.

You can use Ada's strong typing to help
enforce invariants in your code, as in the example
above: Since Miles and Meters are two different types, you cannot mistakenly
convert an instance of one to an instance of the other.

In Ada you can create new types based on existing ones. This is very useful:
you get a type that has the same properties as some existing type but is
treated as a distinct type in the interest of strong typing.

The type Social_Security is said to be a derived type;
its parent type is Integer.

As illustrated in this example, you can refine the valid range when defining a
derived scalar type (such as integer, floating-point and enumeration).

The syntax for enumerations uses the range<range> syntax:

with Ada.Text_IO; use Ada.Text_IO;
procedure Greet is
type Days is (Monday, Tuesday, Wednesday, Thursday,
Friday, Saturday, Sunday);
type Weekend_Days is new Days range Saturday .. Sunday;
-- New type, where only Saturday and Sunday are valid literals.
begin
null;
end Greet;

As we are starting to see, types may be used in Ada to enforce constraints on
the valid range of values. However, we sometimes want to enforce constraints on
some values while staying within a single type. This is where subtypes come
into play. A subtype does not introduce a new type.

with Ada.Text_IO; use Ada.Text_IO;
procedure Greet is
type Days is (Monday, Tuesday, Wednesday, Thursday,
Friday, Saturday, Sunday);
-- Declaration of a subtype
subtype Weekend_Days is Days range Saturday .. Sunday;
-- ^ Constraint of the subtype
M : Days := Sunday;
S : Weekend_Days := M;
-- No error here, Days and Weekend_Days are of the same type.
begin
for I in Days loop
case I is
-- Just like a type, a subtype can be used as a
-- range
when Weekend_Days =>
Put_Line ("Week end!");
when others =>
Put_Line ("Hello on " & Days'Image (I));
end case;
end loop;
end Greet;

Several subtypes are predefined in the standard package in Ada, and are
automatically available to you: