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.
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 isNumeric. (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.
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?
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.