Search code examples
genericstypesenumsrustvariant

Why do I have to destruct and reconstruct the non-generic variant of a generic enum when mapping from one generic to another?


I have a generic enum where only one variant makes use of the generic type.

When “converting” a ParseResult<T> to a ParseResult<U> the compiler forces me to destruct the non-generic part, too. The non-generic part is immediately reassembled and returned in exactly the same way.

Is there a more elegant solution without the code duplication?

#[derive(PartialEq, Debug)]
enum ParseResult<'a, T> {
    Success(T, &'a str),
    Failure(&'static str),
}

fn char<'a>(c: char) -> impl Fn(&'a str) -> ParseResult<'a, char> {
    move |input| match input.chars().next() {
        Some(candidate) if c == candidate => ParseResult::Success(candidate, &input[1..]),
        Some(_) => ParseResult::Failure("chars didn't match"),
        None => ParseResult::Failure("unexpected end of stream"),
    }
}

fn or<'a, T>(
    mut lhs: impl FnMut(&'a str) -> ParseResult<T>,
    mut rhs: impl FnMut(&'a str) -> ParseResult<T>,
) -> impl FnMut(&'a str) -> ParseResult<T> {
    move |input| match lhs(input) {
        ParseResult::Failure(_) => rhs(input),
        ok => ok,
    }
}

fn and<'a, T, U>(
    mut lhs: impl FnMut(&'a str) -> ParseResult<T>,
    mut rhs: impl FnMut(&'a str) -> ParseResult<U>,
) -> impl FnMut(&'a str) -> ParseResult<(T, U)> {
    move |input| match lhs(input) {
        ParseResult::Success(left, after) => match rhs(after) {
            ParseResult::Success(right, after) => ParseResult::Success((left, right), after),
            // Only explicit works:
            ParseResult::Failure(why) => ParseResult::Failure(why),
        },
        // Same as line as above, same goes here.
        ParseResult::Failure(why) => ParseResult::Failure(why),
    }
}

Things that didn't work instead of the two ::Failure lines towards the end:

  1. bad => bad, expected tuple, found type parameter U
  2. bad => bad as ParseResult<(T, U)>,
  3. bad @ ParseResult::Failure(why) => bad, expected tuple, found type parameter U

Playground


Solution

  • But the non-generic part is immediately reassembled and returned in exactly the same way. Isn't there a smarter/more elegant solution without the code duplication?

    It looks like it is reassembled the same way, but it isn't because it changed type from ParseResult<T> to ParseResult<U> (or ParseResult<(T, U)> in your case). These have different sizes so ParseResult<T>::Failed(err) is not the same as ParseResult<U>::Failed(err).

    If we look at how the standard library handles this problem for Result::map, you can see that the same pattern is used:

    match self {
         Ok(t) => Ok(op(t)),
         Err(e) => Err(e),
    }