!standard 5.4(0) 16-06-06 AC95-00278/00

!class Amendment 16-06-06

!status received no action 16-06-06

!status received 16-05-09

!subject Extending case statements

!class Amendment 16-06-06

!status received no action 16-06-06

!status received 16-05-09

!subject Extending case statements

!summary

!appendix

!topic Extended case statements. !reference Ada 2012 RM 5.4 Case Statements !from Pascal Pignard 16-05-01 !keywords case aggregate !discussion As existing Ada "case" prevents from multiple "if", the extension of the selecting_expression from discrete values ("The selecting_expression is expected to be of any discrete type") to aggregates of discrete values could prevent from multiple cascade of "if". Lets take a trivial example of complex number representation: One classical implementation: type Representation is (Cartesian, Polar); type Complexe (R : Representation) is record case R is when Cartesian => X, Y : Float; when Polar => Rho, Teta : Float; end case; end record; Implementation of Add function: function Add1 (Z1, Z2 : Complexe) return Complexe is begin if Z1.R = Cartesian then if Z2.R = Cartesian then return (Cartesian, Z1.X + Z2.X, Z1.Y + Z2.Y); else return (Cartesian, Z1.X + Real (Z2), Z1.Y + Imag (Z2)); end if; else if Z2.R = Cartesian then return (Cartesian, Real (Z1) + Z2.X, Imag (Z1) + Z2.Y); else return (Cartesian, Real (Z1) + Real (Z2), Imag (Z1) + Imag (Z2)); end if; end if; end Add1; Implementation with extended cases: function Add2 (Z1, Z2 : Complexe) return Complexe is type Couple is array (1..2) of Representation; begin case Couple'(Z1.R, Z2.R) is when (Cartesian, Cartesian) => return (Cartesian, Z1.X + Z2.X, Z1.Y + Z2.Y); when (Cartesian, Polar) => return (Cartesian, Z1.X + Real (Z2), Z1.Y + Imag (Z2)); when (Polar, Cartesian) => return (Cartesian, Real (Z1) + Z2.X, Imag (Z1) + Z2.Y); when (Polar, Polar) => return (Cartesian, Real (Z1) + Real (Z2), Imag (Z1) + Imag (Z2)); end case; end Add2; Avantages: - much better readability To be honest I haven't deeply verify Add1 as you have to memorize "else" value of each "if" to check. To verify Add2 is much straight forward. - allow better formal proof And more with records, lets play to French card game "Le Barbu" : type Suit is (Spade, Club, Heart, Diamond); type Value is (Seven, Eight, Nine, Ten, Jack, Queen, King, As); type Card is record S : Suit; V : Value; end record; type Deck is array (1 .. 32) of Card; procedure Play (C : Card) is begin case C is when (Heart, King) => Process_Le_Barbu; <...> end Play; That allows case of String: type TS4 is String(1..4); procedure (V : TS4) is begin case V is when "PAPA" | "MUM " => Process_Parent; ... And more you may have extended ranges: when (Club, Seven) .. (Club, As) => Process_Clubs; Lets imagine "TOTO" .. "TUTU" ;-) *************************************************************** From: Jean-Pierre Rosen Sent: Monday, May 9, 2016 3:27 PM > As existing Ada "case" prevents from multiple "if", the extension of > the selecting_expression from discrete values ("The > selecting_expression is expected to be of any discrete type") to > aggregates of discrete values could prevent from multiple cascade of > "if". If I remember correctly, the CHILL language had that feature (back around '79), and I thought it was one of the few cases where it was better than Ada. CHILL was mainly used for programming automata (especialy PABX) where this need arises quite often. *************************************************************** From: Pascal Pignard Sent: Thursday, May 12, 2016 2:17 PM My son did recently something like that with SWIFT: // Swift code. public enum Number { case Integer(Int); case Floatant(Float); // MARK: Public Methods public func add(n: Number) -> Number { switch (self, n) { case (let.Integer(i1), let.Integer(i2)): return Integer(i1 + i2); case (let.Integer(i1), let.Floatant(f2)): return Floatant(Float(i1) + f2); case (let.Floatant(f1), let.Integer(i2)): return Floatant(f1 + Float(i2)); case (let.Floatant(f1), let.Floatant(f2)): return Floatant(f1 + f2); } } SWIFT has large similitude with OCaml: (* OCaml code *) (* type nombre *) type nombre = Entier of int | Reel of float (*@ ensures : additionne deux nombres @ s'il y a deux Entier en paramètre => retourne un Entier @ sinon retourne le résultat via un Reel @*) let add_nb = fun nb1 -> fun nb2 -> match (nb1, nb2) with |(Entier n1, Entier n2) -> Entier (n1 + n2) |(Entier n1, Reel n2) -> Reel (float_of_int n1 +. n2) |(Reel n1, Entier n2) -> Reel (n1 +. float_of_int n2) |(Reel n1, Reel n2) -> Reel (n1 +. n2) *************************************************************** From: Randy Brukardt Sent: Thursday, May 12, 2016 6:36 PM > > As existing Ada "case" prevents from multiple "if", the extension of > the selecting_expression from discrete values ("The > selecting_expression is expected to be of any discrete > type") to aggregates of discrete values could prevent from multiple > cascade of "if". ... > Implementation with extended cases: > function Add2 (Z1, Z2 : Complexe) return Complexe is > type Couple is array (1..2) of Representation; > begin > case Couple'(Z1.R, Z2.R) is > when (Cartesian, Cartesian) => return (Cartesian, Z1.X + Z2.X, Z1.Y + Z2.Y); > when (Cartesian, Polar) => return (Cartesian, Z1.X + Real (Z2), Z1.Y + Imag (Z2)); > when (Polar, Cartesian) => return (Cartesian, Real (Z1) + > Z2.X, Imag (Z1) + Z2.Y); > when (Polar, Polar) => return (Cartesian, Real (Z1) + Real (Z2), Imag (Z1) + Imag (Z2)); > end case; > end Add2; I don't understand what you are proposing for the rules here. What is allowed as the case labels? What exactly is allowed as the type of the selecting expression? All of your examples are of composite types containing only discrete components, is that intended to be a requirement or are you planning to allow anything? Do the case labels allow lists and subtypes (as currently), or just aggregates with all static components (as in all of your examples)? If the latter, then this really only works with toy examples with small numbers of values. (If one of the components is of type Integer, enumerating the values isn't going to work.) [Note: I'm talking about inside of the case labels, where one would want to use named subsets of enumerations: subtype Red_Suits is Suits with Static_Predicate => Red_Suits in Hearts | Diamonds; when (Red_Suits, Ace) => ... If one can't used named grouping of enumerations, we've lost most of the benefit of Ada 2012 having (finally) eliminated the tyranny of ordering for enumerations.] Otherwise, how do you plan to deal with the syntax ambiguities that would arise from putting lists into aggregates which already have lists of their own? How would the case completeness check work? (The real advantage of a case statement over if statements is the completeness check, as it prevents forgetting anything.) ... > And more you may have extended ranges: > when (Club, Seven) .. (Club, As) => Process_Clubs; What does this mean if any component other than the last is different? Any answer is likely to be wrong for some cases. > Lets imagine "TOTO" .. "TUTU" ;-) Imagining that is a enough to kill the proposal forever in my mind. ;-) In this case, there is a language defined order, but it almost certainly is not what anyone would want. Is "TUAA" included in the range or not? It should be (using language-defined "<"), but that would not make sense for other composite types (which don't have a "<" operation). If I was using one of these case statements to implement Sheephead (a card game that was and is very popular in my hometown, German in origin [the German name being unpronounceable thus having turned into "Sheephead"], which uses the same 32-cards you mentioned): type Suit is (Club, Spade, Heart, Diamond); type Value is (Seven, Eight, Nine, King, Ten, Ace, Jack, Queen); subtype Non_Trump_Suit is Suit with Static_Predicate => Non_Trump_Suit in Clubs | Hearts | Diamonds; case C is when (Club, Queen) .. (Diamond, Queen) => Process_High_Trumps (C); when (Club, Jack) .. (Diamond, Jack) => Process_Middle_Trumps (C); when (Diamond, Seven) .. (Diamond, Ace) => Process_Low_Trumps (C); when (Non_Trump_Suit, Seven) .. (Non_Trump_Suit, Ace) => Process_Normal (C); end case; The problem here is that the ranges need to work in different ways in order to make sense. If the first range was to include values other than Queens it would be very annoying (as an error would happen and one would be forced to write out all of the individual values). If the third range included suits other than Diamonds, the same problem would happen. It seems to me that this is going to be more confusing than helpful (whatever rule is adopted would cause trouble in some reasonable circumstances). It looks like this is a feature that only works when there are small numbers of choices involved; hard to say whether that is valuable enough to pursue. *************************************************************** From: Gautier de Montmollin Sent: Friday, May 13, 2016 2:08 AM My little grain of salt: the cascade of "if" is not the only alternative to your proposal: you can have a cascade of "case", which is more readable (plus the goodies of the case statement). Not trying to lessen the interest of your proposal, which is real (even though the rules around it might be complex ;-) ) Below, something that you can compile. --- with Ada.Numerics.Elementary_Functions; use Ada.Numerics.Elementary_Functions; procedure Multi_Case is type Representation is (Cartesian, Polar); type Complexe (R : Representation) is record case R is when Cartesian => X, Y : Float; when Polar => Rho, Teta : Float; end case; end record; function Real(Z: Complexe) return Float is begin case Z.R is when Cartesian => return Z.X; when Polar => return Z.Rho * Cos(Z.Teta); end case; end Real; function Imag(Z: Complexe) return Float is begin case Z.R is when Cartesian => return Z.Y; when Polar => return Z.Rho * Sin(Z.Teta); end case; end Imag; function Add1b (Z1, Z2 : Complexe) return Complexe is begin case Z1.R is when Cartesian => case Z2.R is when Cartesian => return (Cartesian, Z1.X + Z2.X, Z1.Y + Z2.Y); when Polar => return (Cartesian, Z1.X + Real (Z2), Z1.Y + Imag (Z2)); end case; when Polar => case Z2.R is when Cartesian => return (Cartesian, Real (Z1) + Z2.X, Imag (Z1) + Z2.Y); when Polar => return (Cartesian, Real (Z1) + Real (Z2), Imag (Z1) + Imag (Z2)); end case; end case; end Add1b; begin null; end; *************************************************************** From: Jean-Pierre Rosen Sent: Friday, May 13, 2016 2:32 AM > My little grain of salt: the cascade of "if" is not the only > alternative to your proposal: you can have a cascade of "case", which > is more readable (plus the goodies of the case statement). Yes, and the cascading case can be a good implementation/equivalence model for the 2-dimensional case statement. *************************************************************** From: Christoph Grein Sent: Friday, May 13, 2016 10:35 AM > If I was using one of these case statements to implement Sheephead (a card > game that was and is very popular in my hometown, German in origin [the > German name being unpronounceable thus having turned into "Sheephead"], > which uses the same 32-cards you mentioned): Just for the record: I guess this is Schafkopf, the literal translation of which is sheep head, a very popular Bavarian game. I don't know the game myself. The name's origin is questionable. Some say it derives from the way the score is written down, something like \_/ |_| which resembles a horned head. More likely, the name derives from the old fashioned word Schaff for a keg, on top of which (the Kopf or head) the game was played. It really isn't unpronounceable: Schaf is like sh followed by the the sound of half (British pronunciation) without the h. Kopf is like cop followed by f. Schaff is the same sound with a short a, where Schaf has a long a. Hear I tongue bending? *************************************************************** From: Peter C. Chapin Sent: Friday, May 13, 2016 6:20 AM The proposal resembles a feature common in functional languages called pattern matching. In such languages pattern matching is powerful and widely used. However, in its full generality it also comes with a complex specification and significant implementation cost (in terms of complexity at least). It may or may not make sense for Ada to grow such a feature, and if it does it may be appropriate for Ada's version to be relatively limited (just as the other functional features of Ada are limited). However, it seems to me that any pattern matching-like features in Ada should at least be conceptually consistent with what the broader community of pattern matching users finds natural. *************************************************************** From: Riccardo Bernardini Sent: Saturday, May 14, 2016 4:21 PM I like this idea. I had this type of problem few times and I never found a satisfying solution. I tried the nested if, the nested case and also computing a "composite index" from the two base index (something like Representation'Pos(A)*2 + Representataion'Pos(B)) and then "casing" on the composite index. Let me try with an almost formal definition... Let us call the new type a multi-discrete type. A multi-discrete type is * A discrete type OR * An array of discrete (multi-discrete) types OR * A (non-tagged?) record whose components are all discrete (multi-discrete) If in the definition above, in the last two cases, we use multi-discrete instead of simply discrete, we could have things quite complex like record whose components are arrays of discrete type. I do not think we need this kind of generality and maybe we can stuck with the non-recursive definition. The idea is that a multi-discrete type represents the Cartesian product of the types. For example, if type Representation is (Polar, Cartesian); type Day is (Mo, Tu, We, Th, Fr, Sa, Su); type Couple is array(1..2) of Representation; type Foo is record R : Representation; D : Day; end record; both Couple and Foo would be multi-discrete types and they would represent the sets Representation x Representations and Representation x Day ('x' is the Cartesian product, of course). Since a Cartesian product of discrete sets is still discrete, we can consider the new type as an extension of the idea of discrete types. The parallel with Cartesian products suggests a definition for multi-range. A multi-range represents a subset of the values of the multi-discrete type and it has the same syntax of an aggregate (a record_aggregate or an array_aggregate) with the difference that an expression can be also a range (even a multi-range, if we use the recursive definition) or a subtype. The multi-range will represent the Cartesian product of the components. For example, if type Days is array(1..3) of Day; subtype T_Days is Day with Static_Predicate => Tu | Th: a valid multi-range would be (Mo, T_Days, Fr..Sa) that would represent the set (Mo, Tu, Fr) (Mo, Tu, Sa) (Mo, Th, Fr) (Mo, Th, Sa) If one of the ranges in the aggregate is empty (e.g., (Mo, Tu..Mo, Fr)) the value of the multi-range is the empty set. I did not check with a fine comb the syntax, but I do not think that this extension would introduce ambiguities. Roughly speaking, currently an expression in an aggregate can be followed only by a comma or by a close parenthesis, so if a ".." follows, then it must be a range. A subtype indication would look like an "single identifier expression" on a purely syntactic level, but the ambiguity can be solved by checking if the identifier marks a subtype or not. An expression made of two aggregates separated by a ".." like (Mo, Tu, Fr) .. (Tu, Th, Su) could have the following interpretations * It is illegal * It is an alternative for (Mo..Tu, Tu..Th, Fr..Su) * It denotes the values between (Mo, Tu, Fr) and (Tu, Th, Su) (extremes included). This, however, requires to define an ordering inside the multi-discrete type. If we use the second choice and allow the use of multi-discrete type inside for loop, we could do a "vector for," that is, something like type Many_Days is array(Positive range <>) of Day; procedure do_something(Start, Stop : Many_Days) with Pre => Start'length = Stop'length; procedure do_something(Start, Stop : Many_Days) is begin for Index in Start .. Stop loop -- Here Index is an array of Day end loop; end do_something; The loop inside the procedure would be like a set of Start'Length nested for loops. Note that the number of nested loops is not fixed at compile time. Although this construction can seem strange, I needed it few times. Of course, in order to do this we need to fix an ordering for a multi-discrete type. *************************************************************** From: Georg Bauhaus Sent: Tuesday, May 24, 2016 2:39 AM > The proposal resembles a feature common in functional languages called pattern > matching. Also, insofar as FP's argument pattern matching is contrasted with its procedural counterpart, OO types, makes me think of something in Ada's type system. Conceptually, type Complex_Polar is NOT TAGGED NEW Complex_Interface ...; type Complex_Cartesian is NOT TAGGED NEW Complex_Interface ...; The idea being that the compiler infers and requires the absence of a need for a run-time tag, and checks the presence of the operations specified in Complex_Interface. No idea how big a bottle this would open. (Any cascade of conditionals without it being static type inference is implementing OO by hand, not in the type system?) ***************************************************************

Questions? Ask the ACAA Technical Agent