Initializing an array of Strings

I created an Ada program to initialize the first five strings in an array of 10 Strings. As a proof that my program works it then loops through the array until encountering a string whose value is “DONE”. It works, but it’s ugly:

with Ada.Text_IO;
with Ada.Strings.Fixed;

procedure String_Loop is
   subtype String20 is String(1..20);
   type Text_Arr is array(1..10) of String20;
   Words: Text_Arr;
   ARR_SS: Integer := 1;

begin
   Ada.Strings.Fixed.move("One",   Words(1)); 
   Ada.Strings.Fixed.move("Two",   Words(2)); 
   Ada.Strings.Fixed.move("Three", Words(3)); 
   Ada.Strings.Fixed.move("Four",  Words(4)); 
   Ada.Strings.Fixed.move("DONE",  Words(5)); 
   loop
      exit when Ada.Strings.Fixed.Trim(
         Words(Arr_SS),
         Ada.Strings.Right)
         = "DONE";
      Ada.Text_IO.Put_line(Words(Arr_SS));
      Arr_SS := Arr_SS + 1;
   end loop;
Ada.Text_IO.Put_Line("End of program");
end String_Loop;

Is there a simpler/better/more readable way I could have done this? Thanks!

I would recommend using an indefinite vector (dynamic array) of Strings:

pragma Ada_2022;

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Containers.Indefinite_Vectors;

procedure jdoodle is
    package Vectors is new Ada.Containers.Indefinite_Vectors(Positive,String);
    subtype Text_Array is Vectors.Vector;
    Words : Text_Array := ["One", "Two", "Three", "Four", "DONE"];
begin
    for Word of Words loop
        exit when Word = "DONE";
        Put_Line(Word);
    end loop;
    Put_Line("End of program");
end jdoodle;

Output:

One
Two
Three
Four
End of program
3 Likes

Vectors are definitely the best route for this if you don’t need to work with fixed strings. However, if you absolutely need to use an array of fixed strings, you can still use the iteration in @jere’s solution:

pragma Ada_2022;

with Ada.Text_IO;
with Ada.Strings.Fixed;

procedure String_Loop is
   subtype String20 is String(1..20);
   type Text_Arr is array(1..10) of String20;
   Words: Text_Arr;
   ARR_SS: Integer := 1;

begin
   Ada.Strings.Fixed.move("One",   Words(1)); 
   Ada.Strings.Fixed.move("Two",   Words(2)); 
   Ada.Strings.Fixed.move("Three", Words(3)); 
   Ada.Strings.Fixed.move("Four",  Words(4)); 
   Ada.Strings.Fixed.move("DONE",  Words(5)); 
   for Word of Words loop
       exit when Ada.Strings.Fixed.Trim(
         Word, Ada.Strings.Right) = "DONE";
       Ada.Text_IO.Put_Line(Word);
   end loop;
Ada.Text_IO.Put_Line("End of program");
end String_Loop;
2 Likes

Ok, most of the troubles with Ada’s String-type comes from a simple misunderstanding of array-types in Ada, namely unconstrained arrays. (I go over it here, explicitly on Strings, but will reiterate; hopefully in a more understandable manner.)

Ok, to start, let us make a few types for illustration:

   Type    Elementary is range 32..64;
   Type    Indexia    is range  0..12;
   Subtype Positivity is Indexia'Succ(Indexia'First)..Indexia'Last;

   Type    Vectoria   is array (Positivity range <>) of Elementary;
   Subtype Fixedia    is Vectoria( 8..11 );

So, here we have Vectoria, an array indexed by Indexia, but which doesn’t have any particular length associated with the type… Fixedia, on the other hand, does have an inherent length: the range of 8 through 11, or a length of 4.

Now, because Vectoria does not have a particular length, we don’t know how much space it takes up, so we cannot say X : Vectoria;… there are two ways to get around this: (a) specifying the index-range in the type-portion of the variable (X : Vectoria(4..7);), or (b) specifying the initial value (X : Vectoria := (33, 48, 60, 55);). — These options are constraining the indefinite nature of the type into something definite.

Once X’s type is definite, it thus has the length associated with it, and this cannot change: so, given that X has a length of 4 in both of those examples, you cannot say either X:= (44, 55, 33); or X:= (64, 54, 44, 34); for the simple reason that the length/indices do not match.

Now, a slight detour, we mention “slices” and “index-sliding” — in the (a) example above the indices are 4..7, but the following is legal X:= Fixedia'(32,42,53,64);, this is because the indices 8..11 of Fixedia are ‘slid’ into the 4..7 that we specified. Indeed, given

Y : Constant Vectoria:= (1 => 64,  2 => 63,  3 => 62,  4 => 61,
                         5 => 60,  6 => 59,  7 => 58,  8 => 57,
                         9 => 56, 10 => 55, 11 => 54, 12 => 53);

we could say X:= Y(X'Range);, which copies the values of the slice on 4..7(61,60,59,58)— into X. Likewise, we could say X:= Y(8..11);, and [IIRC] we could say X:= Y(Fixedia'Range) for 8..11 (because it is length 4), using both slicing and sliding to get the appropriate “window” of our table-of-values.

Now, onto String — but String is not special.
It is simply an array which has elements of a Character-type; which in Ada is specified as any enumeration which has in its definition a character-literal, an element surrounded by single quotes. (See here.) / The only “special” thing about a String type is that you can enclose in double-quotes a sequence of said character-literals. So…

Type Silly_Character is (NUL, BELL, HT, 'A', 'B', DEL);
Type Silly_String    is array(Positive range <>) of Silly_Character;

given the above, you can say K : Silly_String:= "AAAB";, which is shorthand for K : Silly_String:= (1 => 'A', 2 => 'A', 3 => 'A', 4 => 'B'). Now, to use the non-graphic characters, it forces us to use an explicit array-aggregate as in the example, or else use the & operator, so something like J : Silly_String:= BELL & BELL & "BB" & NUL & 'A';.

So, that is why a lot of newcomers to Ada get tripped up on Strings: they forget that they are working with arrays, which are really quite versatile (albeit with some limitations) in Ada because they can be unconstrained.

Hope that helps.

2 Likes

Thanks! Your program works as long as I include the pragma Ada_2022. I’ll need to study it to really understand it. A couple questions:

  1. What are the disadvantages of pragma Ada_2022 ?
  2. Are there ways I can do this same vector thing without the pragma Ada_2022 ?
  1. You need an Ada 2022 compiler.
  2. You use the usual way of initialization by providing helper types aggregating some binary operation. In the case of vectors such operation already exists:
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Containers.Indefinite_Vectors;

procedure Test is
    package Vectors is new Ada.Containers.Indefinite_Vectors (Positive, String);
    use Vectors;
    subtype Text_Array is Vectors.Vector;
    Words : Text_Array := Empty & "One" & "Two" & "Three" & "Four" & "DONE";
begin
    for Word of Words loop
        exit when Word = "DONE";
        Put_Line(Word);
    end loop;
    Put_Line("End of program");
end Test;

Note, that Empty is needed only because “&” for String makes expression without Empty ambiguous. You can rename it:

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Containers.Indefinite_Vectors;

procedure Test is
    package Vectors is new Ada.Containers.Indefinite_Vectors (Positive, String);
    use Vectors;
    subtype Text_Array is Vectors.Vector;
    function "/" (Left, Rgiht : String) return Vector renames "&";
    function "/" (Left : Vector; Rgiht : String) return Vector renames "&";
    Words : Text_Array := "One" / "Two" / "Three" / "Four" / "DONE";
begin
    for Word of Words loop
        exit when Word = "DONE";
        Put_Line(Word);
    end loop;
    Put_Line("End of program");
end Test;
1 Like

Hey @stevelitt. You might be interested in one of the Ada letters here about a ragged array. There are ways to do so using access for minimal memory use but this discriminated record way is much nicer so long as you don’t mind the extra storage use like a fixed string package. The range on the string will be right despite the capacity if you will being static. An unusually subtle for Ada feature of discriminated records is that one with a default discriminator but uninitialised is mutable and can be replaced with a new record with a differing size discriminant and so differing sized string within each record in this case.

https://dl.acm.org/doi/pdf/10.1145/1041339.1041340

1 Like

Well, in this case the extra memory is many megabytes. The GNAT compiler makes Rec’Size huge.
Then it is not an array of strings anyway. If you are not shy to have A (I),S instead of A (I), then it could as well be A (I).all.

I think the implication here is if you use the record approach,then you constrain the type to reduce the value of Rec'Size. Like for the OP’s initial example you would use a subtype of Positive that was bounded 1…10 or whatever is big enough to hold all of your intended strings.

Not intending to agree/disagree with your point, just adding clarification to Kevlar’s post.

1 Like

I know no way to constraint an array subtype index leaving it indefinite.

The way variable string arrays can be done is like this:

type Variable is record
   Length : Natural := 0;
   Buffer : String (1..100);
end record;
type Text_Array is array (Positive range <>) of Variable;

Unfortunately there is no way to add implicit conversions between Variable and String.

That is discussed in the letter within the pdf. Gnat warns you if you might try to use stack space upto Integer’Last. The difference to a usual fixed string is that you don’t have to take care of padding. The range is mutable but the stack allocation is not.

The PDF in question was suggesting something similar to:

subtype Buffer_Size is Natural range 0..100;
type Variable(Length : Buffer_Size := 0) is record
   Buffer : String (1..Length);
end record;
type Text_Array is array (Positive range <>) of Variable;

which has a known size but whose value of Buffer can be separately initialized to different lengths. See:

with Ada.Text_IO; use Ada.Text_IO;

procedure jdoodle is
    subtype Buffer_Size is Natural range 0..100;
    type Variable(Length : Buffer_Size := 0) is record
       Buffer : String (1..Length);
    end record;
    type Text_Array is array (Positive range <>) of Variable;
    
    Words : Text_Array :=
        (1 => (Length => 3, Buffer => "One"),
         2 => (Length => 3, Buffer => "Two"),
         3 => (Length => 5, Buffer => "Three"),
         4 => (Length => 4, Buffer => "Four"),
         5 => (Length => 4, Buffer => "DONE"));
begin
    Put_Line("Variable'Size =>" & Variable'Size'Image);
    for Word of Words loop
        exit when Word.Buffer = "DONE";
        Put_Line(Word.Buffer);
    end loop;
    Put_Line("End of Program");
end jdoodle;

Output:

Variable'Size => 832
One
Two
Three
Four
End of Program
3 Likes

Here is a variant with strings allocated in a stack memory pool used as an arena. All strings go down with the arena pool:

with Ada.Text_IO;    use Ada.Text_IO;
with Stack_Storage;  use Stack_Storage;

procedure Test is
   Arena : Pool (100, 10); -- 100 bytes initially
   type String_Ptr is access String with Storage_Pool => Arena;
   type Text_Array is array (Positive range <>) of String_Ptr;
   function "+" (S : String) return String_Ptr is (new String'(S));
   Words : constant Text_Array := (+"One", +"Two", +"Three", +"Four", +"DONE");

begin
   Put_Line ("Pool size:" & Arena.Storage_Size'Image);
   for Word of Words loop
      exit when Word.all = "DONE";
      Put_Line (Word.all);
   end loop;
   Put_Line("End of program");
end Test;

Output:

Pool size: 100
One
Two
Three
Four
End of program
2 Likes

Where does the Stack_Storage package come from ?
Simple Components for Ada ?

Yes.

@stevelitt I know you have a lot of C experience and containers/vectors are great but you might be interested in this which demonstrates using access types, even if you never or rarely use it. I’m not actually sure why it says the Ada 95 way is simplified. Perhaps for the compiler with constants or because the array is indefinite?

This is a special case when strings are never freed.

I cannot tell why is this simpler but it reduces the memory footprint because in

   new String'("One")

you have “One” stored twice, once as a constant somewhere in the program image and once as a dynamically allocated string in the heap. The example:

     type String_Access is
        access constant String;
  
     One   : aliased constant String := "One";
     Two   : aliased constant String := "Two";
     Three : aliased constant String := "Three";
   
     Strings : constant array (Positive range <>) of String_Access
             := ( 1 => One'Access,
                  2 => Two'Access,
                  3 => Three'Access
                );

stores “One” once.and uses no heap.

1 Like

This is the technique I used in my EWS project; I’d hoped it would avoid elaboration code. Maybe it did with GNAT 3.16a1 in 2010, but it doesn’t now.