Shouldn't it be possible to #![allow(non_exhaustive)]

An example of a crate that uses #[non_exhaustive] is target-lexicon. This crate is somewhat of a foundational crate, so updating to a new major version requires updating all crates that depend on it at the same time. The total set of targets, architectures, operating systems, ... is also by definition non-exhaustive. You should never attempt an exhaustive match on any of these things as you are guaranteed to miss cases. For example you will miss "SomeObscureOS" or "ThisReallyOldCpuArchitecture", even if target-lexicon itself doesn't support it yet. For these reasons #[non_exhaustive] is the right option for target-lexicon.

3 Likes

I don't think I agree with this.

The only valid use-case I can think of is for enums taken as input. In this case the caller never needs to match on it, and removing the promise that the enum never changes from the contract makes sense.

As soon as the enum is returned by a crate however, it should (almost) never have this attribute.

Basically, if the answer to the question "what can be done about new items" is failure to support something, it shouls not be non_exhaustive.

The thing here is that even most existing items will not be supported. For example Cranelift doesn't have a MIPS backend, so trying to use the MIPS architecture needs to return an error. The same would apply to any new architectures that target-lexicon adds. It is not possible for Cranelift to support a target without target-lexicon first supporting it, so new architectures will always need to be handled the same as existing unsupported architectures. In pseudo code:

match triple.architecture {
    X86_64 => X86_64Backend::new(),
    Aarch64 => Aarch64Backend::new(),
    _ => return Err("unsupported arch"), // matches both known unsupported archs as well as newly added archs
}
2 Likes

I think the issue here is is that there are conflicting but equally valid situations. The non_exhaustive behavior is great when you need downstream stability, like with any code you'll distribute to other parties. You want them to be aware that something might change, either depending on platform, or build configuration, or because new things come up.

The same is true when you have distributed code that depends on something else using non_exhaustive. You value that the dependency communicates the "volatility". You then have the choice to handle it directly, or propagate the non-exhaustiveness somehow. The kind of propagation of responsibility here feels similar to how unsafe invariants are propagated.

But the situation is a different one when you have in-house code that consumes the propagated volatility. There you want to be notified of the change as soon as it happens, and often before the code runs.

Let's imagine a situation:

  • There's a crate dealing with a specific data format some_data_format.
  • There is a crate fetching this kind of data from some canonical server find_some_data.
  • There is a company having an internal crate managing cached access to this data called our_data_access.
  • The same company also has a web app called our_data_website that uses the cached access.

Now, all of those should be happy with non_exhaustive. It gives the original some_data_format crate options for volatility. Everything downstream likes that they know of this, including the company projects.

But when a change does happen, the company can:

  • Manually re-check exhaustiveness after every update.
  • Show the user a generic error and debug after-the-fact through logs if the user lets you know.
  • Show the user a generic error but also integrate it with logging in a way that lets you know a new case showed up without needing user feedback.
  • Use the clippy lint to detect before the application runs that a new case has emerged. You could have your CI doing its part here.

So I'd say there is a good case for using the lint for good.

But also: clippy is a secondary tool. It wouldn't keep things from building by itself. But it is a great tool to ensure you aren't missing anything. And since it's secondary, it could even be useful for public crates to use from time to time where it makes sense. To make sure you haven't missed anything that's supposed to be exhaustive if and as soon as it can.

This doesn't seem like a good example to me. This is clearly a "method" of the enum, which should be provided by the library (or not, if it doesn't make sense). Implementing it in your crate is a workaround, which even in the exhaustive case only works because enums have no privacy. You can't, usually, implement fn map(fn(T) -> U, MyStruct<T>) -> MyStruct<U> in a downstream crate, because how would you even construct MyStruct<U>?

So more generally, it is reasonable to expect that in some cases, you can't (and don't need to) properly construct a foreign enum, just like you can't do it for a struct. IIRC, that is one original justification for this feature.

2 Likes

Perhaps, for something as simple as map. But maybe it's a hasher or other kind of visitor, perhaps it takes some auxiliary state that the upstream enum has no business knowing about, and ends up as an orphan requiring both MyUpstreamEnum and DownstreamTrait to implement.

If the foreign enum wasn't supposed to be constructible, it shouldn't have been public. There are lots of ways to make this explicit by using private structs inside enums or wrapping the enum in a struct with private fields. I'm assuming that this enum is part of the public interface of the crate, since I think that's the only reason you would have to worry about #[non_exhaustive].

By "constructible" I meant "manually constructible", i.e. specifying a variant and its fields, like is necessary for your map. Of course if it's public, there should probably be some way to get one, but not necessarily one that lets you build whichever you want. An example I can think of are Entry-like APIs. You definitely get instances of the enum, and you can sorta create them manually with one of the variants (if you got an OccupiedEntry or VacantEntry struct somehow), but you very rarely want to do that.

In a sense, constructing (or deconstructing) an enum directly using its variants is similar to constructing or deconstructing a struct with pub fields. It makes sense for some of them, but definitely not for all. For the others, even if you implement your new trait, you use the available public API. Unfourtunately, unlike structs, it is more difficult to mark parts of an enum as private. Wrapping it in a struct with a private field is a possible workaround, but making it completely opaque; #[non_exhaustive] is some point in the middle.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.