¿Cómo asegurar que cada variante enum pueda ser devuelta desde una función específica en tiempo de compilación?

8

Tengo una enumeración:

enum Operation {
    Add,
    Subtract,
}

impl Operation {
    fn from(s: &str) -> Result<Self, &str> {
        match s {
            "+" => Ok(Self::Add),
            "-" => Ok(Self::Subtract),
            _ => Err("Invalid operation"),
        }
    }
}

Quiero asegurar en tiempo de compilación que cada variante enum se maneja en la fromfunción.

¿Por qué necesito esto? Por ejemplo, podría agregar una Productoperación y olvidarme de manejar este caso en la fromfunción:

enum Operation {
    // ...
    Product,
}

impl Operation {
    fn from(s: &str) -> Result<Self, &str> {
        // No changes, I forgot to add a match arm for `Product`.
        match s {
            "+" => Ok(Self::Add),
            "-" => Ok(Self::Subtract),
            _ => Err("Invalid operation"),
        }
    }
}

¿Es posible garantizar que la expresión de coincidencia devuelva cada variante de una enumeración? Si no, ¿cuál es la mejor manera de imitar este comportamiento?

Oleh Misarosh
fuente

Respuestas:

13

Una solución sería generar toda la enumeración, variantes y brazos de traducción con una macro:

macro_rules! operations {
    (
        $($name:ident: $chr:expr)*
    ) => {
        #[derive(Debug)]
        pub enum Operation {
            $($name,)*
        }
        impl Operation {
            fn from(s: &str) -> Result<Self, &str> {
                match s {
                    $($chr => Ok(Self::$name),)*
                    _ => Err("Invalid operation"),
                }
            }
        }
    }
}

operations! {
    Add: "+"
    Subtract: "-"
}

De esta manera, agregar una variante es trivial y no puede olvidar un análisis. También es una solución muy SECA.

Es fácil extender esta construcción con otras funciones (por ejemplo, la traducción inversa) que seguramente necesitará más adelante y no tendrá que duplicar el análisis de caracteres.

patio de recreo

Denys Séguret
fuente
1
Dejaré mi respuesta, ¡pero definitivamente es mejor!
Peter Hall el
12

Si bien existe una forma complicada, y frágil, de inspeccionar su código con macros de procedimiento, una ruta mucho mejor es utilizar pruebas. Las pruebas son más robustas, mucho más rápidas de escribir y verificarán las circunstancias en las que se devuelve cada variante, no solo que aparezca en algún lugar.

Si le preocupa que las pruebas continúen pasando después de agregar nuevas variantes a la enumeración, puede usar una macro para asegurarse de que se prueben todos los casos:

#[derive(PartialEq, Debug)]
enum Operation {
    Add,
    Subtract,
}

impl Operation {
    fn from(s: &str) -> Result<Self, &str> {
        match s {
            "+" => Ok(Self::Add),
            "-" => Ok(Self::Subtract),
            _ => Err("Invalid operation"),
        }
    }
}

macro_rules! ensure_mapping {
    ($($str: literal => $variant: path),+ $(,)?) => {
        // assert that the given strings produce the expected variants
        $(assert_eq!(Operation::from($str), Ok($variant));)+

        // this generated fn will never be called but will produce a 
        // non-exhaustive pattern error if you've missed a variant
        fn check_all_covered(op: Operation) {
            match op {
                $($variant => {})+
            };
        }
    }
}

#[test]
fn all_variants_are_returned_by_from() {
   ensure_mapping! {
      "+" => Operation::Add,
       "-" => Operation::Subtract,
   }
}
Peter Hall
fuente