Type
introspection is a language feature which allows to determine properties of a
type at run-time. Ada is a statically typed language so no introspection is
needed, most of the time. When it comes to class-wide types their instances are
not so static.
One might
think that Ada does not support type introspection, which is almost true, but
not quite true. In fact, Ada (starting with Ada 95) has features which can be
used to introspect types. This is stream-oriented attributes (RM 13.13.2). The
language generates default implementations of T'Read and T'Write based on the
type contents. Of course, the compiler knows the type components in order to be
able to generate these implementations. Can this knowledge be used for run-time
introspection? Yes it can.
We start with the provisions of RM 13.13.2(9):
"...
For type extensions, the Write or Read attribute for the parent type is called,
followed by the Write or Read attribute of each component of the extension
part, in canonical order. ..."
This gives
us an idea of calling T'Write on the container type noticing what its
implementation would call for the components.
First we
declare the abstract base type of the container:
type Container is abstract tagged limited
null record;
procedure Introspect
( Stream : access Root_Stream_Type'Class;
Item : Container
) is null;
for Container'Write use Introspect;
Then the
abstract base type of the members:
type Member is abstract tagged limited null
record;
procedure Introspect
( Stream : access Root_Stream_Type'Class;
Item : Member
);
for Member'Write use Introspect;
And finally
a fake stream type which will collect the information of the container type
contents:
package List_Of_Tags is
new
Ada.Containers.Vectors (Positive, Tag);
use List_Of_Tags;
type Scriber is new
Ada.Streams.Root_Stream_Type with record
List : Vector;
end record;
procedure Read
( Stream : in out Scriber;
Item : out Stream_Element_Array;
Last : out Stream_Element_Offset
) is null;
procedure Write
( Stream : in out Scriber;
Item : Stream_Element_Array
) is null;
The read
and write implementations do nothing. The stream contains a vector of type
tags, written from the implementation of member's write:
procedure Introspect
( Stream : access Root_Stream_Type'Class;
Item :
Member
) is
begin
Scriber'Class (Stream.all).List.Append
( Member'Class (Item)'Tag
);
end Introspect;
This is
basically all. Putting the above in a package, e.g. Introspection, it could be
used as follows:
with
Ada.Tags; use Ada.Tags;
with
Ada.Text_IO; use Ada.Text_IO;
with
Introspection; use Introspection;
procedure
Test is
type A is new Member with null record;
type B is new Member with null record;
type C is new Container with record
X : A;
Y : B;
end record;
Object
: C;
Contents : aliased Scriber;
begin
C'Write (Contents'Access, Object);
for Index in
Contents.List.First_Index
.. Contents.List.Last_Index
loop
Put (Integer'Image (Index));
Put (" ");
Put (Expanded_Name
(Contents.List.Element (Index)));
New_Line;
end loop;
end Test;
This will
print:
1
TEST.A
2
TEST.B
Final
notes:
- The derived types of Container and Member should not override stream
attributes. This will make the compiler to generate them.
- The method works
recursively, that is you can have container types put into containers all
introspected. The trick works as follows. From T'Write of the nested container
you can introspect it using another stream object and use the information obtained.
- Be careful introspecting from Initialize. When Initialize is called, the derived type's Initialize is not yet completed, i.e. the type is not fully constructed. This is also the reason why T'Write is better to use than T'Read, even if T'Read does not actually update the object.