"Subtype Predicates, or Why Was I Not Imformed?", a Drama in Two Acts

The abridged TL;DR version is at the bottom.

Act I : Vjalmr Has Issues with Types. Surprise!

For learning Ada I have set out to complete all the Advent of Code challenges, starting from 2015. I skipped day 4, because I am still not comfortable with bit fiddling for an MD5 implementation, so I ended up on day 5.

Here I need to compare some characters, so I thought “Hey! I will just make a Vowel type, and compare the characters that way!”, forgetting all about what I had read here: Wikibook - Characters as Enumeration Literals

This literal ‘A’ has nothing in common with the literal ‘A’ of the predefined type Character (or Wide_Character).

After a lot of digging, which seems to be the norm with this language, I found a blog by one Jim Rogers, who introduced me to subtypes with Static_Predicate. Just to make sure Mr. Rogers wasn’t pulling my leg, I looked it up in the reference manual, and it was indeed there 3.2.4 Subtype Predicates.

Now, I could create a subtype of Character with only vowels, and compare with my string.

Interlude

Is this the right way of doing it? I know it works, but I am not sure.

Could I cast the character to my type? But if I do that, then all the characters that are not of that type would cause an exception of sorts, if I understand correctly.

Please send your insights, as currently my code looks like the following:

--  Subtype and variable definition, with initialization
subtype Vowels is Character with
     Static_Predicate => Vowels in 'a'|'e'|'i'|'o'|'u';

Test_String : String := "I have some vowels in me.";
Vowel_Count : Natural := 0;
--  How many are there?
for C of Test_String loop
   if C in Vowels then
      Vowel_Count := Vowel_Count + 1;
   end if;
end loop;

Act 2: Vjalmr Would Like to Contribute

As enlightening the journey was, finding this information was not as easy as I had hoped. They are actually on the Learn Ada website, but under “Design by contracts”, and also I found it under “Ada for C++ or Java Developer”.

I had not gotten to contracts yet, and I am neither a C++ or Java developer, so I didn’t know to look there. I wanted a type.

Now, the prerequisite for this question is: Am I going about doing it the right way?

If I am, then I would like to add this tidbit to either the WikiBook, or to the ada-lang.io tutorial, which is where I actually got started in the first place - even though there are only two entries.

How would I go about doing this?

Fin

TL;DR

Should I be using Subtype_Predicate or a unique type for character comparison if I want to see if characters of a string are vowels or not?

If I should use this, could I somehow contribute to the tutorials, or the WikiBook so that this information is easier to find, so the next person who wants to do this doesn’t spend an hour figuring it out?

I don’t know the answer to your question. I’m not sure Static_Predicate counts as a contract in this circumstance. Have you thought about using a Character_Mapping?

1 Like

You are probably right and that’s why it is confusing me.

I have not, but I will also give that a try - just to see how it differs. Thank you.

This also occurs to me: I haven’t looked at the problem yet, but you could define an array over a range of Character. That’s probably the simplest way, and I’m pretty sure it’s both idiomatic and efficient.

with Ada.Text_IO;

procedure Test_Vowel is

   package IO renames Ada.Text_IO;

   subtype Alphabet is Character range 'a' .. 'z';
   Is_Vowel : array (Alphabet) of Boolean
      := ( 'a' | 'e' | 'i' | 'o' | 'u' => True, others => False );

begin

   IO.Put_Line (Is_Vowel ('e')'Image);   --  TRUE
   IO.Put_Line (Is_Vowel ('f')'Image);   --  FALSE

end Test_Vowel;

What you did makes sense based on examples that I’ve seen

Where they helpful and did you want more? I stopped all a sudden because of a bunch of personal things.

So I did this, and it looks like you would expect as the following:

with Ada.Strings.Maps; use Ada.Strings.Maps;
...
Vowels : constant Character_Set := To_Set ("aeiou");

The only interesting bit is, I cannot do an in loop on them, I have to use Is_In():

if Is_In(Current_Character, Vowels) then
   Vowel_Count := Vowel_Count + 1;
end if;

But it works just fine. One of the most eye opening aspects of this exercise is that there are more than one way of doing things with Ada. For some reason I had expected it to be “The Ada way, or the Highway” due to the stringent nature of the language, but that seems not to be the case.

They were helpful indeed, and of course more would be appreciated - not just by me, but by others as well. Some things were not too clear, such as Subprograms actually being the building blocks of the program i.e. procedures and functions.

This was confusing coming with some Pascal knowledge - making me think a “subprogram” was like a unit.

I would love to help out once I actually know what I am doing - but also just for editing, structuring, and commenting, if this would be of any help to the community.

I’m not sure from your post if what you did worked for you or not.

I created this from your examples:

with Ada.Text_IO;
procedure Vjalmr is
   subtype Vowels is Character with
      Static_Predicate => Vowels in 'a' | 'e' | 'i' | 'o' | 'u';

   Test_String : String := "I have some vowels in me.";
   Vowel_Count : Natural := 0;
begin
   for C of Test_String loop
      if C in Vowels then
         Vowel_Count := Vowel_Count + 1;
      end if;
   end loop;
   Ada.Text_IO.Put_Line (Item => Vowel_Count'Image);
end Vjalmr;

and tried it

$ gnatmake -gnata vjalmr.adb 
x86_64-linux-gnu-gcc-12 -c -gnata vjalmr.adb
x86_64-linux-gnu-gnatbind-12 -x vjalmr.ali
x86_64-linux-gnu-gnatlink-12 vjalmr.ali
$ ./vjalmr 
 8

which is the right answer (your definition of Vowels does not include 'I').

With GNAT, assertion checking, which includes pre- and post-conditions and predicates, is not turned on by default, which is why I always use -gnata.

What you did worked, but as pointed out, there are other ways to achieve the same thing. At the basic level, what you want is a set of Character and a membership test against that set. You can do that directly in some languages, but in Ada it takes a generic instantiation for the general case. There are usually multiple ways to do things in Ada. The mapping from Character to Boolean presented by cantanima is a common way to implement a set when the universe is discrete, and generics such as PragmARC.Data_Structures.Sets.Discrete create them for you. Character_Set is a custom implementation of a set for Character.

The only way that lets you use [not] in is a subtype, but since Ada 12 one can use a disjoint set for a membership subtype as in your predicate, so you could just do

if C in 'a' | 'e' | 'i' | 'o' | 'u' then
1 Like

Yes, what I did worked, with several different approaches to solve the same problem.

Not part of the spec, correct.

Which is what I have come to realize. Thank you for your in depth answer.

These interesting bits of information, they are there I am sure, but it takes a lot of digging, trial and error to figure them out.

I have downloaded, and gone through a lot of but not all of course, of the PDFs available on learn.adacore.com and they do help. Generally the information I need for what I am doing is further along than where I have read to. I think sometimes my issue is that I attempt to do things the “non-Ada way” - due to using other languages.

The only interesting bit is, I cannot do an in loop on them, I have to use Is_In():

Maybe I misunderstand you, but with Ada 2022 you can do this (building on the example I have above):

pragma Ada_2022;

--  much of the above example program is the same, but the end:
begin

   for V in Alphabet when Is_Vowel (V) loop
      IO.Put (V);
      IO.Put (' ');
      IO.Put_Line (Is_Vowel (V)'Image);
   end loop;

end Test_Vowel;

This is very clean, I like it.

I am however using 2012 at the moment, based on advice I read somewhere that compilers don’t fully support 2022 yet?

This works in GCC back to version 11 (you have to say pragma 2020 for that older compiler!)

Other 2022 features will have made their appearance in successive releases (with the occasional issue on the way).

1 Like

Thank you, Simon!

The more you know.

Hi,
I tend to prefer relying on exception handling for incorrect entries (when the user knows what is expected), rather than conditionals and explicit test membership, so I really like predicates. But it doesn’t seem to do what I expect.

I read that subtype predicates are the way to go for non-private types, but neither dynamic_predicate nor static_predicate do anything in my case. What I want is for the value to be checked everytime a variable of a given subtype is assigned, and raise an exception when it’s not within a discrete bound.

pragma Ada_2022;
pragma Extensions_Allowed (On);
with Ada.Text_IO, Ada.Integer_Text_IO;
use Ada.Text_IO, Ada.Integer_Text_IO;

procedure Calculator is
   New_expression: Boolean;
   procedure Get_number(Result: out Integer) with Inline is
   begin
      loop
         begin
            Get (Result);
            exit;
         exception
            when others =>
               Put_line("Error, type again the number.");
               Skip_Line;
         end;
      end loop;
   end Get_Number;
   procedure Produce_Expression (Result: out Integer) is
      Operand : Integer := 0;
      subtype Operators is Character
        with Dynamic_Predicate => Operators in '*'|'/'|'+'|'-'|'.';
      Operator : Operators := '+';
   begin
      Get_number(Result);
      outer_loop: loop
         loop
            begin
               Put_line("Gving operator");
               Get(Operator);
               Put_line("operator given");
               exit;
            exception when others =>
                  Put_Line("Character not included in the following list: +,-,*,/. Type again the operator, and without space too.");
                  skip_line;
            end;
         end loop;
         exit outer_loop when Operator = '.';
         Get_number(Operand);
         Result := (if Operator = '+' then Result + Operand
                    elsif Operator = '-' then Result - Operand
                    elsif Operator = '*' then Result * Operand
                    elsif Operator = '/' then Result / Operand
                    else Result);
      end loop outer_loop;
   end Produce_Expression;
   Expression: Integer;
   Yes_Or_no: Character;
   Result: Integer;
begin
   loop
      Put_line ("Enter an expression, finish with a dot to end the expression.");
      Produce_Expression (Result);
      Put_Line("Resultat of the expression: " & Result'Image);
      Put_line("Do we calculate another expression ? Y for y for yes, any other character for no.");
      Get(Yes_Or_No);
      exit when Yes_Or_No not in 'Y'|'y';
   end loop;
   Put_Line("You chose to end the calculations.");
end Calculator;

After “giving operator” I enter “o” or any letter outside the constraints given, and it is accepted without complaints. Am I misunderstanding something ?

Thanks.

The compiler won’t add check for predicates (and other assertions) unless you enable assertions, either with a pragma:

   pragma Assertion_Policy(Check);

or a compiler option. For GNAT, the compiler option is

-gnata Assertions enabled. Pragma Assert/Debug to be activated

2 Likes

Ah, I thought so. Thank you.
What difference is there between static_predicate and dynamic_predicate ? Both add a dynaimc check here…

Primarily, the contexts in which they’re valid differ.

Static_Predicates are eligible to be done at compile time (given compile time eligible inputs). Dynamic_Predicates are run time only.

1 Like

Then how come I changed it to Static and it stil produces a dynamic check, as it has to since it can’t know the value inputted by the user ?

Static eligible does not mean Static guaranteed. It has a lot to do with how you code it and your compiler flags. With Static_Predicate, if the compiler can reason it out at compile time it will (with the correct compiler flags).

Side note: Remember we are talking language specifications in the general sense here. All compilers are allowed to “do better” if they can. So if GNAT knows a quick way to make something compile time it will, regardless of the predicate. If it can’t reason it out, it won’t.