Ada Programming/Constants


Variables and constants both store data. The difference between them is that the data stored in a variable can be changed during program execution, whereas the data stored in a constant cannot. Both must be declared as a specific type, but only the constant requires a value (the data) to be assigned (initialized) when it is declared.

Ada. Time-tested, safe and secure.
Ada. Time-tested, safe and secure.

Objects

edit

In Ada, variables and constants are actually called objects (Ada RM 3.3 [Annotated]). This has nothing to do with object oriented programming. A variable in Ada is an object that has been declared as a given type and with an optional initial value. A constant is the same, except the declaration now contains the reserved word constant and the initial value is no longer optional, but required.

Both variables and constants are declared in the declarative part of a block.

Variables and constants are declared like so:

X, Y: T := Some_Value;  -- Initialization optional
C: constant T := Some_Value;

The initialization value for X and Y is evaluated separately for each one – thus, if this is a function call, they may have different values.

It is good practice to declare local objects which do not change as constant.

Some general advice on how to name variables and constants is given here.

Constants

edit

Constants are an important tool to help make your programs more reliable and maintainable. A constant is, just like a variable, a reference to a specific type of data, but where they differ from variables is in their constant nature. Variables vary, constants are constant. When declared and initialized, the data they reference is static and can no longer be altered. If you try to alter a constant, the compiler will complain, loudly.

Let's see how we declare and initialize a constant:

Name : constant String (1 .. 12) := "Thomas Løcke";

Or

Name : constant String := "Thomas Løcke";

The only differences from a variable are the reserved constant keyword and the fact that a constant must be initialized when declared. Other than that, declaring and initializing constants is done exactly the same way as variables and it works for all types:

A_Array : constant array (1 .. 3) of Integer := (1, 2, 3);
An_Positive : constant Positive := 10;

type Colors is (Red, Blue, Green);
Color : constant Colors := Red;
type Person is record
   Name : String (1 .. 12);
   Age : Natural;
end record;
B_Person : constant Person := (Name => "Thomas Løcke", Age => 37);

If you try to alter a constant in your program, the compiler will complain with a message looking something like this:

 constants.adb:15:04: left hand side of assignment must be a variable
 gnatmake: "/path/to/constants.adb" compilation error

You should use constants whenever data is supposed to be static. Do not underestimate the human capacity for making mistakes by declaring for example Pi like this:

Pi : Float := 3.14159_26535_89793_23846_26433_83279_50288_41971_69399_37511;

Is Pi supposed to change during program execution? Probably not. So why allow for that? Why risk it happening by mistake? Instead do this:

Pi : constant := 3.14159_26535_89793_23846_26433_83279_50288_41971_69399_37511;

There. Now you can feel safe in knowing that Pi will be Pi, no matter what.

By the way, for Pi specifically (and most notable constants), you needn't declare it yourself: It's already done for you in the Ada.Numerics package.

Named numbers

edit

The attentive reader will have noticed that the Pi constant used above is declared without a type. Such constants are called named numbers (3.3 [Annotated]) and their type is called an universal type (3.4.1 [Annotated]). There are four kinds of universal types:

  • universal_integer
  • universal_real
  • universal_fixed
  • universal_access

You cannot explicitly declare an object to be of type universal_integer. But what you can do is declare an object to be a constant named number. These may take values of any size or precision, as opposed to the types derived from these, for example Integer and Float. The universal types are not bound by the same constraints as the types derived from them:

Large_Int : Integer := 4_294_967_296;        --  Value not in range on a 32 bit machine
Named_Large_Int : constant := 4_294_967_296; --  OK on a 32 bit machine

The Named_Large_Int constant is of type universal_integer, because the literal 4_294_967_296 is an integer. Had we instead written:

Named_Real : constant := 4.294_967_296;

then Named_Real is of the type universal_real because the literal 4.294_967_296 contains a point (2.4 [Annotated]).

Advanced Topic: Are Constants Constant?

edit

Silly question: What actually does "constant" mean?

procedure Swap (X, Y: in out T) is
  Temp: constant T := X;
begin
  X := Y;
  Y := Temp;
end Swap;

In the Swap example, the temporary copy of X certainly is constant – we do not touch it. However (a triviality you’ll say), it is not constant during construction nor during destruction at the end of Swap.

Hold it!

In the following, you will learn some astonishing facts about “constants” (objects with the keyword constant).

First Doubts

edit

Let us sow first doubts about the constancy.

Controlled types (RM 7.6) grant the programmer access to the process of construction, copy and destruction via the three operations Initialize, Adjust and Finalize.

Given a controlled type T.

X: constant T := F;

After construction of X and its initialization with a copy of F, the operation Adjust is called with parameter mode in out for the constant object X. (Initialize is not called here because an initial value is given.)

In Ada.*_IO (RM A.6-A.10), the file parameter of Create and Close has mode in out, of Read and Write (resp. Put and Get) it has in. But each of these operations does change the state of the file parameter.

This reflects a design where the (internal) File object's "state" represents the open-ness of the File object, rather than the content of the (external) file. Agreed that this choice is debatable, but it is consistent across the I/O packages.

The presumed implementation model implies a level of indirection, where the File parameter represents a reference, which is essentially "null" when the file is closed, and non-null when the file is open. Everything else is buried in some mysterious netherland whose state is not reflected in the mode of the File parameter.

This is a symptom of any language that has pointer parameters whose mode has no effect on the ability to update the pointed-to object.

So:

An object is not constant, at least during construction and destruction.

But as you have seen with Ada.*_IO, a constant object can indeed change its state also during its lifetime. (An in parameter is like a constant within a subprogram.)

“The constant keyword means that the value never changes” – this is a False statement for Ada (the details are painful); it's true for elementary types and some others, but not for most composite types.

You can find some interesting statements about this in Ada RM: RM 3.3(13/3, 25.1/3), 13.9.1(13/3). The corresponding terms are immutably limited and inherently mutable.

Example Pointers

edit
Pointer:
declare
  type Rec is record
    I: access Integer;
  end record;
  Ob: constant Rec := (I => new Integer'(42));
  -- Ob.I is constant, but not Ob.I.all!
begin
  Ob.I.all := 53;
end Pointer;

Here you will certainly agree with me that this is clear as daylight.

But what about the case of an internal pointer accessing the whole object itself?

Reflexive:
declare
  type Rec is limited record  -- immutably limited
    -- Rosen technique (only possible on immutably limited types);
    -- Ref is a variable view (Rec is aliased since it is limited).
    Ref: access Rec := Rec'Unchecked_Access;
    I  : Integer;
  end record;
  Ob: constant Rec := (I => 42, Ref => <>);
begin
  Ob.I := 53;  -- illegal
  Ob.Ref.I := 53;  -- erroneous until Ada 2005, legal for Ada 2012
end Reflexive;

The object must be limited since copying would ruin the reference.

Ada 2012 AARM 13.9.1(14.e/3): The Rosen trick (named after Jean Pierre Rosen) is no longer erroneous when the actual object is constant, but good practice.

This applies to objects where there is necessarily a variable view at some point in the lifetime of the object which you can "squirrel away" for later use. This certainly does not happen "by mistake", the programmer does it on purpose.

package Controlled is
  type Rec is new Ada.Finalization.Controlled with record
    Ref: access Rec;  -- variable view
    I  : Integer;
  end record;
  overriding procedure Initialize (T: in out Rec);
  overriding procedure Adjust     (T: in out Rec);
  -- "in out" means they see a variable view of the object
  -- (even if it's a constant)
end Controlled;

You can use it like so:

declare
  function Create return Controlled.Rec isend Create;
  Ob: constant Controlled.Rec := Create;
  -- here, Adjust works on a constant on an assignment operation
begin
  Ob.Ref.I := 53;  -- Ob.I := 53;  illegal
end;

A copy of the return object Create is assigned to Ob, the object Create is finalized. Now the component Ref points to a no longer existing object. (Note the difference between assignment statement and assignment operation.) Adjust has to correct the wrong target.

Create might look like this:

function Create return Controlled.Rec is
  X: Controlled.Rec;  -- Initialize called
begin
  return X;
end Create;

The variable X is declared without an initial value, so Initialize is called. Create returns this self referencing object.

This is the package body:

package body Controlled is
  procedure Initialize (T: in out Rec) is
  begin
    T := (Ada.Finalization.Controlled with
          Ref => T'Unchecked_Access,
          I   => 42);
  end Initialize;
  procedure Adjust (T: in out Rec) is
  begin
    T.Ref := T'Unchecked_Access;
  end Adjust;
end Controlled;

Initiallize is only called if no initial value is given in the object declaration. T is regarded as aliased so that the component Ref is a variable view of the object itself.

Example Task and Protected Object

edit

Tasks are active objects, i.e. each task has its own thread of control: All tasks run concurrently. A task communicates with other tasks via rendezvous by calling one of its entries (roughly corresponding to a subprogram call).

Since Ada is a block-oriented language, tasks may be defined as components of arrays and records, and these may be constants.

task type TT is
  entry Set (I: Integer);
end TT;
task body TT is      -- local objects are not
  I: Integer := 42;  -- considered part of constant
begin
  accept Set (I: Integer) do
    TT.I := I;
  end Set;
end TT;
type Rec is record
  T: TT;  -- active object that changes state (even if "constant")
end record;
Ob: constant Rec := (T => <>);
Ob.T.Set (53);  -- execution of an entry of a constant task

Protected objects are very similar to tasks.

protected type Prot is
  procedure Set (I: Integer);
private
  I: Integer := 42;  -- *
end Prot;
protected body Prot is
  procedure Set (I: Integer) is
  begin
    Prot.I := I;  -- Prot.I is *
  end Set;
end Prot;
type Rec is limited record
  Ref: access Rec := Rec'Unchecked_Access;  -- reflexive
  P  : Prot;
end record;
Ob: constant Rec := (others => <>);
Ob.P.Set (53);      -- illegal
Ob.Ref.P.Set (53);  -- variable view required

Constants Are Like This

edit

If an object is declared “constant”, the meaning is just twofold:

  • The object cannot be used in an assignment (if it is not yet limited).
  • The object can only be used as an in parameter.

In no case at all does it mean that the logical state of the object is immutable. In fact, such a type should be private, which means the client has no influence at all on what happens in the object’s inside. The provider is responsible for the correct behavior. Immutably limited and controlled objects are inherently mutable.

See also

edit

Wikibook

edit

Ada Reference Manual

edit