Discussion: Distinct Phantom types for different traits

Following up on this discussion thread on the NoCell/Freeze RFC.

How would we feel about moving in a direction in which we have distinct PhantomXxx types for different behavior? I.e.:

// These opt out of auto traits. `PhantomNotTrait<T>: !Trait` if `T: !Trait`.
struct PhantomNotSend<T>;
struct PhantomNotSync<T>;
// Alternative name: `PhantomCell`
struct PhantomNotNoCell<T>;

// PhantomCovariant et al from https://github.com/rust-lang/rust/issues/135806

// Acts as if owning a `T` for the purposes of drop checking.
struct PhantomDrops<T>;

// ...etc

IMO these are more readily understandable than having to know arcane library/language details like the semantics of PhantomData<Box<T>>.

3 Likes

For variance, these are implemented on nightly. I think it's more than reasonable to extend this to other markers. Though I'd prefer PhantomCell over the double negative :slightly_smiling_face:

4 Likes

Good find! The conclusion is in this comment:

We discussed this in the libs-api meeting last week. We would prefer a language-level solution based on negative impls which would allow you to explicitly implement !Send or !Sync for a type to disable the implicit trait implementation. We are pushing the lang team to partially stabilize this feature to enable this use case.

4 Likes

Hmmm, my understanding is that a negative impl is a semver promise to never implement the trait. There’s cases where you want to ensure a type is not-Send etc. to allow changing the implementation in the future, but you might relax that. I guess this could be a related impl ?Send kind of feature, but it’s not covered by the existing negative impls.

For that you could always do

struct NotSend;
impl !Send for NotSend {}

struct Foo {
    // prevents Foo from automatically being Send,
    // but we're free to remove it later
    marker: NotSend,
}

Now, this is sort of back where we started with marker types, but the local definition at least makes it quite obvious.

4 Likes

Yeah, that's sometimes called "reserved impls", and we would very much like it to exist as well.

I want to replace on_unimplemented with it, for example -- then if you try to use it we could say "That's not implemented today, but the crate has reserved the possibility to implement it in future. Here's the doc-comment they wrote about it".

7 Likes

So I was about to say that this is not a new idea, but your idea seems actually different. You don't just want PhantomNotSend (a non-generic type that is never Send); you want to express something conditional on some other type T.

IMO the right way to do that is with impls, that seems a lot less magical than the types you are proposing:

struct MyType<T> {
  data: *mut T,
  _nosend: PhantomNotSend,
}

unsafe impl<T: Send> Send for MyType <T> {}

I am surprised by that conclusion given that PhantomPinned exists...

Regarding a generic approach:

PhantomData<dyn Every + AutoTrait + ButForTheOne + WeDoNotWant>

This is already usable, and also, imho, rather ugly; I guess the ergonomic approach would have to require a magic type:

PhantomNot<dyn AutoTrait>

for instance?


Regarding negative impls,

already shows that it is possible to make this promise of never-to-impl a trait in stable Rust.

As a matter of fact, the docs of that crate get to showcase how, currently, stuff such as PhantomPinned being "only ?Unpin" w.r.t. coherence is actually limiting.


So the solution here, for now, could be to allow writing proper negative impls, at least for non-pub types (if we are that worried about library authors misusing negative impls in public APIs).

The approach in negative is interesting:

The macro basically expands

unsafe impl !Sync for Foo {}

to

unsafe impl Sync for Foo where for<'a> [()]: Sized {}

(only with an unicode symbol instead of 'a for the lifetime to make a name collision unlikely and with a fully qualified path to Sized)

With the HRTB the trivially false bound [()]: Sized is not caught by the compiler at the definition site, but only once the bound is evaluated when trying to check if Foo implements Sync.

3 Likes