Idiomatic definition of uninhabited (never) newtypes

When #![feature(never_type)] is active, the clippy::empty_enum lint documentation says

… you should use ! (the primitive type “never”), or a wrapper around it, because ! has more extensive compiler support (type inference, etc…) and wrappers around it are the conventional way to define an uninhabited type.

I agree that ! should be used bare when the type does not matter and only its uninhabitedness does. However, in the case where a new type is wanted, I am not aware of any “conventional” claims anywhere but this documentation that struct Foo(!) is preferred over enum Foo {}. I can think of some such reasons, but they are weak:

  • Using Foo(!) is more comprehensible, because “newtype of !” communicates “has the characteristics of ! but is a different type”, so the documentation of ! can be consulted.
  • Understanding the effect of an empty enum requires thinking about the characteristics of empty sets, which is unintuitive to many non-mathematicians.
  • structs are more “plain” types than enums, so when it doesn't matter, it's better to define a struct and have it listed in the “structs” part of the documentation than the “enums” part.
  • struct Foo(!) offers a free conversion function from ! to Foo.

And there are concrete disadvantages:

  • pub struct Foo(!) is documented by rustdoc as pub struct Foo(/* private fields */);, which hides both the fact that it's uninhabited and that it contains no other fields (which might affect auto traits). Of course, pub struct Foo(pub !); fixes that.
  • The compiler currently gives unreachable_code warnings when deriving traits for such structs. This is a bug, but a long-unfixed one.

I'm worried that once ! is stable (which might happen with or shortly after the 2024 edition), this lint suddenly being active (for clippy::pedantic users, at least) will cause pointless churn, or even changes for the worse, in code that currently uses DIY uninhabited types on stable.

So, in this thread, I would like to hear any:

  • Evidence for the claim that the ! newtype is more “conventional” than the empty enum
  • Stronger reasons than the above for choosing the newtype
  • Links to any prior discussions on this topic

I can then use this information to improve the documentation of clippy::empty_enum.

3 Likes

From my view, nothing that requires an unstable feature is conventional. It's conventional to use an empty enum for an uninhabited type, because that's the only stable way to do it.[1]

The current wording reads as aspirational to me. I'm reminded of when the Arc documentation got updated to declare Arc::clone(arc) more idiomatic than arc.clone(), and then eventually got updated to remove that wording for reasons including not actually being true.

It is hard to figure out how many calls there are of the .clone() form, but there are 1550 uses of Arc and only 65 uses of Arc::clone. The recommendation has existed for over two years.

IMO you could just drop the bolded portion that makes claims about what is conventional.


  1. And if ! was stable, I imagine most uses of empty enums would juse use !, not Foo(!). ↩︎

1 Like

Fair, but that’s a criticism of the phrasing, not the recommendation. The most important question is: in the world where ! is stable, when we want to define a new, uninhabited type, which of the two possible definitions should we pick, and why?

1 Like

Clarification: I understand the main point of the lint overall to be suggesting using bare ! instead of defining an empty enum, in cases when a newtype is not necessary. But my question is specifically about what to recommend in the case where a newtype is necessary.

I don't have a strong opinion on which is recommended (if any is recommended). I can buy Foo(!) being more obvious.

But it should be phrased as a recommendation, not as a counter-factual statement.

3 Likes

I posted what I intended to be a “FYI this thread exists” message to Zulip, and the discussion there ended up being much more active instead. So now you get the crosspost FYI.

https://lib.rs/crates/never-say-never :slight_smile:

2 Likes

As touched on in the Zulip thread, I think what the clippy lint is getting at w.r.t. compiler support is the coercions from !, meaning you can do e.0 to get at and coerce the ! instead of needing to write match e {}. But I also think other properties of ! and !-having structures have been extended to all visibly uninhabited types in general since that lint was first written, so a good portion of the original motivation doesn't exist anymore.

Perhaps more relevant to today, when a newtype isn't needed, using ! directly is more obviously beneficial. Using a newtype around ! is an acknowledgement that the newtype is desired rather than being a leftover empty enum from before access to !. The pedantic category is accepting of some false negatives to catch the positives, and #[allow] is just as accepted a way to acknowledge that the lint doesn't apply here, you did mean to do that.

There's also a very slight preference for markers to show up under the "structures" doc section instead of the "enumerations" section, because then you don't have to process what variants the type has (none), only see that the type exists without any attached functionality. You can argue either way whether a unit struct or an empty enum is "simpler" and less cognitive overhead for pure typestate markers. That the type is or isn't uninhabited only really matters when there exist code paths or data layout that observe a potential instance of the type. (It's also a bit easier to work with unit structs than requiring use of a phantom data wrapper, when either work. For related reasons I prefer "strategy" pattern to "typestate" when typestate doesn't get reinterpret cast between.)

2 Likes

I think this is fine because when the field is private the compiler does not do the special treatment:

Of course, this is only about behavior reflected in the semantics - the compiler is probably allowed to do internal optimizations based on the hidden never field. But this doesn't matter either because it's okay for internal optimizations to not be reflected in the generated docs.

I'm talking about what is documented, not what code can be written. Understanding that a public type is uninhabited may be an important part of understanding how it should be used. These are publicly uninhabited:

  • pub struct Foo(pub !);
  • pub enum Foo {}

These are not:

  • pub struct Foo(!);
  • #[non_exhaustive] pub enum Foo {} (sort of)

So, if one switches from pub enum Foo {} to pub struct Foo(!);, one is switching from “publicly uninhabited” to “privately uninhabited”. This might be done on purpose, but it's a potentially surprising change that's not mentioned in the Clippy lint docs, which is why I mentioned it.

1 Like

These two concepts should be tightly coupled. Consider this:

mod my_types {
    pub struct Foo(!);
    pub struct Bar(());
}
use my_types::*;

Outside my_types, Foo and Bar are the same. There is no way to create a Foo, yes, but because Bar has a private field and we have provided no functions that return it - there is no way to create a Bar either (aside from the unsafe transmute, of course - but you can transmute a Foo just as well). And because the compiler cannot use variants that include Foo when considering exhaustivity - they are the same from that aspect as well.

Since the code that can be written with them is the same, it makes perfect sense that the documentation generated for Foo is similar for the one generated for Bar, and they both hide their uninhabitability.


Now, regarding:

I think the main difference between them is that the latter is intentional. One would only add the #[non_exhastive] if they want to prevent users of the type from utilizing the uninhabitability. Foo(!), on the other hand, seems like something one could write by mistake.

I think this justifies a warning. Not a Clippy warning - a first class rustc warning.

Thanks for all your feedback. I've now posted a Clippy PR to improve the documentation: Rephrase and expand `empty_enum` documentation. by kpreid · Pull Request #12833 · rust-lang/rust-clippy · GitHub

6 Likes

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