Keyword idea: Boosting enum functionality

Preamble

Good day folks, I'm new here :wave:
So if there are some issues regarding me and my behavior please point that out and don't raise hell.
Ask for clarification if you're unsure about anything,
apparently I'm bad at communicating. Or so I'm told.
Also, I'm not very proficient with Rust since I'm not a programmer by trade.
Programming still comes somewhat with the territory as an engineer and electrician.
(So if I paste any "dumb" code, advice is appreciated)
Now let's get to the actual topic:

New plan, see here.

The keyword "every"
That is really just a one trick pony or rather syntactic sugar that you can do with macros (I think) but that's not the point...

So enums are absolutely awesome, I love them.
They are very versatile and perfect for any kind of state or anything else that's definitive.
They can even hold stuff, great!
In conjunction with match they force you to implement every possibility so you can't get undefined behavior for forgetting something.

And forgetting something, oh boy, that's easy.
So to prevent that I'd like the compiler to yell at me some more.

I often find myself in a situation where I want to do some setup depending on a enum and its variants.
Basically, I want to execute a block for every variant the enum contains and it would be nice if the compiler yells at me if I temper at some point with enum itself without adjusting those blocks.

match does that, but I'm not aware that there is a native method to do something for every enum variant there is.

And that's the idea, the every keyword.
It would look something like this:

enum State {
    Solid,
    Liquid,
    Gas,
    Plasma,
}
fn setup() {
    //stuff happening
    every State{
        Solid => todo!(),
        Liquid => todo!(),
        Gas => todo!(),
        Plasma => todo!(),
    }
    //more stuff gets done
}

What keyword(s) ends up used isn't that important, an instant alternative I could think of would be foreach.
The functionality is something that could add real value to the language it self.
Granted this is niche, but there is a plethora of macro crates out in the wilds that add this functionality in some form or another.
This just shows that there is a considerable interest in it and worth the effort.





You made it, feel free to give feedback :wink:

Sure! You'll have to clarify the difference to existing match. All I can find on this so far is

So as far as I can tell, your claimed difference is that match isn't "native", whatever that's supposed to mean.

1 Like

Is this the same as, using enum-iterator,

#[derive(enum_iterator::Sequence)]
enum State {
    Solid,
    Liquid,
    Gas,
    Plasma,
}

fn setup() {
    //stuff happening
    
    for state in enum_iterator::all::<State>() {
        match state {
            Solid => todo!(),
            Liquid => todo!(),
            Gas => todo!(),
            Plasma => todo!(),
        }
    }
    //more stuff gets done
}

Or is it something else?

3 Likes

match selects an arm depending on a input.
every doesn't have a input, it's supposed to force you to write a block for every enum variant and executes them all.

Another example:

impl State {
    fn get_all_variants() -> Vec<Self> {
        let mut variants = Vec::new();
        every Self {
            Solid => {variants.push(Self::Solid)},
            Liquid => {variants.push(Self::Liquid)},
            Gas => {variants.push(Self::Gas)},
            Plasma => {variants.push(Self::Plasma)},
        }
        variants
    }
}

And with native I meant without the use of macros.

1 Like

Yes and No.
Iterating comes with it own pitfalls.
You can easily build an iterator with it without the use of any macros.

Ah, thanks for clarifying.

1 Like

Can you provide an example of what happens with a non-unit variant (i.e., with fields)? What is the syntax on the left side for that? Also, is there some correlation between the "pattern" (I don't think it's a pattern as it's not being matched against anything…) on the left and use on the right? That is, would this be lintable?

impl State {
    fn get_all_variants() -> Vec<Self> {
        let mut variants = Vec::new();
        every Self {
            Solid => {variants.push(Self::Liquid)},
            Liquid => {variants.push(Self::Solid)},
            Gas => {variants.push(Self::Plasma)},
            Plasma => {variants.push(Self::Plasma)},
        }
        variants
    }
}
3 Likes

Since this is just to make sure not to overlook something,
fields should just show what they accept.

This entire idea is to give you a piece of mind by providing a safeguard that you have made a conscious decision to do something or ignore something.
This:

fn list_state_properties() -> Vec<String> {
    let mut properties= Vec::new();
    every State{
       Solid(isize) => {properties.push("hard object, has mass".to_string())},
       Liquid(f64) => {properties.push("fluid, in liters".to_string())},
       Gas(String, Vec<Element>) => {properties.push("compound gas, name, elements".to_string())},
       Plasma => {}, //or something else to denote it's ignored
    }
    properties
}

is essentially the same as:

fn list_state_properties() -> Vec<String> {
   let mut properties= Vec::new();
        
   properties.push("hard object, has mass".to_string());
   properties.push("fluid, in liters".to_string());
   properties.push("compound gas, name, elements".to_string());
        
   properties
}

However the second figure gives you zero guaranties that you haven't missed a thing
and provides no visual feedback what is actually happening.

Also why shouldn't it work with linters?

I think we could get most of the value of this by having a (const) way to get an array of all the variants of an enum, similar to what the mentioned enum-iterator provides. For fieldless enums this could be an actual list of the values; for enums with fields it could perhaps enumerate the discriminants in some matchable way. Either way, it'd then be possible to write a compile-time or runtime loop over the variants, with exhaustiveness checking as usual.

8 Likes

The iterator approach requires a actual iteration and match involves branching.
That's completely side stepped with the "every" approach.

All the compiler has to do is to only honer the actual blocks, not the paint around it :rofl:

Still, the ability to iterate enums would be a awesome thing.

EDIT:
Oh wait you're already in business mode, aren't you?
lol, never mind then.

I would love to see some real-world scenarios where this would significantly help. I'm trying to think of past scenarios when I would have used something like this and I can't think of anything.

Also, I think this doesn't do enough to protect against mistakes to justify its addition to the language. @mathstuf's simple example shows it's easy to still make mistakes.

3 Likes

His example is a mistake that wouldn't raise any detection,
but is one easily discovered since it is very clear what the intent of the function is.

Filtering that isn't the intention anyways.

I see that you're explicitly asking for a non-macro solution, but I couldn't help myself and wrote a macro anyway:

macro_rules! every{
    ($enum_type:ty { $($pattern:pat => $code:expr),* $(,)? }) => {{
        fn _check_exhaustive(v: $enum_type) {
            match v {
                $( $pattern => () ),*
            }
        }
        
        $( $code; )*
    }}
}

Playground

No loops or branches are required.

11 Likes

I've previously proposed something similar as a lint to clippy:

This sort of structure doesn't work for my example there, because each variant needs to be mentioned on the rhs of a match arm:

enum Intensity { Normal, Bold, Faint }
struct Unknown;
impl FromStr for Intensity {
    type Err = Unknown;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "normal" => Ok(Intensity::Normal),
            "bold" => Ok(Intensity::Bold),
            "faint" => Ok(Intensity::Faint),
            _ => Err(Unknown),
        }
    }
}
3 Likes

The main use-cases I can think of are testing and conversion, where I want to make sure I produce every variant and get a compiler error when I don't (because a new variant has been added, or because I just forgot one).

 impl FromStr for MyEnum {
     type Err = ();
     fn from_str(input: &str) -> Result<Self, Self::Err> {
-        match input {
-            "foo" => Ok(Self::Foo),
-            "bar" => Ok(Self::Bar),
-            _ => Err(())
-        }
+        every Self {
+            Foo => if input == "foo" { return Ok(Self::Foo) },
+            Bar => if input == "bar" { return Ok(Self::Bar) },
+            // Oops, forgot this one the first time!
+            Quz => if input == "quz" { return Ok(Self::Quz) },
+        }
+        Err(())
    }
}

That said, strum takes care of most of my needs in this area.[1] And when the logic is the same like this, an iterator would be nicer anyway.

EnumIter
    .filter(|var| var.as_str() == input)
    .next()
    .ok_or(())

  1. with EnumVariantNames or EnumDiscriminants and EnumIter if nothing else ↩︎

2 Likes

This loop can be unrolled and the branching eliminated by the optimizer. This should be pretty reliable.

6 Likes

Honestly, the optimization Rust manages to achieve is pure magic to me.
Eliminating branching as much as possible makes stuff so much safer and faster.

Sometimes I manage to get my hands on the PLC source for some of our machines.
And see immediately how the thing self-destructs when some tiny position sensor fails because they opted for a wall of arbitrary if statements instead of stepping through a truth table containing the supposed states of all relevant sensors and actors.
Writing those tables is step one before you even start programming the thing, well according to my teacher who bashed that into my head back when I was in school.

1 Like

What's the point of having to repeat the types of variants' fields? If anything, I'd argue they should work like regular match patterns with the exception that you cannot use that data in the block (and you get a warning if you give it a name that does not begin with an underscore). So

every State{
   Solid(_) => ...,
   Liquid(_) => ...,
   Gas(_, _) => ...,
   Plasma => ...,
}

Doing it like this would mean we could "split" variants if fields of their variants are also enums:

enum Foo {
    Bar(Option<i32>),
    Baz(Option<i32>),
}

every Foo {
    Bar(Some(_)) => ...,
    Bar(None) => ...,
    Baz(_) => ...,
}
5 Likes

So I've brooding over this a bit now.
Essentially there are 3.5 cases where you want to make sure you aren't forgetting any enum variant.


Case one:
Decide to do something based on the enum variant.
That's covered by your trusty old match function.
So we're good here.

Case two:
Make sure you return every variant based on a input.
E.g. useful for the mentioned from_string function.

Case three:
Do something for every enum variant.
Not necessarily with the enum variant itself but the variant is reason why this code exists.
This functionality would be an absolute boon for writing tests.
This is tricky since @mjbshaw concerns that mishaps are still to easy is true,
at least with the current approach.


So I made a revision to my initial suggestion:

//Case two:
let variant =  match input as Enum {
                  _ => Enum::Variant, //to intentionally ignore a Variant needs to be after the catch all.
                  /*normal match flow
                  but compiler makes
                  sure every variant
                  is returned at least
                  once.
                  Throw a warning
                  by returning a variant
                  more than once.*/
               }

//Case three:
      do for Enum{
          Enum::Variant(field type) escaped before { block },
         {Enum::Variant(filled field) used inside block},
          Enum::Variant _, //case ignored
          /*Same spiel as before, make sure every
          variant is mentioned at least once.
          Throw an error or warning otherwise.*/
      }

I think this should add a considerable amount of value to Rusts functionality and make everyone happy.
It also avoids a new keyword by using existing/reserved ones while simultaneously being more concise.

What ya´ think?

Oh, and the Case 0.5 would be to include a std. macro enum iterator :wink:




@idanarye
I fail to see value in your suggestion,because the fields would be ignored anyway.
The primary goal is just to show there is something, so you're aware of it.

Actually, putting in the correct default/dummy data could make sure the compiler yells at you if the field-type changes.
So yeah good call, if that was your intention.

It seems to me that what is wanted here is an iterator over discriminant variants, not variants directly. Could a crate do something like:

#[derive(Discriminant, DiscriminantRange)] // Makes `FooDiscriminant` and enables `Range` usage with it.
enum Foo {
  V1,
  V2(String),
  V3(i32),
}

which could be used as something like (.. as Range<FooDiscriminant>).iter().for_each(…). This feels far more generic and allows using combinators to do things in whatever order (with a sorting function), subset (with filter), or grouping (with other Iterator methods).

Reading docs, my proposed code is not valid, but I hope the .. as bit is understandable to those with more experience with Range to do the "get every discriminant in an Iterator" case.