How to replace associated data of an enum variant, without resorting to match

So I have an enum with many dozens of variants. Each variant has a single associated value, It can be a f32, an u8, i8, 16, etc.

And I have a function that receives a value representing an enum variant, and also receives a string slice that should be castable as the same type that has to go into that variant. I then check whether the value can be casted to the expected type, and whether it is within some expected range. Then I return an Option contanining a specific enum variant containing that value, or None if it fails.

This already works well. I've also created several wrapper functions (one for each associated value type) to help avoiding repetition. Now I'm trying to create a more general wrapper but I'm stuck at this particular pain point:

The function needs receive a specific enum variant as an argument, and it has to be able to put the already checked value inside that received variant. And I don't want to use a match because of the huge number of variants there are.

Do you know if there any ways to do it? I'll share how far I've got.

These are my wrappers for when f32 values are involved:

    /// Receive a string, try to parse it as f32
    pub(crate) fn check_f32(value: &str) -> f32 {
        let num: f32 = value.parse::<f32>()
            .expect(&format!("ERROR: `{}` is not a valid f32 number", value));
        num
    }
    /// Receive a string, try to parse it as f32 between a given range
    pub(crate) fn check_f32_between(value: &str, min: f32, max: f32) -> Option<f32> {
        let num = check_f32(value);
        match num {
            num if (min..=max).contains(&num) => Some(num),
            _ => None
        }
    }

This is how I use them:

    "pan" => {
        let num = check_f32_between(value, 0., 100.);
        num.and(Some(Opcode::pan(num.unwrap_or_default())))

        // My intention is to replace that with something briefer like:
        // f32::check(value, 0., 100., Opcode::pan)
    }

And this is where I'm stuck:

    // trying to make this work...
    impl<T: utils::OpcodeType<T>> Opcode {
        fn replace(self, value: T) -> Self {
            Self(value)
        }
    }

    pub trait OpcodeType<T> {
        fn check(value: &str, min: T, max: T, opcode: Opcode) -> Option<Opcode>;
    }
    impl OpcodeType<f32> for f32 {
        fn check(value: &str, min: f32, max: f32, opcode: Opcode) -> Option<Opcode> {
            let num = utils::check_f32_between(value, min, max);
            num.and(Some(opcode.replace(num.unwrap_or_default())))
        }
    }

If pan has a contained value (it's like pan(f32),) then Opcode::pan isn't an Opcode, it's a fn(f32) -> Opcode. I don't quite understand what existing value you're trying to replace; if you're just constructing new Opcodes, you could do

pub trait ToOpcode: Sized {
    fn check<F>(value: &str, min: Self, max: Self, wrapper: F) -> Option<Opcode>
    where
        F: Fn(Self) -> Opcode;
}

impl ToOpcode for f32 {
    fn check<F>(value: &str, min: f32, max: f32, wrapper: F) -> Option<Opcode>
    where
        F: Fn(Self) -> Opcode,
    {
        utils::check_f32_between(value, min, max).map(wrapper)
    }
}

// ...
"pan" => f32::check(value, 0., 100., Opcode::pan)

Though in that case you probably don't need the trait, since you can just as easily do

"pan" => utils::check_f32_between(value, min, max).map(Opcode::pan)
2 Likes

Aha! That was an illuminating moment for me... I didn't consider looking at it that way. In fact I feel like I learn that for the first time. Do you know by any chance if that is explained in the Rust docs, and where, or how did you learn it? I can't find anything regarding this fact...

me neither, I was just confused on how it really worked.

Aaaand this is absolutely perfect! Infinite thanks for you my friend!

It's evident from the (unique) manner it is possible to use it. If you can say Foo::Bar(42) and this expression makes a Foo, then Foo::Bar must be a callable that takes an integer and returns a Foo.

1 Like

Thank you. It just blows my mind. Indeed it seems evident in retrospective. Rust has a way of finding my programming blind-spots, and making me deal with them...

Another way to think about it is this: if Foo is a type, and one of its variants is Foo::Bar(u32), and Foo::Bar(42) constructs a Foo, then the Foo type (when constructed using Foo::Bar()) is only complete with the integer inside it. Therefore, Foo::Bar is missing something (the integer inner data) – so it can't (yet) be a proper Foo itself.

1 Like

Yes!

Yeah, that is similar to my original reasoning, when I assumed I would have to create a complete type in order to pass it to the function that then would have to replace its value.

The gamechanger part for was finally recognizing it as a function in its own right, without being blinded by the enum label, therefore being able to be used in a map... Simply genious.

That should be taught somewhere, maybe in rust by example?

It is briefly mentioned in the enum variant expressions of the rust references and the relevant tuple struct expression syntax

1 Like