Elliding type in matching an Enum

I am sure this was brought before but I was not able to find a convincing argument against it. Therefore, and within the language ergonomic discussion, I thought it might be a good time to post it here.

I find this example from the book quite annoying:

enum Message {
    Quit,
    ChangeColor(i32, i32, i32),
    Move { x: i32, y: i32 },
    Write(String),
}

fn process_message(msg: Message) {
    match msg {
        Message::Quit => quit(),
        Message::ChangeColor(r, g, b) => change_color(r, g, b),
        Message::Move { x: x, y: y } => move_cursor(x, y),
        Message::Write(s) => println!("{}", s),
    };
}

It is too verbose and due to indentation the code can be very annoying to read if it is deeply nested. So the question is: if you are matching an Enum variant, cannot Message:: (in this case) be implicit?

fn process_message(msg: Message) {
    match msg {
        Quit => quit(),
        ChangeColor(r, g, b) => change_color(r, g, b),
        Move { x: x, y: y } => move_cursor(x, y),
        Write(s) => println!("{}", s),
    };
}

I have seen ways to mitigate this (such as use Message::*) but I think some implicitness here will be nicer. Following the argumentation line proposed the blog post

  • Applicability. It would be applicable in every situation having an enum match, a quite common piece of code in Rust

  • Power. The elided information cannot have an unwanted influence. And if you do something wrong the compiler will complain.

  • Context-dependence. What is elided is the Enum Type, so the question is if there is a way a casual reader of the code can know which type it is and where to find its definition. The variable to be matched (msg in this case) could come from 3 places:

  • (1) it can be defined in the same function

  • (2) it can be an argument of the function being read and

  • (3) it can be the result of calling another function.

Finding the enum type and source for (1) and (2) is straight forward. For (3) it would require going to the definition fo the function.

4 Likes

Note this is valid code:

fn process_message(msg: Message) {
    use Message::*;
    match msg {
        Quit => quit(),
        ChangeColor(r, g, b) => change_color(r, g, b),
        Move { x: x, y: y } => move_cursor(x, y),
        Write(s) => println!("{}", s),
    };
}
8 Likes

I agree this is a problem and it would be good to have something nicer.

The proposed solution has two limitations:

  1. It’s technically a breaking change, since currently this compiles and creates a new variable called Quit (see E0170)

     fn process_message(msg: Message) {
         match msg {
             Quit => quit(), // valid, equivalent of let Quit
             _ => {}
        }
     }
    
  2. It doesn’t generalize to other cases, where shortening enum variants would be nice too (e.g. assignments, function arguments)

     process_message(Message::Quit);
    
    in Swift that would be:
    
     process_message(.Quit);
    

    so maybe Rust should have a syntax for it too. In another thread there was a proposal for generalizing _ to enum types, so that _::Quit uses type inference to find it’s a Message

     process_message(_::Quit)
    

    The syntax is consistent and logical for Rust, but admittedly not pretty.

I rather like that. Sure it's not as concise as Swift, but it's still a large improvement. I'd like to see that that kind of inference in places. In patterns like the match statement is would be an excellent start, but maybe also in expressions?

process_data(_::default())

It definitely shouldn't be used in every expression, but if the code is repetitive or the type is obvious from context I like that this reduces noise.

2 Likes

I had a similar idea for eliding the name of enums, although there the main focus was on expressions rather than patterns. I see no reason why it can’t be extended to patterns as long as there is enough information available for type inferencer.

As @kornel noted, to avoid breaking existing code it would be necessary to use some sort of marker. I suggested _:: as a prefix, which is in line with the use of _ as a way to omit parts of a type.

1 Like

That’s a nice idea but unfortunately eliding trait names will lead to ambiguity.

If someone else defines a Default2 with a default() method, possibly in a totally unrelated crate, then there’s no way to tell whether you meant Default::default() or Default2::default() if both are valid in the context. Unless the compiler explicitly requires use Default;, which I suppose could work too.

I think it would be better if we could do

use Default::default;

I see no reason why this would cause any problems since you can already do the same by defining a wrapper function. (Is there a proposal for this somewhere?)

But _::default() for fn process_message(msg: Message) {} would expand to Message::default(), which seems consistent (and the compiler will then rightfully complain that it can't disambiguate that)

Ah OK, makes sense.

When typing the type too often annoys me too much, I do the trick @leonardo pointed out, with the use statement. I’m coming from C/C++ where constants are in global scope, and I’d hate having to return there.

Under this proposal, the enum variants would not be in the global scope. Rather, they are context-dependent, completely analogous to how .foo is a context-dependent field name.

3 Likes

@kornel: I like the idea of having a syntax for this. As you mention, introducing a non breaking change that generalizes to other cases is extremely nice. We could bikeshed on the syntax but _:: is compact and clear.

Is there enough consensus to write an RFC and open it for official discussion?

2 Likes

One was written not too long ago: https://github.com/rust-lang/rfcs/pull/1949

_:: is neither compact nor clear. It’s ugly. Very ugly.

2 Likes

What do you mean that is not compact? It is only 3 characters! As @kornel discussed using the variant name is not an option. Therefore only a syntax with 1 and 2 characters would be more compact. I am open to ideas, but this brings me to the second point: Clarity

:: already means something in Rust and the syntax proposed by @kornel syntax is just using the same meaning which is a good thing. So the only open question is what to use to indicate "please fill the type here".

You might find it _:: ugly but neither compact nor clear is an overstatement.

4 Likes

See this issue and in particular, see this comment

The main arguments against this seems to be that match x { A =>...} is already valid creates a variable name A. But while valid, this code is strongly discouraged and triggers a warning. I know that rust loves backward compatibility but I think accepting this code (even with a warning) is a bug that should be fixed. The warning has been there for a long time and fixing old code should be easy. I think we should make the warning an error and in the next release make the NameOfEnum:: prefix optional.

OCaml has been doing this from the beginning: variables starting with lowercase letter and variants with uppercase letter.

2 Likes

Could it just be that? If you try to write a floating point literal as .5 you get "expected expression, found .", so I think that confirms there aren't currently any expressions starting with ..

(Does have the downside of being fairly different from any existing rust syntax, though...)

Is this better?

process_message(::<>Quit);

:stuck_out_tongue:

2 Likes

Since there is already an RFC for this, I’d suggest we try to move the discussion over there:

https://github.com/rust-lang/rfcs/pull/1949

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.