Return array from reading a json (question relates to unconstrained array type)

Suppose in the main procedure we have an unconstrained array type defined:

type My_array is array (Integer range <>) of Float;

I’d like to create a function (in another package), that reads a json file and returns an array of floats contained in that file.

I am unsure of a good way to approach this. One thought is to read the json file, find out how long that array is (returning an integer from a querying function). Then, back in main, use “declare” block to instantiate the array of the correct length.

Finally, fill the array by using a 2nd subprogram that re-reads the json, and returns the array using an “out” parameter.

This all feels a bit silly. What is a good approach?

You can implement the whole JSON spec in an afternoon.
The approach I’d recommend is using tagged-types and a few “helpers”:

Package JSON is 
   -- All the various types a JSON-object can be; 'Collection' instead of array.
   Type Form is (Boolean, Numeric, Object, Collection, ETC);

   -- JSON floats can only hold the numeric values; no INF or NaN.
   Type Real is Float range Float'Range;

   Type Base(Style : Form) is abstract tagged null record;
       -- If you want to use streams:
       Function Create (Params : not null access Ada.Streams.Root_Stream_Type'Class)
       return Base is abstract;

   -- All common JSON functions.

   Type JSON_Vector is (Positive range <>) of [not null access] Base'Class;
   -- You could use Indefinite_Vector.

  Type Number is new Base(Style => Numeric) with record
     Value : Real:= raise Format_Error with "Needs Initialization.";
  end record;

   Type JSON_Array( Length : Natural ) is record
     Value : JSON_Vector(1..Length);
   end record;

  -- Etc.

--Private-- Typically I'd hide the implementation…

  -- If you want to use streams:
  -- For Make, use Generic_Dispatching_Constructor; remember:
  --    generic
  --       type T (<>) is abstract tagged limited private;
  --       type Parameters (<>) is limited private;
  --       with function Constructor (Params : not null access Parameters) return T is abstract;
  --    function Ada.Tags.Generic_Dispatching_Constructor (The_Tag : Tag; Params  : not null access Parameters) return T'Class;
  -- 
  -- For Parameters, use: Ada.Streams.Root_Stream_Type'Class
  -- For Constructor, use: Create

   Function Make_Instance is new Generic_Dispatching_Constructor(
      T           => Base,
      Parameters  => Ada.Streams.Root_Stream_Type'Class
      Constructor => Create
     );
   
End JSON;

Then you make each actual instance of BASE’s descendants “know how to create itself” given access to the stream (or text+cursor, as you wish). (Hint: the ‘array’ type reads in until the closing ‘]’; the opening of the call, is supplying the tag.)

JSON array can contain any values. So you need to validate correct but invalid input. You also should recon that there might be no way to have several passes. E.g. JSON input may come from the keyboard stream.

The approach in here is that the JSON parser allocates JSON object in the arena in one pass. You can then inspect the object and throw away the arena when no more needed. Doing so you can construct an array from JSON_Value if that value is an array and all elements of are JSON numbers. The number of elements is the array length.

P.S. If you are not forced to use JSON, then do not. It is an awful way to store/communicate/serialize dart.

Actually in case of JSON value a definite variant record type is sufficient.

I don’t understand why you need the length. In your declare block, directly assign the result of your function as usual.

declare
   Result : Json_Array := Some_Function;
begin
...

That’s true, but then you’re pushing everything up to the singular type; a tagged-type system allows you to (eg) define "+" on just the leaf-type that is Numeric. (Granted, JSON is a small format; so it’s not as unwieldy as systems with a dozen-plus classes of type.)

But, on the level of doing a single variant record, you could use Indefinite_Multiway_Tree along with using structure for the non-elementary types — eg the variant’s discriminant: Value_Null, Value_String, Value_Number, Value_Boolean; an object is a node with children of string themselves with a child of the value, and an array an object with multiple children. You’d have to do a little validation, like ensuring a Null_Value node has no children, and the like; but it doesgive you a nicely uniform way to deal with JSON.

Don’t know how you feel about this, but you could store the float’s in an Ada.Containers.Vectors vector. If you absolutely need an array, you could then declare one of appropriate size and copy its elements.

I realized my original posting could have been better focussed.

The question is really about “How does one return an array from a function, when the caller does NOT know what size array will be returned?” (as happens when reading data from a file.)

First is an non-working thought, which fails for reason that X is unconstrained subtype and not allowed (it needs initialization). But we do not know the size needed, so we can’t initialize.

procedure bad_example is

  type My_Array is array (Natural range <>) of Integer;
  X : My_Array;

  function read_array_from_file return My_Array is
    -- suppose the size is actually set by reading a data file
    -- instead of the silly value 3 here
    M : My_Array (1 .. 3) := (others => 1);
  begin
    return M;
  end read_array_from_file;
begin
  X := read_array_from_file;
end bad_example;

So, a solution is below. But it feels quite clumsy. Is there a more elegant way?

procedure working_example is
  type My_Array is array (Natural range <>) of Integer;

  function read_size_from_file return Integer is
    -- read data text file containing array values
    -- just to get the number of elements (fake example here)
    N_elements : Integer;
  begin
    N_elements := 4;
    return N_elements;
  end read_size_from_file;

  procedure fill_array_from_file(A : out My_Array) is
  begin
    for I in A'Range loop
      A(I) := I;  -- pretend we read from data file
    end loop;
  end fill_array_from_file;

  N : Integer;
begin
  N := read_size_from_file;

  declare
    A : My_array(1 .. N);
  begin
    fill_array_from_file(A);
  end;
end working_example;

This really is a general question. I feel I am missing something obvious.

Yes, but defining + would be useless because JSON is untyped. You do not have a specific type numeric anywhere but in a leaf. So unless you dispatch (and you cannot) you must check the type/tag manually and then cast class to the specific type. Checking discriminant is not any worse.

This is the case when subtype works better than tagged type:

type JSON_Value
     (  JSON_Type : JSON_Value_Type := JSON_Boolean
     )  is
record
   case JSON_Type is
      when JSON_Boolean =>
         Condition : Boolean := False;
      when JSON_Null =>
         null;
      when JSON_Number =>
         Value : Long_Float := 0.0;
      when JSON_String =>
         Text : JSON_String_Ptr;
      when JSON_Array =>
         Sequence : JSON_Sequence_Ptr;
      when JSON_Object =>
         Map : JSON_Pair_Array_Ptr;
   end case;
end record;

and a JSON array can be plain array without additional indirection level;

type JSON_Sequence is array (Positive range <>) of JSON_Value;

But I do not, because it is not needed. The structure above is already a tree and I do not want to allocate it in a pool. For JSON a LIFO memory allocation is sufficient. Therefore I can store it in an arena backed by a mark and release pool that can be backed by a storage element array.

Yes. As Shuihuzhuan pointed out, you can initialize an object of an unconstrained array with a function result. Consider

S : String := Ada.Text_IO.Get_Line;

String is an unconstrained array type. Ada.Text_IO.Get_Line is a function that returns a String. You do not need to know the length of the returned String in advance.

Thanks to all for the replies. I always learn a great deal from the varied responses.

Ahh… I missed the connection with the strings. Thanks for pointing this out.

I missed the connection of your response … sorry about this. It was spot on!

Isn’t that just shifting the logic of finding the array size to Some_Function though? I.e. Some_Function still needs a way to first discover the right size of the array prior to declaring the result object it will return..or am I largely missing something here? :thinking:

It feels like a vector might be a better structure to use for this use case.

Not really. I would not recommend the proposed solution for production code. Yet there is a trick how to handle this if you know in advance that the only input is JSON array (or CSV array etc):

type A is array (Positive range <>) of Float;
function Get return A is
   function Get_Chunk (Prefix : A) return A is
      Suffix : A (1..Chunk_Size);
   begin
      for I in Suffix'Range loop
         if Get_Delimiter (File) then -- E.g. skip comma and blanks
            Suffix (I) := Get_Element (File);
         else
            return Prefix & Suffix (1..I - 1); -- End of array, consume closing parenthesis
         end if;
      end loop;
      return Get_Chunk (Prefix & Suffix); -- Get following chunks
   end Get_Chunk;
begin
   -- Get prefix stuff, e.g. opening parenthesis
   return Get_Chunk ((1..0 => 0.0));
end Get;

This eats stack but works without accessing the pool. This has one character look-ahead. But it can be easily converted to zero look-ahead.

It depends, but most of the time, the callee knows the size of the array it will return to the caller because its algorithm builds the array.

I don’t know in this particular case, but vectors might be used internally, and the returned array might just be a convenience for the public API.