[Feature] Obtain enum variant's discriminant without creating the variant

This feature requires is akin to Idea: Default method of comparing enum variants with data

Motivation

Almost all parsers operate on notion of token stream. For token stream it is useful to compare next token's type with expected type without advancing token stream while disregarding associated data:

pub enum Token {
    Word(String),
    Number(u16),
    SpecialSymbol(char),
    EndOfInput,
}

fn expect(self: &TokenStream, expected: Token) -> Result<(), TokenStreamError> ;

// first example
token_stream.expect(Token::Word); // don't care about data, just the type

// -------------------------------------------
// second example

// this token would be obtained from parsing
// it has both type information and associated data
let token = Token::Number(123);

// Token::Word only represents type of a token. Disregard data.
if token != Token::Word { // not interested in data
                          // notice `!=` not equals operator
                          // with pattern matching there is no way 
                          // to do this without `else` clause
                          // `matches!()` won't work either
  // ...
}

Both examples won't compile now. They require to instantiate Token::Word with at least dummy data like Token::Word("").

The example can be generalized as: check type of data disregarding data itself. Modeling typed data with rust enums is the most straightforward way! Only the last bit is missing: check enum variant without constructing other instance of enum variant.

So the main usage is within parsers and weakly typed environments such as when building DSLs.

Alternatives

We could also use mem::discriminant. But it also requires to instantiate enum variant with data supplied.

So currently there are several ways to accomplish the task:

  • use pattern matching (requires a lot of boilerplate code)
if let Token::Word(_) = token_stream.peek() {
  return Err(TokenStreamError::UnexpectedToken("word"));
}

if let Token::Number(_) = token_stream.peek() {
  return Err(TokenStreamError::UnexpectedToken("number"))
}
  • use macros to either generate the above code or to generate enum with same variants but without associated data:
enum TokenType {
  Word,
  Number,
  SpecialSymbol,
  EndOfInput
}

As you can see the problem is already solvable in user space but not without considerable hassle.

Solution

Allow to compare enum variants only by discriminant without constructing them:

fn expect(&self, expected: Discriminant<Token>) -> Result<(), TokenStreamError> {
  mem::discriminant(self) == expected // expected is a variant of token without data supplied 
}

let token = Token::Number(123);
// essentially next line should be possible
token.expect(mem::discriminant(Token::Word)); 

The proposal

Allow to obtain enum discriminant from enum variant constructor

4 Likes

I think this would be a good feature to have, making mem::discriminant more “complete”. However:

  • It is possible to do currently by using a macro to define a data-less enum (see for example strum::EnumDiscriminants), and this has the advantage of being possible to exhaustively match, whereas mem::Discriminant is opaque.

  • mem::discriminant(Token::Word) isn't a sufficiently general syntax for it, because that works only for tuple-style enum variants and not struct-style (where Token::Word is an erroneous expression). So, a macro (or some other syntax extension) will be needed: mem::discriminant!(Token::Word).

5 Likes

In analogy to offset_of!, this should probably be spelled discriminant_of!(path::Enum, Variant). I concur that this would be useful functionality to have.

For current Rust, using a derive to generate the associated EnumKind enum seems the most functional approach. If you have some method to create dummy const values for the enum's associated data, you can even directly polyfill discriminant_of![1] and create mem::Discriminant.

A notable alternative is syn's approach of having a separate AST type for each token kind and peeking using those.


  1. Although doing so in a path resilient manner requires using the same-path macro name slot, which is highly desirable space for "clever" macros. ↩︎

4 Likes

I'd prefer something resembling an associated value, e.g. Token::Word::DISCRIMINANT

1 Like

Then may be compiler could present this data via associated members, while discriminant_of! macro would just wrap access to the member

I agree. One syntax I've seen proposed, to avoid conflict, is something like TheEnum::Variant::enum::DISCRIMINANT. ::enum is something that can't possibly conflict with any actual associated item, since enum is a keyword.

1 Like

Enum variants can't currently have associated items because they're not types, so Enum::Variant::DISCRIMINANT would require addition of a magic one-off syntax to the language. If enum variants ever become actual types then this would work.

Something like this is possible on nightly:

#![feature(inherent_associated_types)]

enum Foo { Bar, Baz }

struct Foo_Bar;
struct Foo_Baz;

impl Foo {
    type Bar = Foo_Bar;
    type Baz = Foo_Baz;
}
impl Foo_Bar {
    const DISCR: Discriminant = /* ... */;
}
impl Foo_Baz {
    const DISCR: Discriminant = /* ... */;
}

but I think it's not possible to access Foo::Bar::DISCR in any way because Foo::Bar the variant hides Foo::Bar the type. I guess it would be possible to adjust the name resolution rules a bit so the compiler could generate these stand-in types, but I don't think it's worth it when a macro is a "good enough" solution.

That sounds like a bug in the inherent_associated_types feature to me. Given the context of a subsequent :: path separator, it should be unambiguous that it's an associated item being referenced rather than the variant.

1 Like