Ada Programming/Type System

Ada's type system allows the programmer to construct powerful abstractions that represent the real world, and to provide valuable information to the compiler, so that the compiler can find many logic or design errors before they become bugs. It is at the heart of the language, and good Ada programmers learn to use it to great advantage. Four principles govern the type system:


 * Type: a way to categorize data. characters are types 'a' through 'z'. Integers are types that include 0,1,2....
 * Strong typing: types are incompatible with one another, so it is not possible to mix apples and oranges. The compiler will not guess that your apple is an orange. You must explicitly say my_fruit = fruit(my_apple). Strong typing reduces the amount of errors. This is because developers can really easily write a float into an integer variable without knowing. Now data you needed for your program to succeed has been lost in the conversion when the complier switched types. Ada gets mad and rejects the developer's dumb mistake by refusing to do the conversion unless explicitly told.
 * Static typing: type checked while compiling, this allows type errors to be found earlier.
 * Abstraction: types represent the real world or the problem at hand; not how the computer represents the data internally. There are ways to specify exactly how a type must be represented at the bit level, but we will defer that discussion to another chapter. An example of an abstraction is your car. You don't really know how it works you just know that bumbling hunk of metal moves. Nearly every technology you work with is abstracted layer to simplify the complex circuits that make it up - the same goes for software. You want abstraction because code in a class makes a lot more sense than a hundred if statements with no explanation when debugging
 * Name equivalence: as opposed to structural equivalence used in most other languages. Two types are compatible if and only if they have the same name; not if they just happen to have the same size or bit representation.  You can thus declare two integer types with the same ranges that are totally incompatible, or two record types with exactly the same components, but which are incompatible.

Types are incompatible with one another. However, each type can have any number of subtypes, which are compatible with their base type and may be compatible with one another. See below for examples of subtypes which are incompatible with one another.

Predefined types
There are several predefined types, but most programmers prefer to define their own, application-specific types. Nevertheless, these predefined types are very useful as interfaces between libraries developed independently. The predefined library, obviously, uses these types too.

These types are predefined in the package:
 * Integer: This type covers at least the range $$-2^{15}+1$$ .. $$+2^{15}-1$$ (RM ). The Standard also defines  and   subtypes of this type.


 * Float: There is only a very weak implementation requirement on this type (RM ); most of the time you would define your own floating-point types, and specify your precision and range requirements.
 * Duration: A fixed point type used for timing. It represents a period of time in seconds (RM ).
 * Character : A special form of Enumerations. There are three predefined kinds of character types: 8-bit characters (called ), 16-bit characters (called  ), and 32-bit characters .   has been present since the first version of the language (Ada 83),   was added in Ada 95, while the type   is available with Ada 2005.
 * String: Three indefinite array types, of,  , and   respectively.  The standard library contains packages for handling strings in three variants: fixed length , with varying length below a certain upper bound , and unbounded length .  Each of these packages has a   and a   variant.
 * Boolean : A  in Ada is an Enumeration of   and   with special semantics.

Packages  and   predefine some types which are primarily useful for low-level programming and interfacing to hardware.


 * System.Address : An address in memory.
 * System.Storage_Elements.Storage_Offset : An offset, which can be added to an address to obtain a new address. You can also subtract one address from another to get the offset between them.  Together, ,   and their associated subprograms provide for address arithmetic.
 * System.Storage_Elements.Storage_Count : A subtype of  which cannot be negative, and represents the memory size of a data structure (similar to C's  ).
 * System.Storage_Elements.Storage_Element : In most computers, this is a byte. Formally, it is the smallest unit of memory that has an address.
 * System.Storage_Elements.Storage_Array : An array of s without any meaning, useful when doing raw memory access.

The Type Hierarchy
Types are organized hierarchically. A type inherits properties from types above it in the hierarchy. For example, all scalar types (integer, enumeration, modular, fixed-point and floating-point types) have operators "", "" and arithmetic operators defined for them, and all discrete types can serve as array indexes.



Here is a broad overview of each category of types; please follow the links for detailed explanations. Inside parenthesis there are equivalences in C and Pascal for readers familiar with those languages.


 * Signed Integers (int, INTEGER) : Signed Integers are defined via the range of values needed.
 * Unsigned Integers (unsigned, CARDINAL) : Unsigned Integers are called Modular Types. Apart from being unsigned they also have wrap-around functionality.
 * Enumerations (enum, char, bool, BOOLEAN) : Ada Enumeration types are a separate type family.
 * Floating point (float, double, REAL) : Floating point types are defined by the digits needed, the relative error bound.
 * Ordinary and Decimal Fixed Point (DECIMAL) : Fixed point types are defined by their delta, the absolute error bound.
 * Arrays ( [ ], ARRAY [ ] OF, STRING ) : Arrays with both compile-time and run-time determined size are supported.
 * Record (struct, class, RECORD OF) : A record is a composite type that groups one or more fields.
 * Access (*, ^, POINTER TO) : Ada's Access types may be more than just a simple memory address.
 * Task & Protected (similar to multithreading in C++) : Task and Protected types allow the control of concurrency
 * Interfaces (similar to virtual methods in C++) : New in Ada 2005, these types are similar to the Java interfaces.

Classification of Types
Ada's types can be classified as follows.

Specific vs. Class-wide

T ...     T'

and  are the same.

Primitive operations with parameters of specific types are non-dispatching, those with parameters of class-wide types are dispatching.

New types can be declared by deriving from specific types; primitive operations are inherited by derivation. You cannot derive from class-wide types.

Constrained vs. Unconstrained

I  1 .. 10;            AC   (1 .. 10)  ...

AU  (I  <>)  ...            R (X: Discriminant [:= Default])  ...

By giving a constraint to an unconstrained subtype, a subtype or object becomes constrained:

RC R (Value); OC: R (Value); OU: R;

Declaring an unconstrained object is only possible if a default value is given in the type declaration above. The language does not specify how such objects are allocated. GNAT allocates the maximum size, so that size changes that might occur with discriminant changes present no problem. Another possibility is implicit dynamic allocation on the heap and re-allocation followed by a deallocation when the size changes.

 Definite vs. Indefinite

I  1 .. 10;                      RD (X: Discriminant := Default)  ...

T (<>) ...                      AU   (I  <>)  ...    RI (X: Discriminant)  ...

Definite subtypes allow the declaration of objects without initial value, since objects of definite subtypes have constraints that are known at creation-time. Object declarations of indefinite subtypes need an initial value to supply a constraint; they are then constrained by the constraint delivered by the initial value.

OT: T := Expr; OA: AU := (3 => 10, 5 => 2, 4 => 4); OR: RI := Expr;

Unconstrained vs. Indefinite

Note that unconstrained subtypes are not necessarily indefinite as can be seen above with RD: it is a definite unconstrained subtype.

Concurrency Types
The Ada language uses types for one more purpose in addition to classifying data + operations. The type system integrates concurrency (threading, parallelism). Programmers will use types for expressing the concurrent threads of control of their programs.

The core pieces of this part of the type system, the task types and the protected types are explained in greater depth in a section on tasking.

Limited Types
Limiting a type means disallowing assignment. The &ldquo;concurrency types&rdquo; described above are always limited. Programmers can define their own types to be limited, too, like this:

T  &hellip;;

(The ellipsis stands for, or for a definition, see the corresponding subsection on this page.) A limited type also doesn't have an equality operator unless the programmer defines one.

You can learn more in the limited types chapter.

Defining new types and subtypes
You can define a new type with the following syntax:

T ...

followed by the description of the type, as explained in detail in each category of type.

Formally, the above declaration creates a type and its first subtype named. The type itself, correctly called the "type of T", is anonymous; the RM refers to it as  (in italics), but often speaks sloppily about the type T. But this is an academic consideration; for most purposes, it is sufficient to think of   as a type. For scalar types, there is also a base type called, which encompasses all values of T.

For signed integer types, the type of T comprises the (complete) set of mathematical integers. The base type is a certain hardware type, symmetric around zero (except for possibly one extra negative value), encompassing all values of T.

As explained above, all types are incompatible; thus:

Integer_1  1 .. 10; Integer_2   1 .. 10; A : Integer_1 := 8; B : Integer_2 := A;

is illegal, because  and   are different and incompatible types. It is this feature which allows the compiler to detect logic errors at compile time, such as adding a file descriptor to a number of bytes, or a length to a weight. The fact that the two types have the same range does not make them compatible: this is name equivalence in action, as opposed to structural equivalence. (Below, we will see how you can convert between incompatible types; there are strict rules for this.)

Creating subtypes
You can also create new subtypes of a given type, which will be compatible with each other, like this:

Integer_1  1 .. 10; Integer_2  Integer_1       7 .. 11;   Integer_3  Integer_1'Base  7 .. 11;  A : Integer_1 := 8; B : Integer_3 := A;

The declaration of  is bad because the constraint   is not compatible with  ; it raises   at subtype elaboration time.

and  are compatible because they are both subtypes of the same type, namely.

It is not necessary that the subtype ranges overlap, or be included in one another. The compiler inserts a run-time range check when you assign A to B; if the value of A, at that point, happens to be outside the range of, the program raises.

There are a few predefined subtypes which are very useful:

Natural  Integer  0 .. Integer'Last; Positive Integer  1 .. Integer'Last;

Derived types
A derived type is a new, full-blown type created from an existing one. Like any other type, it is incompatible with its parent; however, it inherits the primitive operations defined for the parent type.

Integer_1  1 .. 10; Integer_2   Integer_1  2 .. 8; A : Integer_1 := 8; B : Integer_2 := A;

Here both types are discrete; it is mandatory that the range of the derived type be included in the range of its parent. Contrast this with subtypes. The reason is that the derived type inherits the primitive operations defined for its parent, and these operations assume the range of the parent type. Here is an illustration of this feature:

Derived_Types Pak Integer_1  1 .. 10;       P (I:  Integer_1); Integer_2  Integer_1  8 .. 10;     Pak; Pak Pak; Pak; A: Integer_1 := 4; B: Integer_2 := 9; P (B); Derived_Types;

When we call, the parameter B is converted to  ; this conversion of course passes since the set of acceptable values for the derived type (here, 8 .. 10) must be included in that of the parent type (1 .. 10). Then P is called with the converted parameter.

Consider however a variant of the example above:

Derived_Types Pak Integer_1  1 .. 10;     P (I:  Integer_1; J:  Integer_1); Integer_2  Integer_1  8 .. 10;   Pak; Pak P (I: Integer_1; J:  Integer_1) J := I - 1; P;   Pak; Pak; A: Integer_1 := 4; X: Integer_1; B: Integer_2 := 8; Y: Integer_2; P (A, X); P (B, Y); Derived_Types;

When  is called, both parameters are converted to. Thus the range check on J (7) in the body of P will pass. However on return parameter Y is converted back to  and the range check on Y will of course fail.

With the above in mind, you will see why in the following program Constraint_Error will be called at run time, before  is even called.

Derived_Types Pak Integer_1  1 .. 10;     P (I:  Integer_1; J:  Integer_1); Integer_2  Integer_1'Base  8 .. 12;   Pak; Pak P (I: Integer_1; J:  Integer_1) J := I - 1; P;   Pak; Pak; B: Integer_2 := 11; Y: Integer_2; P (B, Y); Derived_Types;

Subtype categories
Ada supports various categories of subtypes which have different abilities. Here is an overview in alphabetical order.

Anonymous subtype
A subtype which does not have a name assigned to it. Such a subtype is created with a variable declaration:

X : String (1 .. 10) := ( => ' ');

Here, (1 .. 10) is the constraint. This variable declaration is equivalent to:

Anonymous_String_Type String (1 .. 10); X : Anonymous_String_Type := ( => ' ');

Base type
In Ada, all types are anonymous and only subtypes may be named. For scalar types, there is a special subtype of the anonymous type, called the base type, which is nameable with  notation. This  (read "name tick attribute") is the special notation used in Ada for what is called an attribute, i.e. a characteristic of a type, a variable, or some other program entity, that is defined by the compiler and can be queried. In this case, the base type comprises all values of the first subtype. Some examples:

Int  0 .. 100;

The base type  is a hardware type selected by the compiler that comprises the values of. Thus, it may have the range -27 .. 27-1 or -215 .. 215-1 or any other such type.

Enum  (A, B, C, D); Short  Enum  A .. C;

is the same as, but   also holds the literal.

Constrained subtype
A subtype of an indefinite subtype that adds constraints. The following example defines a 10 character string sub-type.

String_10 String (1 .. 10);

You cannot partially constrain an unconstrained subtype:

My_Array  (Integer  <>, Integer  <>)  Some_Type; Constr My_Array (1 .. 10, -100 .. 200);

Constraints for all indices must be given, the result is necessarily a definite subtype.

Definite subtype
A definite subtype is a subtype whose size is known at compile-time. All subtypes which are not indefinite subtypes are, by definition, definite subtypes.

Objects of definite subtypes may be declared without additional constraints.

Indefinite subtype
An indefinite subtype is a subtype whose size is not known at compile-time but is dynamically calculated at run-time. An indefinite subtype does not by itself provide enough information to create an object; an additional constraint or explicit initialization expression is necessary in order to calculate the actual size and therefore create the object.

X : String := "This is a string";

is an object of the indefinite (sub)type. Its constraint is derived implicitly from its initial value. may change its value, but not its bounds.

It should be noted that it is not necessary to initialize the object from a literal. You can also use a function. For example:

X : String := Ada.Command_Line.Argument (1);

This statement reads the first command-line argument and assigns it to.

A subtype of an indefinite subtype that does not add a constraint only introduces a new name for the original subtype (a kind of renaming under a different notion).

My_String String;

My_String and String are interchangeable.

Named subtype
A subtype which has a name assigned to it. “First subtypes” are created with the keyword  (remember that types are always anonymous, the name in a type declaration is the name of the first subtype), others with the keyword. For example:

Count_To_Ten  1 .. 10;

is the first subtype of a suitable integer base type. However, if you would like to use this as an index constraint on, the following declaration is illegal:

Ten_Characters String (Count_to_Ten);

This is because  has   as its index, which is a subtype of   (these declarations are taken from package  ):

Positive Integer  1 .. Integer'Last; String (Positive  <>)  Character;

So you have to use the following declarations:

Count_To_Ten Integer  1 .. 10; Ten_Characters  String (Count_to_Ten);

Now  is the name of that subtype of   which is constrained to. You see that posing constraints on types versus subtypes has very different effects.

Unconstrained subtype
Any indefinite type is also an unconstrained subtype. However, unconstrainedness and indefiniteness are not the same.

My_Enum (A, B, C); My_Record (Discriminant: My_Enum) ...; My_Object_A: My_Record (A);

This type is unconstrained and indefinite because you need to give an actual discriminant for object declarations; the object is constrained to this discriminant which may not change.

When however a default is provided for the discriminant, the type is definite yet unconstrained; it allows to define both, constrained and unconstrained objects:

My_Enum (A, B, C); My_Record (Discriminant: My_Enum := A) ...; My_Object_U: My_Record; My_Object_B: My_Record (B);

Here, My_Object_U is unconstrained; upon declaration, it has the discriminant A (the default) which however may change.

Incompatible subtypes
My_Integer  -10 .. + 10;  My_Positive  My_Integer  + 1 .. + 10;  My_Negative  My_Integer  -10 .. - 1;

These subtypes are of course incompatible.

Another example are subtypes of a discriminated record:

My_Enum (A, B, C); My_Record (Discriminant: My_Enum) ...; My_A_Record My_Record (A); My_C_Record My_Record (C);

Also these subtypes are incompatible.

Qualified expressions
In most cases, the compiler is able to infer the type of an expression; for example:

Enum (A, B, C); E : Enum := A;

Here the compiler knows that  is a value of the type. But consider:

Bad Enum_1 (A, B, C); P (E : Enum_1) ...      Enum_2  (A, X, Y, Z); P (E : Enum_2) ...     P (A); Bad;

The compiler cannot choose between the two versions of ; both would be equally valid. To remove the ambiguity, you use a qualified expression:

P (Enum_1'(A));

As seen in the following example, this syntax is often used when creating new objects. If you try to compile the example, it will fail with a compilation error since the compiler will determine that 256 is not in range of.

; Convert_Evaluate_As Byte      2**8; Byte_Ptr  Byte; T_IO ; M_IO   (Byte); A : Byte_Ptr :=  Byte'(256); T_IO.Put ("A = "); M_IO.Put (Item => A.all,              Width =>  5,              Base  => 10); Convert_Evaluate_As;

You should use qualified expression when getting a string literal's length.

Type conversions
Data do not always come in the format you need them. You must, then, face the task of converting them. As a true multi-purpose language with a special emphasis on "mission critical", "system programming" and "safety", Ada has several conversion techniques. The most difficult part is choosing the right one, so the following list is sorted in order of utility. You should try the first one first; the last technique is a last resort, to be used if all others fail. There are also a few related techniques that you might choose instead of actually converting the data.

Since the most important aspect is not the result of a successful conversion, but how the system will react to an invalid conversion, all examples also demonstrate faulty conversions.

Explicit type conversion
An explicit type conversion looks much like a function call; it does not use the tick (apostrophe, ') like the qualified expression does.

Type_Name (Expression)

The compiler first checks that the conversion is legal, and if it is, it inserts a run-time check at the point of the conversion; hence the name checked conversion. If the conversion fails, the program raises Constraint_Error. Most compilers are very smart and optimise away the constraint checks; so, you need not worry about any performance penalty. Some compilers can also warn that a constraint check will always fail (and optimise the check with an unconditional raise).

Explicit type conversions are legal:
 * between any two numeric types
 * between any two subtypes of the same type
 * between any two types derived from the same type (note special rules for tagged types)
 * between array types under certain conditions (see RM 4.6(24.2/2..24.7/2))
 * and nowhere else

(The rules become more complex with class-wide and anonymous access types.)

I: Integer := Integer (10); J: Integer := 10; K: Integer := Integer'(10);

This example illustrates explicit type conversions:

; Convert_Checked Short  -128 .. +127;    Byte    256; T_IO ; I_IO   (Short); M_IO   (Byte); A : Short := -1; B : Byte; B := Byte (A); T_IO.Put ("A = "); I_IO.Put (Item =>  A,              Width =>  5,              Base  => 10); T_IO.Put (", B = "); M_IO.Put (Item =>  B,              Width =>  5,              Base  => 10); Convert_Checked;

Explicit conversions are possible between any two numeric types: integers, fixed-point and floating-point types. If one of the types involved is a fixed-point or floating-point type, the compiler not only checks for the range constraints (thus the code above will raise Constraint_Error), but also performs any loss of precision necessary.

Example 1: the loss of precision causes the procedure to only ever print "0" or "1", since  is an integer and is always zero or one.

Ada.Text_IO; Naive_Explicit_Conversion Proportion  4  0.0 .. 1.0;    Percentage   0 .. 100;    To_Proportion (P :  Percentage)  Proportion Proportion (P / 100); To_Proportion; Ada.Text_IO.Put_Line (Proportion'Image (To_Proportion (27))); Naive_Explicit_Conversion;

Example 2: we use an intermediate floating-point type to guarantee the precision.

Ada.Text_IO; Explicit_Conversion Proportion  4  0.0 .. 1.0;    Percentage   0 .. 100;    To_Proportion (P :  Percentage)  Proportion Prop  4  0.0 .. 100.0;       Proportion (Prop (P) / 100.0); To_Proportion; Ada.Text_IO.Put_Line (Proportion'Image (To_Proportion (27))); Explicit_Conversion;

You might ask why you should convert between two subtypes of the same type. An example will illustrate this.

String_10 String (1 .. 10); X: String := "A line long enough to make the example valid"; Slice: String := String_10 (X (11 .. 20));

Here,  has bounds 1 and 10, whereas   has bounds 11 and 20.

Change of Representation
Type conversions can be used for packing and unpacking of records or arrays.

Unpacked ; Packed   Unpacked; Packed ;

P: Packed; U: Unpacked; P := Packed (U); U := Unpacked (P);

Checked conversion for non-numeric types
The examples above all revolved around conversions between numeric types; it is possible to convert between any two numeric types in this way. But what happens between non-numeric types, e.g. between array types or record types? The answer is two-fold:


 * you can convert explicitly between a type and types derived from it, or between types derived from the same type,
 * and that's all. No other conversions are possible.

Why would you want to derive a record type from another record type? Because of representation clauses. Here we enter the realm of low-level systems programming, which is not for the faint of heart, nor is it useful for desktop applications. So hold on tight, and let's dive in.

Suppose you have a record type which uses the default, efficient representation. Now you want to write this record to a device, which uses a special record format. This special representation is more compact (uses fewer bits), but is grossly inefficient. You want to have a layered programming interface: the upper layer, intended for applications, uses the efficient representation. The lower layer is a device driver that accesses the hardware directly and uses the inefficient representation.

Device_Driver Size_Type  0 .. 64;    Register A, B : Boolean; Size : Size_Type; ;    Read (R :  Register); Write (R : Register); Device_Driver;

The compiler chooses a default, efficient representation for. For example, on a 32-bit machine, it would probably use three 32-bit words, one for A, one for B and one for Size. This efficient representation is good for applications, but at one point we want to convert the entire record to just 8 bits, because that's what our hardware requires.

Device_Driver Hardware_Register  Register; Hardware_Register A 0  0 .. 0;      B  0  1 .. 1;      Size  0  2 .. 7;    ;     Get  Hardware_Register; Put (H : Hardware_Register); Read (R : Register) H : Hardware_Register := Get; R := Register (H); Read; Write (R : Register) Put (Hardware_Register (R)); Write; Device_Driver;

In the above example, the package body declares a derived type with the inefficient, but compact representation, and converts to and from it.

This illustrates that type conversions can result in a change of representation.

View conversion, in object-oriented programming
Within object-oriented programming you have to distinguish between specific types and class-wide types.

With specific types, only conversions in the direction to the root are possible, which of course cannot fail. There are no conversions in the opposite direction (where would you get the further components from?) ; extension aggregates have to be used instead.

With the conversion itself, no components of the source object that are not present in the target object are lost, they are just hidden from visibility. Therefore, this kind of conversion is called a view conversion since it provides a view of the source object as an object of the target type (especially it does not change the object's tag).

It is a common idiom in object oriented programming to rename the result of a view conversion. (A renaming declaration does not create a new object; it only gives a new name to something that already exists.)

Parent_Type ; ;  Child_Type   Parent_Type ; ; Child_Instance : Child_Type; Parent_View   : Parent_Type  Parent_Type (Child_Instance); Parent_Part   : Parent_Type := Parent_Type (Child_Instance);

is not a new object, but another name for  viewed as the parent, i.e. only the parent components are visible, the child-specific components are hidden. , however, is an object of the parent type, which of course has no storage for the child-specific components, so they are lost with the assignment.

All types derived from a tagged type  form a tree rooted at. The class-wide type  can hold any object within this tree. With class-wide types, conversions in any direction are possible; there is a run-time tag check that raises  if the check fails. These conversions are also view conversions, no data is created or lost.

Object_1 : Parent_Type' := Parent_Type' (Child_Instance); Object_2 : Parent_Type' Parent_Type' (Child_Instance);

is a new object, a copy;  is just a new name. Both objects are of the class-wide type. Conversions to any type within the given class are legal, but are tag-checked.

Success : Child_Type := Child_Type (Parent_Type' (Parent_View)); Failure : Child_Type := Child_Type (Parent_Type' (Parent_Part));

The first conversion passes the tag check and both objects  and   are equal. The second conversion fails the tag check. (Conversion assignments of this kind will rarely be used; dispatching will do this automatically, see object oriented programming.)

You can perform these checks yourself with membership tests:

Parent_View Child_Type  ...  Parent_View  Child_Type'  ...

There is also the package.

Address conversion
Ada's access type is not just a memory location (a thin pointer). Depending on implementation and the access type used, the access might keep additional information (a fat pointer). For example GNAT keeps two memory addresses for each access to an indefinite object &mdash; one for the data and one for the constraint informations (', ', ').

If you want to convert an access to a simple memory location you can use the package. Note however that an address and a fat pointer cannot be converted reversibly into one another.

The address of an array object is the address of its first component. Thus, the bounds get lost in such a conversion.

My_Array  (Positive  <>)  Something; A: My_Array (50 .. 100); A' = A(A')'

Unchecked conversion
One of the great criticisms of Pascal was "there is no escape". The reason was that sometimes you have to convert the incompatible. For this purpose, Ada has the generic function Unchecked_Conversion:

Source (<>)  ; Target (<>)  ; (S : Source) Target;

will bit-copy the source data and reinterpret them under the target type without any checks. It is your chore to make sure that the requirements on unchecked conversion as stated in RM are fulfilled; if not, the result is implementation dependent and may even lead to abnormal data. Use the 'Valid attribute after the conversion to check the validity of the data in problematic cases.

A function call to (an instance of)  will copy the source to the destination. The compiler may also do a conversion in place (every instance has the convention Intrinsic).

To use  you need to instantiate the generic.

In the example below, you can see how this is done. When run, the example will output. No error will be reported, but is this the result you expect?

; ;  Convert_Unchecked Short  -128 .. +127;    Byte    256; T_IO ; I_IO   (Short); M_IO   (Byte); Convert   (Source => Short,                                                      Target => Byte); A : Short := -1; B : Byte; B := Convert (A); T_IO.Put ("A = "); I_IO.Put (Item =>  A,              Width =>  5,              Base  => 10); T_IO.Put (", B = "); M_IO.Put (Item =>  B,              Width =>  5,              Base  => 10); Convert_Unchecked;

There is of course a range check in the assignment. Thus if  were defined as ,   would be raised.

Overlays
If the copying of the result of  is too much waste in terms of performance, then you can try overlays, i.e. address mappings. By using overlays, both objects share the same memory location. If you assign a value to one, the other changes as well. The syntax is:

Target' expression; (Ada, Target);

where expression defines the address of the source object.

While overlays might look more elegant than, you should be aware that they are even more dangerous and have even greater potential for doing something very wrong. For example if  and you assign a value to Target, you might inadvertently write into memory allocated to a different object.

You have to take care also of implicit initializations of objects of the target type, since they would overwrite the actual value of the source object. The Import pragma with convention Ada can be used to prevent this, since it avoids the implicit initialization, RM.

The example below does the same as the example from "Unchecked Conversion".

; Convert_Address_Mapping Short  -128 .. +127;    Byte    256; T_IO ; I_IO   (Short); M_IO   (Byte); A : Short; B : Byte; B' A'; (Ada, B); A := -1; T_IO.Put ("A = "); I_IO.Put (Item =>  A,              Width =>  5,              Base  => 10); T_IO.Put (", B = "); M_IO.Put (Item =>  B,              Width =>  5,              Base  => 10); Convert_Address_Mapping;

Export / Import
Just for the record: There is still another method using the and  pragmas. However, since this method completely undermines Ada's visibility and type concepts even more than overlays, it has no place here in this language introduction and is left to experts.

Elaborated Discussion of Types for Signed Integer Types
As explained before, a type declaration

T  1 .. 10;

declares an anonymous type  and its first subtype   (please note the italicization). encompasses the complete set of mathematical integers. Static expressions and named numbers make use of this fact.

All numeric integer literals are of type. They are converted to the appropriate specific type where needed. itself has no operators.

Some examples with static named numbers:

S1: := Integer' + Integer'; S2: := Long_Integer' + 1; S3: := S1 + S2; S4: := Integer' + Long_Integer';

Static expressions are evaluated at compile-time on the appropriate types with no overflow checks, i.e. mathematically exact (only limited by computer store). The result is then implicitly converted to.

The literal 1 in  is of type   and implicitly converted to.

implicitly converts the summands to, performs the calculation and converts back to.

is illegal because it mixes two different types. You can however write this as

S5: := Integer'Pos (Integer') + Long_Integer'Pos (Long_Integer');

where the Pos attributes convert the values to, which are then further implicitly converted to  , added and the result converted back to.

is the anonymous greatest integer type representable by the hardware. It has the range. All integer types are rooted at, i.e. derived from it. can be viewed as.

During run-time, computations of course are performed with range checks and overflow checks on the appropriate subtype. Intermediate results may however exceed the range limits. Thus with  of the subtype   above, the following code will return the correct result:

I := 10; J := 8; K := (I + J) - 12;

Real literals are of type, and similar rules as the ones above apply accordingly.

Relations between types
Types can be made from other types. Array types, for example, are made from two types, one for the arrays' index and one for the arrays' components. An array, then, expresses an association, namely that between one value of the index type and a value of the component type.

Color (Red, Green, Blue); Intensity  0 .. 255;  Colored_Point   (Color)  Intensity;

The type Color is the index type and the type Intensity is the component type of the array type Colored_Point. See array.

Wikibook

 * Ada Programming

Ada Reference Manual


|Type System Programación en Ada/Tipos