Array of tasks and using its index as discriminant

Just looking for an elegant solution…
Currently I have this procedure with a bunch of parallel workers in Zip-Ada’s BZip2 encoder:

      procedure Block_Split_Parallel is

        type Any_Splitting_Tactic is (single, halved, three, segmented_1, segmented_2, segmented_3);

        subtype Segmentation_Tactic is Any_Splitting_Tactic range segmented_1 .. segmented_3;

        procedure Execute_Tasks is

          task Do_Single_Block;
          task Do_Halved_Block;
          task Do_Block_Cut_In_Three;
          task type Do_Segmented_Block_Type (tactic : Segmentation_Tactic);
          Do_Segmented_Block_1 : Do_Segmented_Block_Type (segmented_1);
          Do_Segmented_Block_2 : Do_Segmented_Block_Type (segmented_2);
          Do_Segmented_Block_3 : Do_Segmented_Block_Type (segmented_3);

It works fine but it is quite ugly. Ideally I would rather have an array of tasks instead of the three last tasks listed above.

Do_Segmented_Block : array (Segmentation_Tactic) of Do_Segmented_Block_Type (...)

The index of the array would be the discriminant. Any idea?

If you are willing to use a default discriminant and a constructing function, it can be done like this:

procedure Hello is
    type Any_Splitting_Tactic is (single, halved, three, segmented_1, segmented_2, segmented_3);

    subtype Segmentation_Tactic is Any_Splitting_Tactic range segmented_1 .. segmented_3;

    procedure Execute_Tasks is

        -- Notice the default discriminant.  It allows you to make an array of the tasks
        task type Do_Segmented_Block_Type (tactic : Segmentation_Tactic := segmented_1);

        task body Do_Segmented_Block_Type is
        begin
            null;
        end Do_Segmented_Block_Type;

        -- The construcing function allows you to build the tasks so you can return them
        -- to initialize the array elements
        function Make_Task(Tactic : Segmentation_Tactic) return Do_Segmented_Block_Type is
        begin
            return Result : Do_Segmented_Block_Type(Tactic);
        end Make_Task;

        Task_Array : array (Segmentation_Tactic) of Do_Segmented_Block_Type:= 
            (Make_Task(Segmented_1),
             Make_Task(Segmented_2),
             Make_Task(Segmented_3));
    begin
        null;
    end Execute_Tasks;
begin
   Put_Line("Hello, World!");
end Hello;

There might be better ways, but that is one I’ve used in the past that came to mind.

1 Like

You can create an array aggregate of a limited type these days, and a function that returns a task is also well defined now, so this should work, so long as there is a default for the discriminant:

          task type Do_Segmented_Block_Type
            (tactic : Segmentation_Tactic := segmented1);
          function SB (X : Segmentation_Tactic) return Do_Segmented_Block_Type is
          begin
             return Result : Do_Segmented_Block_Type (X);
          end SB;
          Do_Segmented_Block : 
            array (Segmentation_Tactic) of Do_Segmented_Block_Type :=
              [for seg in Segmentation_Tactic => SB (seg)];
2 Likes

Why a default for the discriminant is needed ?

To make it mutable ?

To allocate an array you need to know the size of all the elements at compile time.

If the discriminant does not have a default, each variant of the record can have a different size, so it would be impossible to have an array of varying sized objects, the compiler wouldn’t know how much memory to allocate.

When you give it a default discriminant, the compiler will choose a single size for all variants, allowing it to be a known size at compile time. As mentioned above, compilers like GNAT pick the largest size variant and allocate that amount always (which can be wasteful if one variant is really big and all the others are smaller). Other compilers like JanusAda will instead just use a pointer in the record and heap allocate under the hood.

Without a default discriminant, the type is indefinite; you can’t declare an object of the type without providing a value for the discriminant:

task type Do_Segmented_Block_Type (Tactic : Segmentation_Tactic);

V : Do_Segmented_Block_Type; -- Illegal

Array components have to be definite. Adding a default discriminant makes the type definite:

task type Do_Segmented_Block_Type (Tactic : Segmentation_Tactic := Segmented_1);

V : Do_Segmented_Block_Type; -- OK, discriminant is Segmented_1

Tasks cannot be mutable.

I don’t see any mention of this “above” in this topic. Am I missing something? There is a reference to this in this topic. While relevant to variant records, it’s not an issue for task types.

1 Like

If you’re willing to use a version of the language for which no compiler exists, you can do

type Disc is (A, B, C);
task type T (D : Disc := A);
type T_Set is array (Disc) of T;
function New_T (D : in Disc) return T;
Set : T_Set := (for D in Disc => New_T (D) );

(I think).

That’s something I don’t understand.

There’s nothing indefinite in Segmentation_Tactic . This is a subtype of Any_Splitting_Tactic type and there’s nothing undefined here.

In the task type Do_Segmented_Block_Type, the Tactic discriminant is of type Segmentation_Tactic which is perfectly known, initialized or not.

If the compiler uses the largest size variant (which makes sense), there’s no point to specify a default value since whichever the default value is, the largest size variant will be the same.

If you specify a default, the record must be able to hold all variants at runtime (you can change the variant when there is a default discriminant), so it has to pick a consistent size appropriate to do that. If you don’t specify a default, then the variant cannot change and the size would just be the size of the variant selected when you initialize the object (it would be wasteful to pick the largest size for all variants in this case since you couldn’t change an object from smaller size to larger size variants).

Yeah I mistakenly remembered it from another thread and for some reason thought it was in this thread. I was trying to give you credit for the GNAT/Janus comparison. I’m human and make mistakes. My apologies.

1 Like

That’s counter intuitive. The opposite would make more sense.

However, thanks for the explanation. That’s very clear.

Following Jere’s answer, I understand that the use case in the original post is a specific one (discriminant’s type is definite) and that in a general use case the discriminant’s type can be indefinite so we have to provide a default value. Right ?

I think the discriminant itself has to always be definite (A discrete type, access type, etc). It’s more that it makes the overall type indefinite since it can be one of many things (and at a lower level, in the general case, potentially one of many sizes).

No problem, if it helps to illustrate why it matters (in general), here are some contrived examples:

example 1

type Variant_1(Is_Populated : Boolean) is record
   case Is_Populated is
      when False => null; -- no data
      when True  => Integers : Integer_Array(1..100);  -- 100x integers, consider 32bits each
   end case;
end record;

v1 : Variant_1(False);  -- Total size = Is_Populated'Size
v2 : Variant_1(True);   -- Total size = Is_Populated'Size + 100 * Integer'Size + maybe array bounds

You definitely don’t want all the Variant_1(False) objects to have the same size as Variant_1(True) objects.

example 2

type Variant_2(Capacity: Positive) is record
   Integers : Integer_Array(1..Capacity);  -- Capacity x integers, consider 32bits each
end record;

v1 : Variant_2(100);    -- Total size = Capacity'Size + 100  * Integer'Size + maybe array bounds
v2 : Variant_2(1000);   -- Total size = Capacity'Size + 1000 * Integer'Size + maybe array bounds

Here the size of the array (and thus the record) changes for every value supplied. Also note that a programmer can choose to supply the discriminant’s value at runtime (like prompt a user for the size of Variant_2 and create an object of exactly that size after the user inputs it. So the compiler has to handle that case as well.

You might notice that in both of those examples, the discriminant is a definite type (Boolean for Variant_1, Positive for Variant_2), but the inner working of the overarching record (or task) type could be indefinite because of the discriminant.

In the OP’s specific example, the Task object’s size wouldn’t change due to the discriminant, but in a general language sense, it could have. The ARG could have maybe put in verbiage in the Ada language spec to account for the specific cases like that, but it didn’t (not sure why, but there’s probably some underlying reasoning)

Side note: A default discriminant for Variant_2 would potentially be a bad idea for some compilers. Take GNAT that allocates the largest variant to hold all, for Variant_2 type that would be Positive’Last which makes the array have that many elements, which isn’t a practical size and would most likely just raise Storage_Error (for a 32bit positive it would be 4294967295x Integers in the array).

1 Like

Thanks for your explanations. That’s very instructive.

Perhaps I should have said, “Without a default discriminant, the task type is indefinite”.

The discriminant type is always definite, so I didn’t think the qualification was needed.

Makes more sense. Thanks.

Thanks for sharing, like you say that may not be a feasible option for compiler implementers but maybe the compiler could even be made smart enough to apply the default-discriminant-size rule only when that record type is an array type and not otherwise. As you mention, in example 2 if a discriminant were to be specified then both v1 and v2 would have a size of the max Positive value but there’s no memory layout reason this should be required (or is there, not sure :thinking:)

Returning to the original question though, I was also wondering whether an array of accesses to the task type would work too, as in:

task type Do_Segmented_Block_Type2 (tactic : Segmentation_Tactic);

  task body Do_Segmented_Block_Type2 is
  begin
      Put_Line ("Hello with " & tactic'Image);
  end Do_Segmented_Block_Type2;
      
  type Task_Acc is access Do_Segmented_Block_Type2;

and then declare an array like (also, the task type has no default discriminant since an access type is a definite type):
Task_Array_2 : array (Segmentation_Tactic) of Task_Acc := [Make_Task(Segmented_1)'Access];
But that doesn’t seem to work I think because of type incompatibility (i.e. we’re giving the array an anonymous access type whereas a solid one is expected). Trying to convert to the declared access type doesn’t seem to work either.