Yuni Tsukiyama
Yuni Tsukiyama

Reputation: 121

How to get around forbidden discriminants defaults for tagged records in Ada?

I am learning Ada and I've hit a design problem. Excuse me as I'm not up with basic Ada mechanisms and idioms.

Let's say I want to represent an operation. Operators can be either plus or minus and operands can be either integers or strings.

Disclaimer: some things may not make much sense on a semantic level (minus on strings, operators without operands, ...) but it's all about representation.

For now I have the following incorrect code:

operand.ads:

package Operand is

   --  I want a None for unary operands or operators without operands
   type Operand_Type is (Int, Str, None);

   --  This needs to be tagged
   type Instance (Op_Type : Operand_Type := None) is tagged record
      case Op_Type is
         when Int =>
            Int_Value : Integer;
         when Str =>
            Str_Value : String (1 .. 10);
         when None =>
            null;
      end case;
   end record;

   --  Some methods on operands...

end Operand;

operation.ads:

with Operand;

package Operation is

   type Operation_Type is (Plus, Minus);

   --  This also needs to be tagged
   type Instance is tagged record
      Left, Right : Operand.Instance;
   end record;

   --  Some methods on operations...

end Operation;

main.adb:

with Operand;
with Operation;

procedure Main is
   Op : Operation.Instance;
begin
   Op.Left := (Op_Type => Operand.Int, Int_Value => 1);
   Op.Right := (Op_Type => Operand.Int, Int_Value => 3);
end Main;

When I try to compile I get the following errors:

$ gnatmake main.adb
gcc -c main.adb
operand.ads:7:45: discriminants of nonlimited tagged type cannot have defaults
operation.ads:9:28: unconstrained subtype in component declaration
gnatmake: "main.adb" compilation error

I get why I can't use defaults on tagged type's discriminant but I don't really know how to get around this limitation.

Proposal 1:

Stop using variant records and use a record with one field for each operand. But I feel like this is just throwing away code elegance.

Proposal 2:

Remove defaults from Operand.Instance record and constrain Left and Right from Operation.Instance record. But I get a runtime error :

raised CONSTRAINT_ERROR : main.adb:7 discriminant check failed

As I cannot dynamically change discriminant value of a record's field.

Any help would be much appreciated !

Upvotes: 6

Views: 291

Answers (1)

Jere
Jere

Reputation: 3641

Jim Rogers already discussed using inheritance. You can also use composition if you like by creating an internal non tagged type (which allows defaults), make the Operand.Instance type tagged private, have the private implementation use the internal non tagged version, and just add what operations you need to set and get the operands:

with Ada.Text_IO; use Ada.Text_IO;
procedure Hello is

    package Operand is

        --  I want a None for unary operands or operators without operands
        type Operand_Type is (Int, Str, None);
        Invalid_Operand : exception;
        
        --  This needs to be tagged
        type Instance is tagged private;
        function Int_Value(Value : Integer) return Instance;
        function Str_Value(Value : String) return Instance;
        function Null_Instance return Instance;
        
        function Int_Value(Self : Instance) return Integer;
        function Str_Value(Self : Instance) return String;
        function Op_Type(Self : Instance) return Operand_Type;
        
        --  Some methods on operands...
        
    private
    
        
        type Instance_Internal (Op_Type : Operand_Type := None) is record
            case Op_Type is
                when Int =>
                    Int_Value : Integer;
                when Str =>
                    Str_Value : String (1 .. 10);
                when None =>
                    null;
            end case;
        end record;
        
        type Instance is tagged record
            Impl : Instance_Internal;
        end record;
        
        function Int_Value(Value : Integer) return Instance is (Impl => (Int, Value));
        function Str_Value(Value : String)  return Instance is (Impl => (Str, Value));
        function Null_Instance              return Instance is (Impl => (Op_Type => None));
        
        function Int_Value(Self : Instance) return Integer
            is (if Self.Impl.Op_Type = Int then Self.Impl.Int_Value else raise Invalid_Operand);
        function Str_Value(Self : Instance) return String
            is (if Self.Impl.Op_Type = Str then Self.Impl.Str_Value else raise Invalid_Operand);
        function Op_Type(Self : Instance) return Operand_Type
            is (Self.Impl.Op_Type);

    end Operand;
    
    package Operation is

        type Operation_Type is (Plus, Minus);
    
        --  This also needs to be tagged
        type Instance is tagged record
            Left, Right : Operand.Instance;
        end record;
    
       --  Some methods on operations...
    
    end Operation;
    
    Op : Operation.Instance;

begin
    Put_Line("Hello, world!");
    
    Op.Left  := Operand.Int_Value(1);
    Op.Right := Operand.Int_Value(3);
    
    Put_Line(Integer'Image(Op.Left.Int_Value));
    Put_Line(Integer'Image(Op.Right.Int_Value));
end Hello;

You can break the Operand package into spec and body for better readability, this was just for example.

Upvotes: 5

Related Questions