Discriminated & Variant Records

Parameterize records with discriminants (sizing array components), write variant records whose components depend on a discriminant, understand discriminant checks, constrained vs mutable objects with default discriminants ('Constrained), and when to prefer tagged types instead.

A discriminant is a special component that parameterizes a record type, much like the bounds of an unconstrained array. Discriminants can size array components (a buffer whose capacity is chosen per object) or select which components exist (a variant record, Ada's safe tagged union). Every access to a variant component is checked against the discriminant, so the classic C union bug — reading the wrong member — is impossible.

Discriminants that Size Components

'type Buffer (Capacity : Positive) is record Data : String (1 .. Capacity); Used : Natural := 0; end record;' Each object chooses its capacity: B : Buffer (Capacity => 5);. The discriminant is read like a component (B.Capacity) but cannot be assigned individually. Objects declared with a discriminant constraint stay constrained for their lifetime. This gives variable-sized records without any heap allocation.

• type R (D : Discrete_Or_Access_Type) is record ... end record; • Discriminants can size array components. • Read with Obj.D; cannot be assigned individually.

Variant Records

A variant part is a case construct inside the record: 'case Kind is when Circle => Radius : Float; when Rectangle => Width, Height : Float; ... end case;'. It must come after the common components and cover every discriminant value (when others => null is allowed). Aggregates must include the discriminant and exactly the components of the chosen variant. Accessing a component not present in the object's variant raises Constraint_Error (a discriminant check) — GNAT even warns at compile time when it can see the mismatch.

• case Discriminant is when V => components; ... end case; (last in the record). • Aggregates include the discriminant and the matching variant's components. • Wrong-variant access → Constraint_Error.

Mutable Objects and Default Discriminants

If the discriminant has a default ('type Measurement (Valid : Boolean := False)'), you can declare an object without a constraint (M : Measurement;). Such an object is mutable: a whole-record assignment may change its discriminant (M := (Valid => True, Value => 42);). The attribute M'Constrained tells whether an object is constrained. The compiler typically reserves space for the largest variant — beware defaults on discriminants that size large arrays (a Positive default would require space for Positive'Last components!).

Variant records are a closed set: every case statement over Kind must be updated when a new variant is added — the compiler finds them. Tagged types (the OOP lessons) are an open set: new kinds can be added without modifying existing code. Choose variants when the set of alternatives is fixed and small and operations are added often; choose tagged types when kinds are added often.

• Default discriminant → unconstrained objects are mutable via whole assignment. • X'Constrained queries mutability. • Beware defaults on array-sizing discriminants (worst-case storage). • Variants = closed set of kinds; tagged types = open set.

Code Examples

GNAT warns at compile time that C.Width does not exist for a Circle; at run time the discriminant check raises Constraint_Error. M starts unconstrained with Valid = False and changes variant through whole-record assignment. The sixth Add fails the index check before Used changes. Output: Buffer capacity 5, holds "Ada" Area of circle = 1.25664E+01 Area of rectangle= 1.20000E+01 Area of triangle = 1.50000E+01 C.Width on a Circle raised Constraint_Error (discriminant check) M'Constrained = FALSE, Valid = FALSE After assignment: Valid = TRUE, Value = 42 Overfilling the buffer raised Constraint_Error, Used = 5 S.Kind:= Rectangle;

S := (Kind => Rectangle, Name => 'r', Width => 1.0, Height => 2.0);

A discriminant cannot be assigned alone; replace the whole (mutable) object.

type Text (Len: Natural:= 0) is record Data: String (1.. Len); end record; T: Text; -- may reserve Natural'Last bytes subtype Text_Length is Natural range 0.. 256; type Text (Len: Text_Length:= 0) is record...

Mutable objects reserve worst-case space; keep the discriminant's range small.

Area:= C.Width * C.Height; -- C is a Circle case C.Kind is when Rectangle =>...; when others =>...; end case;

Test the discriminant before touching variant components.

What happens when you read a component that is not in the object's variant? A. Garbage value B. Constraint_Error C. Returns 0 D. Program_Error

B. Constraint_Error (discriminant check).

When can an object's discriminant change? A. Never B. By assigning the discriminant alone C. By whole-record assignment if the object is unconstrained (default discriminant) D. Always

What does X'Constrained return for 'M: Measurement;' with a default discriminant? A. True B. False C. Compile error D. Depends on the value B. False (verified in the example).

Which is better when new kinds of shapes are added frequently by different teams? A. Variant record B. Tagged type hierarchy C. Unchecked_Union D. Array of Floats

B. Tagged types are open to extension without modifying existing code.

Trace the discriminated/variant record example.

Real output: Buffer capacity 5, holds "Ada" Area of circle = 1.25664E+01 Area of rectangle= 1.20000E+01 Area of triangle = 1.50000E+01 C.Width on a Circle raised Constraint_Error (discriminant check) M'Constrained = FALSE, Valid = FALSE After assignment: Valid = TRUE, Value = 42 Overfilling the buffer raised Constraint_Error, Used = 5

Add a Perimeter function for Shape using a case expression.

function Perimeter (S : Shape) return Float is (case S.Kind is when Circle => 2.0 * 3.14159 * S.Radius, when Rectangle => 2.0 * (S.Width + S.Height), when Triangle => raise Program_Error with "needs side lengths");

The case expression must cover all kinds; Ada 2012 raise expressions let an alternative signal that it is unsupported.

with Ada.Text_IO; use Ada.Text_IO;

procedure L24_Variant is
   --  A discriminant parameterizes the record: here it sizes an array component
   type Buffer (Capacity : Positive) is record
      Data : String (1 .. Capacity);
      Used : Natural := 0;
   end record;

   procedure Add (B : in out Buffer; C : Character) is
   begin
      B.Data (B.Used + 1) := C;   --  index check happens before Used changes
      B.Used := B.Used + 1;
   end Add;

   --  A variant record: which components exist depends on the discriminant
   type Shape_Kind is (Circle, Rectangle, Triangle);

   type Shape (Kind : Shape_Kind) is record
      Name : Character;
      case Kind is
         when Circle =>
            Radius : Float;
         when Rectangle =>
            Width, Height : Float;
         when Triangle =>
            Base, Altitude : Float;
      end case;
   end record;

   function Area (S : Shape) return Float is
     (case S.Kind is
        when Circle    => 3.14159 * S.Radius * S.Radius,
        when Rectangle => S.Width * S.Height,
        when Triangle  => 0.5 * S.Base * S.Altitude);

   --  With a default discriminant, unconstrained objects are mutable:
   --  the discriminant can 
…