Before I answer the question, it would probably be helpful to distinguish between Ada and Java/C#'s modeling of objects. In Java, everything is an object, and so all constants have to be final -- in Ada things are a bit different, Ada's object-system ("tagged types" in Ada parlance) is built upon two items: records and type-derivation. This means that, when teaching OOP, we could arrive gradually introducing first type-derivation (eg Type Degree is new Integer;), then records (ie encapsulation), then private-types (ie information-hiding), and finally unifying everything together with tagged-types... all of which I'll assume you are cognizant of.
In Ada, a constant is just that: some object that can be read but [generally] not written to. (Things can get funny with eg memory-mapped IO.) So we could say:
Package Ex1 is
Type Stub1 is private; -- Some type, with almost nothing public.
C1 : Constant Stub1; -- A constant of that type.
Private
Type Stub1 is tagged record
Data_1 : Integer;
Data_2 : Float;
end;
-- And now we can tell the compiler what C1 _is_.
C1: Constant Stub1 := (Data_1 => 3, Data_2 => 1.2);
End Ex1;
That's how we'd make a constant for a tagged type while keeping its implementation details hidden; though, admittedly, we could have exposed everything and gotten rid of the entire private section.
Now we get to an interesting feature of records [and tagged types] called discriminants -- these are kind of like constants, and kind of like generic-types in other languages. With discriminants we could make a message-type that varies in size according to the message-length:
Package Ex2 is
Type Message(Length : Natural) is private; -- A message.
Function Create( Text : String ) return Message;
Private
Type Message(Length : Natural) is record
Data : String(1..Length) := (Others => ' '); -- Defaults to space-filled string.
end;
Function Create( Text : String ) return Message is
( Data => Text, Length => Text'Length );
End Ex2;
Now, in this case, when you do an assignment like X : Message := Create("Steve"); the variavle's type [unconstrained, in this example, becomes constrained in this case to Message(5) (because "Steve" is 5 characters) and so trying to re-assign with a different-sized message-string wouldn't work. (So, while you couldn't say X:= Create("Why") you can say X:= Create("Hello") because the discriminant [Length] here is 5.) -- So, in that manner discriminants can act like constant fields in some cases.
The limited keyword means that the type doesn't have an assignment [but does have initialization], so you could make the entire type behave as a constant; this is different than having the one component be constant though, certainly not as subtle as the distinction between T and T'Class (T'Class is the Type T and all types derived therefrom, where as T is only that type.)