Can I save a pattern as generic parameter?

For example, I have an enum type:

enum Item {
    Foo(i32),
    Bar(String),
    Baz(f64),
}

And a vector of Item: items: Vec<Item>
I want to know if there's any item which is Foo, no matter what value it has, I write like this:

fn has_foo(items: &[Item]) -> bool {
    for item in items {
        if let Foo(_) = item {
            return true;
        }
    }
    return false;
}

And now comes the questions: What if I want to check an arbitrary variant? I guess the match pattern is something that only exist on compile time, so the function may look like this:

fn has<Item::XX/*I don't know how to write this*/>(pattern:XX, items: &[Item]) -> bool 

So I can use the function like:

let res = has(Item::Foo, items);

It looks to me that the only option is to use macros, but I wondered if there are other ways.

A pattern is not a type, so no. But you can accept a closure that returns true if a specific item matches, and you can turn a match into a boolean directly using the matches!macro.

Also note that your loop is essentialy Iterator::any():

items.any(|item| matches!(item, Item::Foo(_)))
5 Likes

As @H2CO3 says, a closure helps:

fn has(items: &[Item], f: impl FnMut(&Item) -> bool) -> bool {
    items.iter().any(f)
}

IIRC, there's a function in std that can turn an enum variant to some sort of numeric representation, but I forget the name of that function and can't find it again, do you happen to know that?

I found that, it's std::mem::discriminant

It's mem::discriminant(), but that only works for enums, not arbitrary patterns.

2 Likes

Too bad that Discriminant does not fit for feature adt_const_params for now, or I can write like this:

fn find<const I: Discriminant<Item>>(items: &[Item]) {
    items.any(|item| std::mem::discriminant(item) == I)
}

Another bad thing is that it is not allowed to write like this:

discriminant(&Item::Foo);

Though, a MaybeUninit::uninit().assume_init() can help:

find::<{std::mem::discriminant(&Item::Foo(unsafe {MaybeUninit::uninit().assume_init()}))}>(items.as_slice())

But you don't need that – you can use a Discriminant like any other regular value. This works:

fn find(items: &[Item], d: Discriminant<Item>) -> bool {
    items.iter().any(|item| discriminant(item) == d)
}

It can't, that's instant undefined behavior. You could use a sensible, cheap-to-construct value instead, such as 0 or String::new() (for Item::Bar) or in general Default::default().

1 Like

Please do not focus on the example of

enum Item {
    Foo(i32),
    Bar(String),
    Baz(f64),
}

I'm talking about a more generic situation, in which not all types are implemented Default, even more, you may not able to create a instance of a certain type. In this case, I cannot offer a

d: Discriminant<Item>

without using MaybeUninit. And I think it is safe, because in this case, I don't realy read any value from the variant, I just use the Discriminant for comparing, right? Please correct me if I wrong.

Don't use MaybeUninit::uninit().assume_init() for a type you don't know it's safe to be fully uninitialized, there's very few of those, mostly types composed entirely of MaybeUninit, certainly no enums,

I strongly favor the "pass a closure to match against the variant" approach,
with deriving a mirror of the enum without.fields (usually with a Kind suffix) as the other approach.

Even if it was "fine" to use MaybeUninit::uninit().assume_init() the way you have been,
you'll get some surprising behaviors:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=cf49ff1fc315f5a20d39ac0742fc5c60

#[derive(Debug)]
enum Item {
    Foo,
    Bar(core::num::NonZeroU8),
}

fn main() {
    for item in [
        Item::Foo,
        unsafe{ Item::Bar(std::mem::MaybeUninit::uninit().assume_init()) },
    ] {
        eprintln!("{item:?} {:?}", std::mem::discriminant(&item));
    }
}

the above prints

Foo Discriminant(0)
Foo Discriminant(0)
2 Likes

Wrong. Creating an uninitialized value is instant UB, as I mentioned already. You don't need to read it in order to trigger UB.

3 Likes

@matt1992 @H2CO3 let me make sure we are talking the same thing, let's say the enum is now like:

enum Item {
    Foo(FooStruct),
    Bar(BarStruct),
    Baz(BazStruct),
}

I'm not creating an enum with MaybeUninit, I'm creating, for example, an Item::Foo(MaybeUninit), and use it to compare with other Item::Foo(_)s, by using Discriminant.

Even in this case, An Item::Foo(MaybeUninit) can still have a chance to mess up Discriminant, right?

As you can see in the example in my previous message, leaving fields uninitialized can change the variant.
(I'm still ignoring UB for the moment, you shouldn't use uninitialized memory for this at all)

2 Likes

That's really conviencing, appearently discriminant deos work the way I think it is.

I understood what you were talking about. Foo(MaybeUninit::uninit().assume_init()) is undefined behavior. It doesn't matter what kind of type you are instantiating in this way: if it's an enum, a struct, a primitive integer, etc. – if you lie to the compiler about the value being initialized, it is always and unconditionally UB. (The only exception is when the type is MaybeUninit itself or a composite type only consisting of MaybeUninits, but that is not the case here.)

3 Likes

It seems that I asked an X/Y problem, I will open a new topic to explain what I met and why I'm so eager to use a variant without the type.