The type of a Loop Parameter

Viewed 212

A beginner's question, I'm afraid. I need to record the position (index) of a particular element in an array. Consider the following:

with Ada.Text_IO; use Ada.Text_IO;

procedure Main is

    Ten : constant Positive := 10;

    type ArrayIndex is new Positive range 1 .. Ten;

    type MyRecord is record
        firstItem  : Integer;
        secondItem : Integer;
    end record;

    TheRecords : array (1 .. Ten) of MyRecord;

    indexOfElement50  :  ArrayIndex := 1;

begin

    -- set the values in TheRecords

    for i in TheRecords'Range loop
        TheRecords(i).firstItem  := i * 10;
        TheRecords(i).secondItem := i * 20;
    end loop;

    -- find which element of TheRecords has a
    -- firstItem with a value of 50

    for i in TheRecords'Range loop
        if TheRecords(i).firstItem = 50 then
            -- this next line is horrible: I should
            -- not be required to do type casting
            -- in a strongly-typed language.

            indexOfElement50 := ArrayIndex(i);
            exit;
        end if;
    end loop;

    Put(ArrayIndex'image(indexOfElement50));

end Main;

Everything down to the comment "find which element of TheRecords has a firstItem with a value of 50" is just setting up the problem I have (in a much larger program, of course).

Although coming from a C and Python world, I have been trying to be religious about strong typing in Ada. So, I have defined "indexOfElement50" carefully, and I would like it to be the index to the element in TheRecords which has a firstItem of 50. The loop starting under the comment is the code which searches for that element. And finds it!

But then I have to cast i to be an ArrayIndex. And casting is so wrong in a strongly typed world. I have tried using indexOfElement50 as the loop parameter, but the compiler won't have any of that.

So, it seems that I am forced either to declare indexOfElement50 as an integer (which breaks the guideline of restricting ranges as much as possible), or to perform type casting (which is great in C, but which I shouldn't be doing in a strongly-typed language).

Or, which is more likely, I have missed something really obvious and this will be pointed out enthusiastically by the experts.

3 Answers

I’d be inclined to reverse the approach a little.

with Ada.Text_IO; use Ada.Text_IO;

procedure Main is

You really don’t need to have a constant named Ten with value 10! What if you wanted to make it 12 later?

   type MyRecord is record
      firstItem  : Integer;
      secondItem : Integer;
   end record;

We want an array of records, but let’s postpone the decision of how long it needs to be ...

   type Record_Array is array (Positive range <>) of MyRecord;

... and define a test array, whose size happens to be 10 but could be anything.

   TheRecords : Record_Array (1 .. 10);

A valid result (for this test program) can only be in TheRecords’Range, but let’s add an out-of-range value to indicate ’not found’.

   subtype Possible_Index is Natural range 0 .. TheRecords'Last;
   indexOfElement50  : Possible_Index := 0;  -- indicates 'not found'

OK!

begin

   -- set the values in TheRecords

   for i in TheRecords'Range loop
      TheRecords(i).firstItem  := i * 10;
      TheRecords(i).secondItem := i * 20;
   end loop;

   -- find which element of TheRecords has a
   -- firstItem with a value of 50

   for i in TheRecords'Range loop
      if TheRecords(i).firstItem = 50 then
         indexOfElement50 := i;
         exit;
      end if;
   end loop;

   Put_Line (indexOfElement50'Image); -- legal in Ada2012

end Main;

I don't really know if this is what you want to achieve. But maybe it helps.

You can use ArrayIndex as index for the array:

TheRecords : array (ArrayIndex) of MyRecord;

but then you have to cast i to Integer:

TheRecords(i).firstItem  := Integer(i) * 10;
TheRecords(i).secondItem := Integer(i) * 20;

Full example:

with Ada.Text_IO; use Ada.Text_IO;

procedure Main is

    Ten : constant Positive := 10;

    type ArrayIndex is new Positive range 1 .. Ten;

    type MyRecord is record
        firstItem  : Integer;
        secondItem : Integer;
    end record;

    TheRecords : array (ArrayIndex) of MyRecord;

    indexOfElement50  :  ArrayIndex := 1;

begin

    -- set the values in TheRecords

    for i in TheRecords'Range loop
        TheRecords(i).firstItem  := Integer(i) * 10;
        TheRecords(i).secondItem := Integer(i) * 20;
    end loop;

    -- find which element of TheRecords has a
    -- firstItem with a value of 50

    for i in TheRecords'Range loop
        if TheRecords(i).firstItem = 50 then
            indexOfElement50 := i;
            exit;
        end if;
    end loop;

    Put(ArrayIndex'image(indexOfElement50));

end Main;

The major problem you have is that you declared a new type, and then didn't consistently use it.

That's not too surprising because types are no longer well taught, but they take practice to get good use out of them to catch real errors without letting them get in the way.

You're doing well : if you catch yourself writing too many type conversions (not casting) it's a sign that the design is wrong (a code smell), and you caught that.

Now I'm going to split your type declaration into two parts, to illustrate how I approach the problem.

with Ada.Text_IO; use Ada.Text_IO;

procedure Main is

    type FunnyInteger is new Positive;
    subtype ArrayIndex is FunnyInteger range 1 .. 10;

Ada offers both types, and subtypes.

Types are incompatible (without explicit conversions). Their job is to stop you making category errors, like adding feet and horses.

Subtypes are compatible with each other, but can express restrictions like a limited range of values.

So I have introduced a new type, of something that is not to be accidentally confused with Integer.

And I have named a subtype of it, to define the size of the array. This is important : because the array is indexed with this subtype, anything incompatible with that subtype cannot be used to index it ... goodbye Heartbleed. A new type would do the same : but also require type conversions somewhere, as you noticed.

Now,

type MyRecord is record
    firstItem  : Integer;
    secondItem : FunnyInteger;
end record;

One record field is type compatible with the index type; the other is not and must not be accidentally confused with it. This choice comes from the problem domain. If there can be no harm in mixing Integers with ArrayIndex, see the second example below.

Also, having declared the ArrayIndex (sub)type, use it consistently throughout...

TheRecords : array (ArrayIndex) of MyRecord;
indexOfElement50  :  ArrayIndex := 1;

begin

    for i in ArrayIndex loop
        TheRecords(i).firstItem  := Integer(i) * 10;
        TheRecords(i).secondItem := i * 20;
    end loop;

    for i in ArrayIndex loop
        if TheRecords(i).firstItem = 50 then
            indexOfElement50 := i;
            exit;
        end if;
    end loop;

    Put(ArrayIndex'image(indexOfElement50));

end Main;

Note that firstItem, being incompatible with our ArrayIndex requires a type conversion. This documents that we are breaking the type rules; a reviewer will pay attention to that, and it alerts the next guy to work on the code to pay attention.

I regard it like the "I meant to do that" look my cat gives me when he falls off the couch, again.

secondItem is compatible and requires no such conversion (because the problem domain allows us to).

If there was no reason to separate ArrayIndexand Integer, simply make ArrayIndex a subtype of Integer. It's still range protected, but no longer type protected. So you are in power : choose the level of protection you need.

Noting also that a range is a subtype without a name, we can simplify to

with Ada.Text_IO; use Ada.Text_IO;

procedure Main2 is

    type MyRecord is record
        firstItem  : Integer;
        secondItem : Integer;
    end record;

    TheRecords : array (1 .. 10) of MyRecord;
   
    indexOfElement50  : Integer range TheRecords'range;

begin
    for i in TheRecords'range loop
        TheRecords(i).firstItem  := i * 10;
        TheRecords(i).secondItem := i * 20;
    end loop;

    for i in TheRecords'range loop
        if TheRecords(i).firstItem = 50 then
            indexOfElement50 := i;
            exit;
        end if;
    end loop;

    Put(Integer'image(indexOfElement50));
end Main2;

Everything index-related is now directly derived from the array declaration, and range-protected, (but here assumed to be fundamentally compatible with our Integer type).

Note also I have left the bug Simon pointed out intact : if there are no matches you return 1 which is not the correct answer. Initialise to an out of range value :

indexOfElement50  : Integer range TheRecords'range := 0;

This compiles with a warning; because indexOfElement50 is range protected, and running it produces:

./main2
raised CONSTRAINT_ERROR : main2.adb:12 range check failed

at the initialisation, demonstrating the range protection. Simon explains well how to resolve that!

Related