!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
!summary Extending case statements
!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?)
***************************************************************