Pre-RFC: Marker types for making parameters invariant

Some remarks:

  1. This behaves the same with newtypes;

  2. Given that @RalfJung's examples used a single lifetime parameter in both non-covariant (arg spot) and non-contravariant (return type) spot, doesn't it imply invariance? The examples you gave used two distinct lifetimes parameters ('a and 'b) which showed indeed that in that case each parameter can have its own variance rules;

  3. *mut T : !Send + !Sync

    Hence using *mut T over fn(Box<T>)-> Box<T> would require adding:

    unsafe impl<T : ?Sized> Send for PhantomInvariant<T> {}
    unsafe impl<T : ?Sized> Sync for PhantomInvariant<T> {}

    (Which is not a bad thing per se, specially if the non-covariant + non-contravariant stuff somehow turned out to not always imply invariance)

  4. &'_ mut T requires having a lifetime "outlived" by T, which is not possible if we want our PhantomInvariant to have only one generic unconstrained parameter T (unless we had some lifetime literal for that, e.g. an 'empty/'nil lifetime).

Note that _ expressions are also a desirable feature for destructuring assignment, and I’m not sure how compatible these two different meanings are for it.

let x;
(x, _) = func();

At first glance I don’t see any serious conflicts between the two meanings (the meaning proposed here could always be suppressed in the LHS of an assignment), but I’ve seen more surprising conflicts in the past.

What might be a simpler solution (a libs solution rather than a lang solution) is ConstDefault or <T: const Default>, possibly reexported as a generic const.

Actually, I take this idea back. If we’re going to give _: T in structs a magic meaning, then it should behave exactly like _marker: PhantomData<T> behaves today, except as before

  • _ cannot be initialized in struct expressions or extracted in struct patterns, or accessed with a dot.
  • _ can be repeated.

But really it feels silly to leave out tuple structs at this point, so maybe we should just introduce the syntax struct K { phantom T, u: U } (note: no colon), where phantom T is the same as _: T above, except struct K(phantom T); is now reasonable, too. (Syntactically it’s something like a visibility?) PhantomData is no longer a lang item and is instead defined as struct PhantomData<T>(phantom T);

Bonus extra-silly footgunny version: introduce the phantom visibility, introduce _ as a valid field name (with the semantics of not-accessible-and-no-drop-glue, behaving like a inaccessible [u8; size_of::<T>()]; you could now instead define PhantomData as

struct Phantom<T> {
  phantom _: T,

As a bonus, you get a slightly cleaner way to play stupid games with transmute

// Must fit in %eax for the purposes of sysexit
struct SyscallErrorPadded {
  inner_err: Option<SyscallError>,
  _: [u8; size_of::<u32>() - size_of::<SyscallError>()],
// ...
asm!("sysexit" :: "{eax}"(transmute::<_, u32>(SyscallErrorPadded{ inner_err: err }) ));
1 Like

I really like the idea of having such types in the core library.

Personally I have those typedefs:

pub type PhantomVariant<T> = PhantomData<fn() -> T>;
pub type PhantomInvariant<T> = PhantomData<fn(T) -> T>;
pub type PhantomContravariant<T> = PhantomData<fn(T)>;
pub type LifetimeVariant<'a> = PhantomData<&'a ()>;
pub type LifetimeInvariant<'a> = PhantomData<&'a mut ()>;

using type instead of a newtype has the huge advantage in initializing a struct: _marker: PhantomData, which is much more comfortable than writing PhantomContravariant::default()

Well, you are then indeed a good example of the OP's point: your LifetimeInvariant definition is wrong! (and your other definitions require that T be Sized, but that's acceptable for custom typedefs, given the unlikelyhood of using these type aliases with DSTs).

1 Like

Then I'm glad I posted this!


For reference LifetimeInvariant would be:

pub type LifetimeInvariant<'a> = PhantomData<&'a mut &'a ()>;

Because the T in &mut T is invariant.

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