Como garantir que toda variante de enumeração possa ser retornada de uma função específica no momento da compilação?

8

Eu tenho um enum:

enum Operation {
    Add,
    Subtract,
}

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

Desejo garantir, em tempo de compilação, que todas as variantes de enumeração sejam tratadas na fromfunção.

Por que eu preciso disso? Por exemplo, eu posso adicionar uma Productoperação e esquecer de lidar com este caso na fromfunção:

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"),
        }
    }
}

É possível garantir que a expressão de correspondência retorne todas as variantes de um enum? Caso contrário, qual é a melhor maneira de imitar esse comportamento?

Oleh Misarosh
fonte

Respostas:

13

Uma solução seria gerar toda a enumeração, variantes e braços de tradução com uma 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: "-"
}

Dessa forma, adicionar uma variante é trivial e você não pode esquecer uma análise. É também uma solução muito SECA.

É fácil estender essa construção com outras funções (por exemplo, a tradução inversa) que você certamente precisará mais tarde e não precisará duplicar o caractere de análise.

Parque infantil

Denys Séguret
fonte
1
Vou deixar minha resposta, mas isso é definitivamente melhor!
6139 Peter Hall
12

Embora exista uma maneira complicada - e frágil - de inspecionar seu código com macros procedurais, um caminho muito melhor é usar testes. Os testes são mais robustos, muito mais rápidos de escrever e verificarão as circunstâncias nas quais cada variante é retornada, não apenas que ela aparece em algum lugar.

Se você estiver preocupado com a possibilidade de os testes continuarem após adicionar novas variantes à enumeração, use uma macro para garantir que todos os casos sejam testados:

#[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
fonte