3 ms·
Note that the stance of the OP here is broadly in agreement with what Rust does. His main objection is this: > The word “other” means “not mentioned elsewhere”
by kibwen 2y ago
Note that the stance of the OP here is broadly in agreement with what Rust does. His main objection is this:
> The word “other” means “not mentioned elsewhere”, so the presence of an Other logically implies that the enumeration is exhaustive.
In Rust, because all enums are exhaustive by default and exhaustive matching is enforced by the compiler, there is no risk of this sort of confusion. And then the fact that his proposed solution is:
> Just document that the enumeration is open-ended
The non_exhaustive attribute is effectively compiler-enforced documentation; users now cannot forget to treat the enum as open-ended.
Of course, adding non_exhaustive to Rust was not without its own detractors; it usage for any given enum fundamentally means shifting power away from library consumers (who lose the ability to guarantee exhaustive matching) and towards library authors (who gain the ability to evolve their API without causing guaranteed compilation errors in all of their users (which some users desire!)). As such, the guidance is that it should be used sparingly, mostly for things like error types. But that's an argument against open-ended enums in general, not against the mechanisms we use to achieve those (which, as you say, was already possible in Rust via hacks).
- tyre 2y agoMaybe there should be a compiler option or function to assert that a match is exhaustive. If the match does not handle a defined case, it blows up.
- aecsocket 2y agoRust already asserts that a match is exhaustive at compile time - if you don't include a branch for each option, it will fail to compile. This extends to integer range matching and string matching as well. It's just that with #[non_exhaustive], you must specify a default branch (`_ => { .. }`), even if you've already explicitly matched on all the values. The idea being that you've written code which matches on all the values which exist right now, but the library author is free to add new variants without breaking your code - since it's now your responsibility as a user of the library to handle the default case.
- ffminus 2y agoLibrary users can force a compile error when new variants get added, using a lint from rustc. It's "allow" by default, so it's opt-in. https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html#non-exhaustive-omitted-patterns https://doc.rust-lang.org/rustc/lints/listing/allowed-by-def...
- WiSaGaN 2y agoDoes this require nightly? If so, #[warn(clippy::wildcard_enum_match_arm)] will do the samething but no need for nightly, and from clippy instead of rustc natively.
- codetrotter 2y agoThat's pretty neat. I still don't completely understand why #[non_exhaustive] is so desirable in the first place though. Let's say I am using a crate called zoo-bar. Let's say this crate is not using non-exhaustive. In my code where I use this crate I do: let my_workplace = zoo_bar::ZooBar::new(); let mut animal_pens_iter = my_workplace.hungry_animals.iter(); while let Some(ap) = animal_pens_iter.next() { match ap { zoo_bar::AnimalPen::Tigers => { me.go_feed_tigers(&mut raw_meat_that_tigers_like_stock).await?; } zoo_bar::AnimalPen::Elephants => { me.go_feed_elephants(&mut peanut_stock).await?; } } } I update or upgrade the zoo-bar dependency and there's a new enum variant of AnimalPens called Monkeys. Great! I get a compile error and I update my code to feed the monkeys. diff --git a/src/main.rs b/src/main.rs index 202c10c..425d649 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,5 +10,8 @@ zoo_bar::AnimalPen::Elephants => { me.go_feed_elephants(&mut peanut_stock).await?; } + zoo_bar::AnimalPen::Monkeys => { + me.go_feed_monkeys(&mut banana_stock).await?; + } } } Now let's say instead that the AnimalPen enum was marked non-exhaustive. So I'm forced to have a default match arm. In this alternate universe I start off with: let my_workplace = zoo_bar::ZooBar::new(); let mut animal_pens_iter = my_workplace.hungry_animals.iter(); while let Some(ap) = animal_pens_iter.next() { match ap { zoo_bar::AnimalPen::Tigers => { me.go_feed_tigers(&mut raw_meat_that_tigers_like_stock).await?; } zoo_bar::AnimalPen::Elephants => { me.go_feed_elephants(&mut peanut_stock).await?; } _ => { eprintln!("Whoops! I sure hope someone notices this default match in the logs and goes and updates the code."); } } } When the monkeys are added, and I update or upgrade the dependency on zoo-bar, I don't notice the warning in the logs right away after we deploy to prod. Because the logs contain too many things no one can go and read everything. One week passes and then we have a monkey starving incident at work. After careful review we realize that it was due to the default match arm and we forgot to update our program. So we learn from the terrible catastrophe with the monkeys and I update my code using the attributes from your link. diff --git a/src/main.rs b/src/main.rs index e01fcd1..aab0112 100644 --- a/wp/src/main.rs +++ b/wp/src/main.rs @@ -1,3 +1,5 @@ +#![feature(non_exhaustive_omitted_patterns_lint)] + use std::error::Error; #[tokio::main] @@ -11,6 +13,7 @@ async fn main() -> anyhow::Result<()> { let mut animal_pens_iter = my_workplace.hungry_animals.iter(); while let Some(ap) = animal_pens_iter.next() { + #[warn(non_exhaustive_omitted_patterns)] match ap { zoo_bar::AnimalPen::Tigers => { me.go_feed_tigers(&mut raw_meat_that_tigers_like_stock).await?; @@ -18,8 +21,12 @@ async fn main() -> anyhow::Result<()> { zoo_bar::AnimalPen::Elephants => { me.go_feed_elephants(&mut peanut_stock).await?; } + zoo_bar::AnimalPen::Monkeys => { + // Our monkeys died before we started using proper attributes. If they are hungry it means they have turned into zombies :O + me.alert_authorities_about_potential_outbreak_of_zombie_monkeys().await?; + } _ => { - eprintln!("Whoops! I sure hope someone notices this default match in the logs and goes and updates the code."); + unreachable!("We have an attribute that is supposed to tell us if there were any unmatched new variants."); } } } And next time we update or upgrade the crate version to latest, another new variant exists, but thanks to your tip we get a lint warning and we happily update our code so that we won't have more starving animals. diff --git a/wp/src/main.rs b/wp/src/main.rs index aab0112..4fc4041 100644 --- a/wp/src/main.rs +++ b/wp/src/main.rs @@ -25,6 +25,9 @@ async fn main() -> anyhow::Result<()> { // Our monkeys died before we started using proper attributes. If they are hungry it means they have turned into zombies :O me.alert_authorities_about_potential_outbreak_of_zombie_monkeys().await?; } + zoo_bar::AnimalPen::Capybaras => { + me.go_feed_capybaras(&mut whatever_the_heck_capybaras_eat_stock).await?; + } _ => { unreachable!("We have an attribute that is supposed to tell us if there were any unmatched new variants."); } But what was the advantage of marking the enum as #[non_exhaustive] in the first place?