Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for using both '_' and else prongs at the same time in switch statements for non-exhaustive enums. #12250

Open
Flaminator opened this issue Jul 26, 2022 · 5 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@Flaminator
Copy link

Currently when using switch statements with non-exhaustive enums you are able to use it in the following 2 ways as explained in the documentation:

A switch on a non-exhaustive enum can include a '_' prong as an alternative to an else prong with the difference being that it makes it a compile error if all the known tag names are not handled by the switch.

const Enum = enum(u32)
{
    A = 1,
    B = 2,
    C = 44,
    _
};

fn some_function(value: Enum) void
{
    // Compiles as all tags are handled
    switch(value)
    {
        .A => {},
        .B => {},
        .C => {},
        _ => {},  // Unnamed tags go here
    }

    // Compiles as all tags are handled
     switch(value)
    {
        .A => {},
        .B => {},
        else => {}, // Both named and unnamed tags go here
    }
}

As I had not read that part of the documentation and I have only been using '_' prongs, I was expecting the following code to work as well:

fn some_other_function(value: Enum) void
{
    // Does not compile giving "error: else and '_' prong in switch expression"
    switch(value)
    {
        .A => {},
        .C => {},
        else => {},   // Named tags go here so .B in this case
        _ => {},      // Unnamed tags go here
    }
}

The idea I had in my head was that the '_' prong would catch the unnamed tags and the else prong the named tags. I was running into this when I was using the Win32 API and had written my own enum for the WM_NOTIFY values. As the part of the code I was working on did not need all of the named tags I thought I could add the else prong to filter those out.

I asked about this on IRC and more people thought it worked the way I thought it would work. So I opened this issue to see if it is viable to add something like this to the language. I have no real preference for any kind of syntax for this but the way I had written it down seemed really natural.
One thing that did struck me when I was making this issue was that in my head I saw else as something for named tags and '_' for unnamed tags so it didn't even come up to me that using else in non-exhausted enums would actually also catch the unnamed tags.

@ifreund ifreund added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Jul 26, 2022
@ifreund ifreund added this to the 0.11.0 milestone Jul 26, 2022
@mk12
Copy link
Contributor

mk12 commented Sep 10, 2022

If you think of non-exhaustive enums as "Allow arbitrary u32 values in addition to the named ones," then this proposal makes sense. Even though you aren't specifically handling .B, you care about named vs. unnamed.

But the main use case for them is, "Allow me to add new named values in the future". And in particular, "Force client code to decide whether they want to be broken when I add new members". When clients write exhaustive switches, they use _ to say, "Yes, please break so I can handle the new member" or else to say "No, do not break my code." Using both here seems strange: "No, don't break my code, but let me react differently if the member was added before I compiled or after".

There are also a couple alternatives to changing the language:

  • Explicitly list all the named tags instead of else. Maybe this is impractical if there are a lot of them.
  • Use else only, but inside it handle the two cases with something like isNamedTag(Enum, value). I think that should be possible with Zig's metaprogramming. Maybe that function could go in the stdlib.

I'm not necessarily opposed to your idea — you could argue it's simplifying the language rather than complicating it, by removing a barrier. My only qualm is that my intuition gets else and _ backwards here. You said:

it didn't even come up to me that using else in non-exhausted enums would actually also catch the unnamed tags.

To me it seems intuitive because else makes me think of converting the switch to an if-else chain, where it becomes the final else that handles everything. But in your proposal else is the first level catch-all, and _ is the real catch-all.

At the very least if this is accepted I think the compiler should enforce putting them in the order you showed: else, then _.

@tsmanner
Copy link

I was just toying with an idea after question about whether switches cases could handle using a set of comptime-known tags/values for a prong rather than requiring each value to be written out individually. The example given was a way to define punctuation characters as the return value from a comptime function (presumably so the same set of them can be reused in multiple places), and have a switch prong catch those. Allowing else and _ prongs in non-exhaustive enum switches would allow for a user-space solution to that problem by reifying a new enum type rather than returning a slice. Example:

const std = @import("std");

fn Punctuation() type {
    return @Type(.{
        .Enum = .{
            .layout = .Auto,
            .tag_type = u8,
            .fields = &[_]std.builtin.Type.EnumField{
                .{ .name = ".", .value = '.' },
                .{ .name = ",", .value = ',' },
            },
            .decls = &[_]std.builtin.Type.Declaration{},
            .is_exhaustive = false,
        }
    });
}

pub fn main() !void {
    var x: u8 = ' ';
    switch (@intToEnum(Punctuation(), x)) {
        inline else => |punc| std.debug.print("Punctuation {s}\n", .{[_]u8{@enumToInt(punc)}}),
        _ => |non_punc| std.debug.print("Non-punctuation {s}\n", .{[_]u8{@enumToInt(non_punc)}}),
    }
}

It's a little bit unfortunate that it has to use intToEnum and enumToInt all over the place, but it's also being truthful and could be cleaned up or encapsulated better, I'm sure.

@Flaminator
Copy link
Author

Flaminator commented Dec 20, 2022

@mk12 I only just saw your comment, in my use case as I am wrapping around an existing c api so I am indeed just using an enum to give names to an u32. Of course that also means there are values that should be in the enum that I just haven't encountered or seen yet that exist.

I don't necessarily see it as something to do with breakage and more something to do with there might be values I don't know about or that might be added later on. Adding a new tag to a normal enum would also break client code after all. I kinda forgot what the idea behind adding non-exhaustive enums was again.

I still think having else for tagged and '_' for untagged things makes the most sense. I just wasn't necessarily thinking else would also cover '_'.

@hryx
Copy link
Sponsor Contributor

hryx commented Dec 21, 2022

I've recently wanted this feature when dealing with a collection of fairly long non-exhaustive enums. Here is an example: https://github.com/hryx/llvm-bitcode/blob/6b60fd4/src/Bitcode.zig#L352-L432

In this case, I need to switch on a subset of the enum for partial processing, then switch on a different subset for further processing. On either of these passes, it would be nice to use else to catch values which are known but irrelevant, and _ to catch values which are unknown/invalid. (Or vice-versa, keyword choice is besides the point.) As it is, I have to either duplicate the large list of values or lose the distinction between irrelevant and unknown values.

@Khitiara
Copy link

my biggest use case here is to use an inline else for handling known values which is presently illegal as that isnt exhaustive, and i have to fall back to an inline for and hope the optimizer realises its a switch with the _ case being after the for

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

7 participants