You completely misread hat I was saying.
I was saying that if you have Z as your constraints, being (X + Y), it is often helpful to have an intermediate subtype wherein only one is there, especially when they’re proper subsets; consider:
Type Integer is -2**3..2**3-1 with Size => 4; -- -8..7
Type Natural is Integer range 0..Integer'Last; -- 0..7
Type Positive is Natural range Natural'Succ(Natural'First)..Natural'Last;-- 1..7
Type String is Array (Positive range <>) of Character;
Type Label is String
with Dynamic_Predicate => Label'Length in Positive
and (For all C of Label => C in 'A'..'Z'|' '|'0'..'9');
Here we define an Integer as a signed 4-bit number. We then introduce a name for “all non-negative numbers” in this set: Natural. Then we introduce another name, for “all the non-zero elements of Natural” and name this Positive. — Next, we define a String as an array indexed by Positive; and a subtype of String which restricts the characters to (a) UPPERCASE, (b) Space, or (c) digits AND having a length of at least 1.
Now, when we search a string, we can say Function Index( Source : String; Target : Character) return Natural, we don’t have to deal with Index returning a negative number. Likewise, when we get a 0, we know that this value is the only one representing “that Target was not found”.
Wrong.
This is introducing a new discrete type (I would say integer-type, except that would likely reinforce your incorrect mental model), which has values of 0 to 15. It has the proper arithmetic operations. — Remember what I said earlier: use the type-system to describe your problem-space, then use that to solve your problem. — That the compiler may use a hardware 64-bit register to handle operating on variables of this is irrelevant.
I think maybe my examples have been too much on the numeric-side… well, here’s some non-numeric usages, from a compiler I’m working on:
Pragma Ada_2012;
Pragma Assertion_Policy( Check );
With
Ada.Wide_Wide_Characters.Unicode;
-- The Pure_Types package both defines pure-types, such as identifier-strings,
-- which can be proven in SPARK.
Package Byron.Internals.SPARK.Pure_Types
with Pure, Elaborate_Body, SPARK_Mode => On is
--------------------------------------------------------------------------------
-- PACKAGE/SUBPROGRAM RENAMES --
--------------------------------------------------------------------------------
Package WCU renames Ada.Wide_Wide_Characters.Unicode;
Use Type WCU.Category;
Function Get_Category (Input : Wide_Wide_Character) return WCU.Category
renames WCU.Get_Category;
--------------------------------------------------------------------------------
-- INTERNAL STRINGS --
--------------------------------------------------------------------------------
-- The String-type used within the internals of the compiler.
Subtype Internal_String is Wide_Wide_String;
--------------------------------------------------------------------------------
-- IDENTIFIERS --
--------------------------------------------------------------------------------
-- LRM 2.3 (3/2)
-- identifier_start ::= letter_uppercase
-- | letter_lowercase
-- | letter_titlecase
-- | letter_modifier
-- | letter_other
-- | number_letter
Subtype Identifier_Start is WCU.Category
with Static_Predicate => Identifier_Start in
WCU.Lu | WCU.Ll | WCU.Lt | WCU.Lm | WCU.Lo | WCU.Nl;
-- LRM 2.3 (3.1/3)
-- identifier_extend ::= mark_non_spacing
-- | mark_spacing_combining
-- | number_decimal
-- | punctuation_connector
Subtype Identifier_Extend is WCU.Category
with Static_Predicate => Identifier_Extend in
WCU.Mn | WCU.Mc | WCU.Nd | WCU.PC;
-- Intermediate subtype asserting the contents of the string are members of
-- either Identifier_Start OR Identifier_Extend.
Type Valid_ID_Chars is new Internal_String
with Dynamic_Predicate =>
(For All C of Valid_ID_Chars =>
Get_Category(C) in Identifier_Start or
Get_Category(C) in Identifier_Extend),
Predicate_Failure => Raise Constraint_Error with
"Invalid character in Identifier.";
-- LRM 2.3 (2/2)
-- identifier ::= identifier_start {identifier_start | identifier_extend}
Type Identifier is new Valid_ID_Chars
with Dynamic_Predicate =>
(Identifier'Length in Positive)
and then (Identifier'First < Positive'Last)
and then (Get_Category(Identifier(Identifier'First)) in Identifier_Start)
and then
-- LRM 2.3 (4/3)
-- An identifier shall not contain two consecutive characters in category
-- punctuation_connector, or end with a character in that category.
((for all Index in Identifier'First..Identifier'Last-1 =>
(if Get_Category(Identifier(Index)) in WCU.Pc
then Get_Category(Identifier(1+Index)) not in WCU.Pc))
and Get_Category(Identifier(Identifier'Last)) not in WCU.Pc
),
Predicate_Failure => Raise Constraint_Error with "Invalid Identifier.";
Private
End Byron.Internals.SPARK.Pure_Types;
Notice how I use subtypes to restrict the Unicode character-classes to allow only the valid characters? (Valid_ID_Chars) And then I derive from this and add new constraints for Identifier. The reason that I make Identifier a type and not a subtype is precisely because I want it to be incompatible (w/o explicit conversion) with any other String type.
Because in the example given a delta of -1 is perfectly valid. (eg a decrementing loop.)
It’s a simple example to give you the point of what’s going on.
That’s going to give you a lot of errors; not the general idea [per se] but how you’re writing it. What you want is this
Type Whatever is new Base with null record
with Dynamic_Predicate => Whatever.X in Nybble;
or this
Subtype Other_Whatever is Base
with Dynamic_Predicate => Other_Whatever.X in Nybble;
-- Notice, even though Base is a tagged type, this is not making use of inheritence.
Given some of the things you’re saying…
That way lies madness.
(Don’t get over-clever; don’t try to do EVERYTHING in one object/class/type/package.)
No.
A subtype has all the operations of the type. TYPE: A SET OF VALUES, AND A SET OF OPERATIONS ON THOSE VALUES. SUBTYPE: A SET, POSSIBLY THE NULL SET, OF CONSTRAINTS UPON THE TYPE.
Notice that there is nothing done to the operations from the TYPE, a SUBTYPE only constrains the acceptable values.
You’re thinking backwards from what I’m showing.
The example is a nullable value.
Imagine a button which brings a mini-map popup to the front, oh look there’s pop_up is it null? Then open the map and store the handle in pop_up, otherwise bring it to the fore.
Statistics are going to want to have things “batched” into numbers, possibly to the point that having many types (eg Die) may get in the way. [Statistics vs game-mechanics: Think the differences between “accounting” (currency) and “financials” (currency, statistics, trends/projections, rates).] It is, again, a matter of looking at the problem-space and designing for that.
Yes. Kind of.
I suggested a DB-amiable OOP-based DIANA.
Meh, that would be easily addressed: you would have to have a “regeneration process” to de-serialize from the IR into your own system. Making that into a module that e.g. mirrors to github, or generates files inside a webserver is simply using that process in those particular cases.
Because often constraints aren’t often one-off.
They are, sometimes; but that ‘sometimes’ is a bad reason to make the language able to “put the constraint on the variable”.
That’s fair, but you have to give a little, too: Ada was the first ISO-standard OOP language, with Ada 95, and this was before the OOP-terminology had been settled “in-industry”. Also, some of the things that “are the same” really aren’t: access and pointer, for example. (C pretty specifically associates pointers with addresses [and integers]; Ada doesn’t make this mandatory for access, and they may be realized by something else.)
…making smaller variables?
No, constraining values.
Consider:
Type Instruction ( Halt, Add, Sub, Mul, Div, x64, x32, x16, x8 );
Subtype Executable is Instruction Range Add..Div;
Subtype Size_Modifier is Instruction Range Instruction'Succ(Executable'Last)..Instruction'Last;
Type CPU is record --…
Type Instruction_Stream is --…
Procedure Execute( Object : in out CPU; Stream : in out Instruction_Stream ) is
Current_Size : Size_Modifier:= Size_Modifier'Last;
Current_Instruction : Instruction;
Begin
RUN:
loop
Current_Instruction := Instruction'Read( Stream );
case Current_Instruction is
when Halt => Exit RUN;
when Size_Modifier => Current_Size:= Current_Instruction;
when Executable =>
case Executable'(Current_Instruction) is
When Add => --…
When Sub => --…
When Mul => --…
When Div => --…
end case;
end case;
end loop RUN;
End Execute;
See?
It’s “thinking in sets”.
Because of the subtypes, we could make the case statements both compact and readable. They also allowed us to use qualification (Subtype'(Value)) on the second case so that we only needed to handle the Executable instructions.
No, it doesn’t.
Really. Inheritance can only expand on the values.
(Ok, so you could have a inherited type that overrides operations.)
Stop.
You’re already wrong trying to apply this.
This sort of inheritance you’re talking about has nothing to do, at all, with subtypes.
This is why OOP (“tagged type”) is talked about differently in the ARM: it is different.
…have you been listening to people on this thread? They’ve provided you many examples, many reasons.
Take a step back.
Remember to your math courses.
Remember your proofs class.
Do you remember how definitions and restrictions were used to prove things?
I: Integers:
-INF...-100......................0......................100...INF
C: Integers, [-100, 100]:
-100......................0......................100
N: Natural numbers < 101:
0......................100
P: Positive numbers < 101:
1.....................100
F( X ) -> P; X in N.
Because I was showing you how to iteratively move from a C API to something in idiomatic Ada.
It’s not like we can change the implementation of the published API, can we?
if the C-import is int API_BLARG( int y ) but API demands the return is 1 and 0 (fail, succeed) and the inputs are 2..8, then why wouldn’t I say
Subtype Inputs is Interfaces.C.int range 2..8;
Procedure Blarg( Y : Inputs ) is
Function APIBLARG( Y : Interfaces.C.int ) return Interfaces.C.int
with Import, Convention => C;
Begin
-- NOTE, I cannot now pass in invalid values!
Case APIBLARG(Inputs) is
When 0 => Do_Success_Thing;
When 1 => Raise Some_Speciffic_Error with "FAILURE!";
When others => Raise Program_Error with "RESULT UNDEFINED BY API!";
End case;
End Blarg;
Why not?
And, honestly it was a mistype.
But, anyway, I’m curious if you’re even seeing what people are saying: by using subtypes you are constraining values on a type, and then you can operate on the properties derived therefrom.
For example: Function Division(Numerator : Integer; Denominator : Positive) return Real;
Oh, look, it’s now impossible to pass in zero to the denominator! By construction, I avoid DIVIDE-BY-ZERO!
Line : String (1..80); -- String is constrained to 1..80
But normally programs are written on a higher abstraction level, e.g. in terms of [sub-]types rather than individual values, or higher, in terms of sets of types rather than individual types.
C++ class is just a type you can derive from [by extension] = Ada tagged type.
You derive from a type and so get a new type. This new type is a member of the class rooted in the type you derived from. But what was the question?
Throwing an exception is a part of implementation of the long → int conversion. The point is not exception but substitution of long where int is expected. That is subtyping. Instead of exception it could wrap the value for example. C++ model of user-defined conversions was more powerful than Ada subtypes because it allowed different representations of types and value sets were mapped. Ada subtype has same representation and values sets are subsets.
No, the goal of Ada to make conversions a user-defined implementation detail. It is all about Liskov substitutability. If A is substitutable for B then for the program logic A is B. Implicit conversions as known in PL/1 and C were bad because they violated substitutability. In Ada it is up to the programmer.
You could take IP6 and constrain it to IP4.
BTW, “X can be threated as Y” is a definition of subtyping and of a class Y = all X such that X can be threated as Y. In a good language you can express that in language terms by declaring types.
So library writing rather than directly writing to the case? Kind of like when someone does:
int a;
a = 1; //Turn on
a <<= 4; //It's actually in bit 5 (offset from lowest order by 4)
(unsigned char)*0x0152 = a; //The GPIO port is memory mapped at 0x0152
instead of simply (unsigned char)*0x0152 = 0x10 because they believe the method of how you got to your conclusion needs to be defined instead of simply the conclusion?
I meant in the context above. My understanding is that when i do type X is integer with range 1 .. 6; i’m making a derived type, inheriting from integer, but if i do type X is range 1 .. 6; i’m making a new die primative that inherits from whatever Integer inherits from (some nameless type that’s totally different from what wide_wide_character inherits from). Because apparently, if i don’t derive a type, i’m making a new “top-level type” that’s just as primitive as “Integer” and things like +, -, *, /, etc are already done for me and work consistently with Integer, but with my limitations. Because the first question i had (before coming here) was “what is the practical difference between type and subtype?” and my response (from chatgpt) introduced “derived type” into the mix, and that all these were completely separate of records (equivalent to classes and structs). So the fact that somehow all 3 (type, subtype, and derived type) basically came with all the same goodies (with subtype being the exception in that i can upconvert to the base type without a caste) all look exactly like class inheritance, but for simple types instead of complex records/classes (because i still get everything built in but with different limitations on possible values defined by the constraint i choose), especially because it’s clear that i’m getting these goodies from a nebulous ideal type with no name.
So the struggle is, since ultimately subtype bypasses type checks for up-conversion (which is supposedly a big sin otherwise) why would anyone do this? Moreoever, outside of that, for something like Integer, the end result of making a variable with all 3 using the same constraint is functionally the same, so why use one over the other? Why derive an integer with a constraint if i get all the exact same things without deriving from an integer?
Record, yes, that actually makes sense because subtype and derived type are your only options that point if you want goodies brought in for you. Then again, even for those, why subtype over derived type unless we intend to allow implicit up-casting (by pretending that’s not what we’re doing by saying subtype isn’t a new type)?
That one wasn’t so much a question so much as a conclusion. If i type num is range Integer'First .. Integer'Max just re-invented Integer, in every way. It’s like Integer and num both derive from the same type and then do nothing with it, but the type is not named, but simply implied (so a sort of signed int type that i can’t directly use without declaring a type and using that instead, but Ada was nice enough to declare Integer for me), and i know i’m doing this because i get all the goodies without deriving Integer.
This isn’t a hugely important thing, but to actually understand what’s going on under the hood. Because if i somehow found a way to change Integer (maybe add a math operation for character, for example), suddenly that difference becomes important, because my new type wouldn’t inherit those changes.
Oh, so implicit conversion isn’t a problem, but rather we need to clearly define a way that it isn’t a problem. By using subtype this is done automatically because were only restricting to a subset. So what i was reading erred in the statement, because the common implementation of implicit conversion risks loss of accuracy without warning, rather than implicit conversion itself.
I’m thinking of another angle. Normally an ipv4 address (even ipv6 but more bytes) is addressed as 4 separate 1-byte values. If, for example, I had a game where i wanted automatic names asigned to players with no risk of collision (for a cheap example that’s no longer relevant today), it’d be more practical to treat that IPv4 as a 4-byte Integer, then have a lookup table of “names” then mod and divide by the number of entries in the table, and instead of someone trying to remember 49.82.242.55 (random, i don’t know whose that is), it could instead be Darth Cookie Cutter Pillow Tube Shooter Champion (assuming i have a dictionary of about 100-200 words) and then someone could easily recognize who they’re playing against if they’ve played each other before (because names are easier to remember than numbers, even if much longer) and have a static ip.
Yes, this is an overly complex and niche example of why one might try to take something and use it in terms of a completely different representation. But to do this, i would want to completely change the front end while maintaining the backend, because guaranteed that is going to be an array of characters under normal representation, but i’d be wanting to change it to a single number (or bignum with ipv6).
I cannot comment on this. In Ada GPIO port is typically a record type with a representation clause.
The difference is subtyping. Type has no relation to other types. Subtype is in the subtype/supertype relation with the parent type.
You are thinking in terms structural equivalence. Ada type system has nominal equivalence. You did not derive from anything. You created a completely new type. The fact that some implementations can be reused by the compiler is an implementation detail.
As an update to the topic I happen to see that the Barnes book is currently 33% off for both physical and Kindle editions. I have a little bit of extra money this past paycheck leftover so I bought a copy and we’ll see if what said about it is true or not.
I’m glad to see someone mention Ada as a Second Language by Cohen. It covers Ada 95 and is not only the best book on Ada that I know of but the best book on programming that I have seen for any language; however, as the title implies, it assumes prior programming experience so you won’t be bothered learning what a loop is or with building project programs. I’m pretty sure it covers the entire Ada Reference Manual for Ada 95. I have found the book by Barnes to be OK but the coverage is spotty and not comprehensive and topics are scattered, but it is good for help on post-95 things.
Barnes’ rationale covered the new Ada 95 stuff. Back then you would first read Gehani and then Barnes (tagged and protected types very nicely explained).
Gehani was a great Ada 83 introduction book. It had an Ada 83 RM annex which was quite readable back then. After reading the book you could easily navigate the RM.