Thread overview
semi-final switch?
Jun 17, 2021
H. S. Teoh
Jun 17, 2021
Dennis
Jun 18, 2021
jfondren
Jun 18, 2021
jfondren
Jun 18, 2021
Mathias LANG
Jun 18, 2021
Johan
June 17, 2021
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
June 17, 2021
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)
June 17, 2021

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.

June 18, 2021

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:

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"));+/
}
June 18, 2021

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:

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).

June 18, 2021

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:

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).

June 18, 2021
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



June 18, 2021
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
June 18, 2021
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
June 18, 2021
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