`Phantom` "Trait"

What problem does this solve or what need does it fill?

The PhantomData struct is very useful. It often allows for the creation of safe and abstract API.

But it has two downsides:

  1. Boilerplate, adding a _marker: PhantomData tends to be messy.
  2. Makes the following pattern impossible:
struct Foo<T>; // Where T is "Phantom"

The second is a bit niche, but I have found myself needing it recently

What solution would you like?

A Phantom trait (with special compiler privileges), If a generic type T is specified to implement that, than the compiler would treat the struct (enum / ..) as though it stores a value of T, even though it doesn't really.

struct Foo<T: Phantom>; // Now possible

Currently, for example:

struct Bytes<'a, T>{
   bytes: Box<[u8]>,
   _marker: PhantomData<T>,
   _lt_marker: PhantomData<&'a ()>
}

// ...

fn as_bytes<'a, T: 'a>(val: T) -> Bytes<'a, T> {
    Bytes {
        bytes: raw_bytes(val),
        _marker: PhantomData,
        _lt_marker: PhantomData,
    }
}

With Phantom trait:

struct Bytes<'a, T: Phantom + 'a> where &'a (): Phantom {
    bytes: Box<[u8]>
}

// ...

fn as_bytes<'a, T: 'a>(val: T) -> Bytes<'a, T> {
    Bytes {
        bytes: raw_bytes(val)
    }
}

Phantom will be an auto trait (or just syntactic sugar in the shape of a trait?)

What alternative(s) have you considered?

Well, as I mentioned, it seems right now there is actually a scenario that's impossible to express. Other than that, just use the current PhantomData struct.

Additional context

lifetimes are a bit more tricky because you often have to go through the &'a () shenanigans.

2 Likes

AFAIK, a PhantomData with ..Default::default() in the constructor may suits your need.

use core::marker::PhantomData;
trait Phantom{}
impl<T> Phantom for PhantomData<T>{}
impl<T: Phantom> Phantom for Foo<T>{}
fn foo<T:Phantom>(_arg:T){
    println!("{}",core::any::type_name::<T>())
}
#[derive(Default)]
struct Foo<T:Phantom>(T);

fn main(){
    let a=PhantomData::<&mut i32>;
    foo(a);
    let b=Foo::<PhantomData<&mut PhantomData<&Foo<PhantomData<&mut i32>>>>>{..Default::default()};
    foo(b)
}

Your primary argument appears to be that using PhantomData precludes a struct from being a "plain ZST".

struct Bar;
// Works
let b = Bar;

struct Foo<T>(PhantomData<T>);
// Doesn't work
let f: Foo<u8> = Foo;
// Have to do this instead
let f: Foo<u8> = Foo::new();

The generic_const_items feature could fulfill that role:

struct Foo<T> { _ph: PhantomData<T> }
// This works because consts and types
// live in different namespaces
const Foo<T>: Foo<T> = Foo { _ph: PhantomData };
// Okay
let f: Foo<u8> = Foo;

playground

4 Likes

Thanks, I'll keep that in mind!

I want the Phantom trait to reduce boilerplate - to not have to create a messy field for it.

1 Like

Another use case is structs with public fields such as the types in the euclid library. It's annoying to have to write the phantom _unit field, especially in patterns which can't be replaced with functions.

That said, I would personally prefer a way to explicitly specify variance and auto traits for a generic: I think that would be less likely to cause subtle errors via not thinking about every factor.

4 Likes

So the field is messy and is often boilerplate, but it is also specifying something that the generic parameter alone doesn’t: variance. (This example is written in terms of lifetimes, but it applies to type parameters too.) Being able to omit the PhantomData for a type parameter would imply picking a default variance. Which would probably be “invariant”, but then if that’s the wrong choice, you can’t change it (in a non-source-breaking way) without introducing more new syntax, or using the const trick shown above.

[EDIT: jinx!]

1 Like

No need for nightly features; enum and a bit of re-export trickery is sufficient.

https://crates.io/crates/ghost

4 Likes

This feels very footgunny, especially since this doesn't seem to have a way to express the equivalent of PhantomData<fn() -> T> and the like.

Also, bounding by a trait seems unintuitive. Why would restricting (but actually not!) the types that T can assume give it somehow a variance?

For this though I’d just want to see a way for a type to opt in to pattern matching/destructuring its public fields even if it also has private (trivial, Copy) parts. And struct field default initializer support, should it happen someday, would handle the construction side of things.

2 Likes

The reason for requiring PhantomData is that it enables "by example" variance. Thus for the majority of cases, code doesn't need to directly consider variance, and can instead say "I own T" (PhantomData<T>), "I borrow T" (PhantomData<&[mut] T>), "I produce T (PhantomData<fn() -> T>), "I consume T" (PhantomData<fn(T)>), or some combination of those.

If we wish to avoid using PhantomData, I would expect this to happen with explicit variance annotations, e.g. struct Foo<#[covariant] T>;. This would remove the requirement to otherwise constrain T in the type definition[1], as well as limiting structural usage of T to usages which are compatible with said variance. It's a distinctly power-user feature for variance-critical unsafe trickery, but it does enable what OP wants.

However, to note, PhantomData is already specially treated by lints — the unused fields lint ignores fields of type PhantomData even if they don't have _-prefixed names. I feel like a better approach to the issue felt by the OP would be extending this idea and allowing complete elision of (named or tail) PhantomData-typed fields in patterns and literals. The field would still exist, but you'd be permitted to skip mentioning it outside of the type definition. It doesn't get you all the way to being a unit struct, but it's most of the desirable properties.

In destruction, such functionally just omits the .. rest pattern (and checks exhaustiveness of the other fields). In construction, it's essentially a specialized subset of field-level defaults (and omits .. if that's gets used to indicate defaults are getting filled in).


Aside: for implementations of a typestate pattern, PhantomData<T> can often be preferable because it allows you to actually transmute and/or cast between states. But if you aren't transmuting, using the related "strategy" pattern where you own T and any trait methods implementing the strategy take &self is often preferable, and is "zero cost" for ZST strategy types.


On nightly, using PhantomData also has implications for the dropck eyepatch. This can have amusing effects even on stable in that PhantomData<NeedsDrop> is a Copy type with mem::needs_drop() == false which is nonetheless treated as having drop glue. (I don't recall the exact example, but it has to do with non-lexical lifetime inference.) But other than to mention that it exists, this isn't particularly relevant, as the dropck eyepatch remains kinda halfbaked for the time being.


  1. It doesn't necessarily have to constrained by a field, if it's constrained by an associated type! Such a parameter is bivariant if that's the only constraint. ↩︎

4 Likes

How is that "variance" ever meaningful semantics-wise?

  • If the type owns or borrows T, it does not need PhantomData. It already has fields of type T.
  • Producing and consuming T is not a property of the type itself - it's a property of its methods.

It can't matter in purely safe code, because variance is structurally inferred. It's present in safe code, but you typically don't need to worry about it. Cases with "functionlike" (contra)variance are even rarer, and generally amount to the type being/storing functions, e.g. fn() -> T.

The use of phantom type arguments as markers has been popularized to the point of being the most common usage of PhantomData, but the "standard" usage is wrapping some kind of type erasure, usually utilizing some unsafe. Consider for example a type like

struct AnyTypeSoLongAsIts<T> {
    data: Box<dyn Any>,
    marker: PhantomData<Box<T>>,
}

Utilizing a statically typed shell but a progressively/dynamically typed core is a common pattern in unsafely implemented data structures.

As a hopefully illustrative example, consider channels:

// okay, `fn(T)` is covariant in T
fn lengthen_func_arg<'a>(x: fn(&'a i32)) -> fn(&'static i32) { x }
// error, Sender<T> is invariant in T
fn lengthen_chan_snd<'a>(x: Sender<&'a i32>) -> Sender<&'static T> { x }

// okay, `fn() -> T` is contravariant in T
fn shorten_func_ret<'a>(x: fn() -> &'static i32) -> fn() -> &'a i32 { x }
// error, Receiver<T> is invariant in T
fn shorten_chan_rcv<'a>(x: Receiver<&'static i32>) -> Receiver<&'a T> { x }

(This is required for the soundness of the current impl. However, in the abstract, it's an artificial side-effect of the implementation using essentially Arc<Shared<T>>, and with a sufficiently careful impl[1], the channel halves could be made to be variant.)


  1. The important part is that if Sender can ever drop T, it's also functioning as a receiver during said Drop. Since potentially-variant usages are restrictions, the opposed variance restrictions combine into invariance. So long as Receiver only exposes the ability to remove items from the shared handoff location (includes dropping them) and never stores any to the location, it could be contravariant. ↩︎

1 Like

I think that claim is a bit strong. If you're in a situation where PhantomData is needed (which can happen in safe code), then you can always overconstrain the variance, and make it invariant where it should be covariant or contravariant. You can also overconstrain auto traits, making something !Send or !Sync where it doesn't need to be.

An alternative formulation that might be more (or less) acceptable this made me think of is "anonymous fields":

struct Foo<T> {
  id: u32,
  _: PhantomData<T>,
}

let foo: Foo<String> { id: 123 }

The difference is there's less magic in the field type, but it would still need something, but perhaps just a Default bound, which might be useful outside of PhantomData?

4 Likes

I like this idea, it would also be useful for PhantomPinned. Not sure it would be very useful apart from those two though.

One note: You would need to be able to add multiple such ignored fields as well (PhantomData + PhantomPinned or multiple different PhantomData).

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