www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - semi-final switch?

reply Steven Schveighoffer <schveiguy gmail.com> writes:
A final switch on an enum complains if you don't handle all the enum's 
cases. I like this feature.

However, sometimes the data I'm switching on is coming from elsewhere 
(i.e. a user), and while I want to enforce that the data is valid (it's 
one of the enum values), I don't want to crash the program if the 
incoming value is not correct. But final switch doesn't let me declare a 
default case (to throw an exception instead).

If I use a non-final switch, then my code might forget to handle one of 
the cases.

Oh, and to throw a monkey wrench in here, the value is a string, not an 
integer. So I can't use std.conv.to to verify the enum is valid (plus, 
then I'm running a switch twice).

Any ideas on better ways to handle this?

-Steve
Jun 17
next sibling parent reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Thu, Jun 17, 2021 at 05:41:28PM -0400, Steven Schveighoffer via
Digitalmars-d-learn wrote:
[.[..]
 Oh, and to throw a monkey wrench in here, the value is a string, not
 an integer. So I can't use std.conv.to to verify the enum is valid
 (plus, then I'm running a switch twice).
 
 Any ideas on better ways to handle this?
[...] Why not just: try { MyEnum value = input.to!MyEnum; } catch (Exception e) { stderr.writeln("Invalid input"); } ? T -- People demand freedom of speech to make up for the freedom of thought which they avoid. -- Soren Aabye Kierkegaard (1813-1855)
Jun 17
parent Steven Schveighoffer <schveiguy gmail.com> writes:
On 6/17/21 5:54 PM, H. S. Teoh wrote:
 On Thu, Jun 17, 2021 at 05:41:28PM -0400, Steven Schveighoffer via
Digitalmars-d-learn wrote:
 [.[..]
 Oh, and to throw a monkey wrench in here, the value is a string, not
 an integer. So I can't use std.conv.to to verify the enum is valid
 (plus, then I'm running a switch twice).

 Any ideas on better ways to handle this?
[...] Why not just: try { MyEnum value = input.to!MyEnum; } catch (Exception e) { stderr.writeln("Invalid input"); }
Yeah, that was a possibility I listed. Of course `to` is going to do a switch, which means then after I validate the value is valid, I then have to do a final switch to process it. I'd prefer to do it both in one switch. -Steve
Jun 18
prev sibling next sibling parent Dennis <dkorpel gmail.com> writes:
On Thursday, 17 June 2021 at 21:41:28 UTC, Steven Schveighoffer 
wrote:
 Any ideas on better ways to handle this?
I've had such a situation before too where I want to switch over enums I read from an ELF file which can't be assumed to be correct, but I also don't want to forget one. For a tight numerical enum you simply check `if (i <= EnumType.max)`, but when there's gaps (or strings like in your case) that doesn't work. I got the idea for a DIP to allow a `default` statement in `final switch` to allow a custom error handler instead of the default `__switch_error`, but never pursued it further. I'm still in favor of it though.
Jun 17
prev sibling next sibling parent reply jfondren <julian.fondren gmail.com> writes:
On Thursday, 17 June 2021 at 21:41:28 UTC, Steven Schveighoffer 
wrote:
 A final switch on an enum complains if you don't handle all the 
 enum's cases. I like this feature.
...
 Oh, and to throw a monkey wrench in here, the value is a 
 string, not an integer. So I can't use std.conv.to to verify 
 the enum is valid (plus, then I'm running a switch twice).
Wanting to avoid more work than a switch means generating a switch. I think that's the real monkey wrench. Something like: ```d T enumCases(T, E, T[E] cases)(string x) { import std.format : format; import std.algorithm : map, all, joiner; import std.array : array; import std.traits : EnumMembers; import std.conv : to; mixin("switch (x) {\n" ~ [EnumMembers!E].map!(e => format!"case %(%s%): return %(%s%);\n"([e.to!string], [cases[e]])) .joiner.array ~ "default: assert(0);\n}"); } unittest { enum C { ABC, XYZ } assert("x\tb" == enumCases!(string, C, [ C.ABC: "x\tb", // error to omit an enum value C.XYZ: "ab", // impossible to have a bad enum value ])("ABC")); // the first problem with this solution: the following is an error... // unless the preceding usage is commented out. /+assert(2 == enumCases!(int, C, [ C.ABC: 1, C.XYZ: 2, ])("XYZ"));+/ } ```
Jun 17
parent jfondren <julian.fondren gmail.com> writes:
On Friday, 18 June 2021 at 04:24:19 UTC, jfondren wrote:
 On Thursday, 17 June 2021 at 21:41:28 UTC, Steven Schveighoffer 
 wrote:
 A final switch on an enum complains if you don't handle all 
 the enum's cases. I like this feature.
...
 Oh, and to throw a monkey wrench in here, the value is a 
 string, not an integer. So I can't use std.conv.to to verify 
 the enum is valid (plus, then I'm running a switch twice).
Wanting to avoid more work than a switch means generating a switch. I think that's the real monkey wrench.
Alternately, weave the check you want into your switch: ```d import std.traits : EnumMembers; import std.algorithm : map, canFind; import std.conv : to; enum C { ABC, XYZ } alias namesEnum(E) = s => [EnumMembers!E].map!(to!string).canFind(s); enum enumCount(E) = [EnumMembers!E].length; int example(string k) { switch (k) { case "ABC": static assert("ABC".namesEnum!C); return 1; case "XYZ": static assert("XYZ".namesEnum!C); return 2; default: static assert(2 == enumCount!C); return 0; } } unittest { example("force asserts"); } ``` but this repeats the keys (which could be mis-repeated), and it requires hand-counting the cases checked (which could be mis-counted). At least it's easier to check without reference to the enum. What I wanted to do was add "ABC"&c to a static string[] and confirm that it's a permutation of [EnumMembers!C].map!(to!string).
Jun 17
prev sibling next sibling parent reply Mathias LANG <geod24 gmail.com> writes:
On Thursday, 17 June 2021 at 21:41:28 UTC, Steven Schveighoffer 
wrote:
 A final switch on an enum complains if you don't handle all the 
 enum's cases. I like this feature.

 However, sometimes the data I'm switching on is coming from 
 elsewhere (i.e. a user), and while I want to enforce that the 
 data is valid (it's one of the enum values), I don't want to 
 crash the program if the incoming value is not correct. But 
 final switch doesn't let me declare a default case (to throw an 
 exception instead).

 If I use a non-final switch, then my code might forget to 
 handle one of the cases.

 Oh, and to throw a monkey wrench in here, the value is a 
 string, not an integer. So I can't use std.conv.to to verify 
 the enum is valid (plus, then I'm running a switch twice).

 Any ideas on better ways to handle this?

 -Steve
Well, if you receive an `enum` that have an out of bounds value, your problem lies in the caller, not the callee. You're breaking the most fundamental promise of a type, that is, the values it can take. And you obviously also break any ` safe` function by feeding it this value. So instead of thinking in terms of `enum`, I would say, think in them of the value, and generate the switch: ```D SWITCH: switch (myRawValue) { static foreach (EV; NoDuplicates!(EnumMembers!MyEnum)) { case EV: // Handle; break SWITCH; } default: throw new Exception("Invalid value: " ~ myRawValue); } ``` Note that this can be encapsulated in its own function, like `validateEnum (EnumType) (BaseType!EnumType value)` (not sure if we have a `BaseType` template, but you get the point).
Jun 17
parent Steven Schveighoffer <schveiguy gmail.com> writes:
On 6/18/21 12:40 AM, Mathias LANG wrote:
 On Thursday, 17 June 2021 at 21:41:28 UTC, Steven Schveighoffer wrote:
 A final switch on an enum complains if you don't handle all the enum's 
 cases. I like this feature.

 However, sometimes the data I'm switching on is coming from elsewhere 
 (i.e. a user), and while I want to enforce that the data is valid 
 (it's one of the enum values), I don't want to crash the program if 
 the incoming value is not correct. But final switch doesn't let me 
 declare a default case (to throw an exception instead).

 If I use a non-final switch, then my code might forget to handle one 
 of the cases.

 Oh, and to throw a monkey wrench in here, the value is a string, not 
 an integer. So I can't use std.conv.to to verify the enum is valid 
 (plus, then I'm running a switch twice).

 Any ideas on better ways to handle this?
Well, if you receive an `enum` that have an out of bounds value, your problem lies in the caller, not the callee. You're breaking the most fundamental promise of a type, that is, the values it can take. And you obviously also break any ` safe` function by feeding it this value.
Yeah, I know. But I'm not receiving an enum. I'm receiving a string. But I want to handle a certain set of those strings everywhere. So what I tried is to make an enum that has those strings. Then I would use final switches whenever I handle it, so if I add a new string to the list, the compiler will tell me where I missed handling that new one.
 
 So instead of thinking in terms of `enum`, I would say, think in them of 
 the value, and generate the switch:
 ```D
 SWITCH: switch (myRawValue)
 {
      static foreach (EV; NoDuplicates!(EnumMembers!MyEnum))
      {
          case EV:
              // Handle;
              break SWITCH;
      }
      default:
          throw new Exception("Invalid value: " ~ myRawValue);
 }
 ```
 
The // Handle then becomes a new switch. Though maybe I can group some of them together. I may as well use std.conv.to at that point. I think that's what I'm probably going to do, I just wondered if there was a better way. -Steve
Jun 18
prev sibling parent reply Johan <j j.nl> writes:
On Thursday, 17 June 2021 at 21:41:28 UTC, Steven Schveighoffer 
wrote:
 However, sometimes the data I'm switching on is coming from 
 elsewhere (i.e. a user), and while I want to enforce that the 
 data is valid (it's one of the enum values), I don't want to 
 crash the program if the incoming value is not correct. But 
 final switch doesn't let me declare a default case (to throw an 
 exception instead).

 If I use a non-final switch, then my code might forget to 
 handle one of the cases.

 Any ideas on better ways to handle this?
Perhaps just a non-final switch, with a static assert comparing the number of cases handled to the number of enum members? (managable if the number of cases is small and easily countable) -Johan
Jun 18
parent Steven Schveighoffer <schveiguy gmail.com> writes:
On 6/18/21 6:35 AM, Johan wrote:
 On Thursday, 17 June 2021 at 21:41:28 UTC, Steven Schveighoffer wrote:
 However, sometimes the data I'm switching on is coming from elsewhere 
 (i.e. a user), and while I want to enforce that the data is valid 
 (it's one of the enum values), I don't want to crash the program if 
 the incoming value is not correct. But final switch doesn't let me 
 declare a default case (to throw an exception instead).

 If I use a non-final switch, then my code might forget to handle one 
 of the cases.

 Any ideas on better ways to handle this?
Perhaps just a non-final switch, with a static assert comparing the number of cases handled to the number of enum members? (managable if the number of cases is small and easily countable)
Hm... interesting idea! I don't know how to count the number of cases, but possibly a CTFE validation would be possible (basically use CTFE to try all the switch cases, and if any throws an exception, then you found one that's not handled). -Steve
Jun 18