PhantomData Debug Derive Macro

Currently the Debug output provided by implementing the Debug trait via a Derive macro is a bit unhelpful. For example the following code:

use std::marker::PhantomData;

fn main() {
    println!("{:?}", Foo::<i32>::new());
    println!("{:?}", Foo::<i64>::new());
    println!("{:?}", Foo::<usize>::new());
}

#[derive(Debug)]
struct Foo<T> {
    phantom: PhantomData<T>,
}

impl<T> Foo<T> {
    fn new() -> Self {
        Self {
            phantom: PhantomData,
        }
    }
}

produces this output:

Foo { phantom: PhantomData }
Foo { phantom: PhantomData }
Foo { phantom: PhantomData }

I think the following output would be more helpful:

Foo { phantom: PhantomData<i32> }
Foo { phantom: PhantomData<i64> }
Foo { phantom: PhantomData<usize> }

It may also be helpful in the general case (not specifically PhantomData) but I can see an argument for it becoming a bit too verbose on complex types. I believe PhantomData is an exception since it only has one "value" (kinda) and the real info is sitting the type.

Thoughts? Concerns?

Usually, when Debug-printing something, it is assumed that the type of the thing being printed is known, so the output only needs to be concerned with representing the value. E.g. 0_usize or 0_i32 or 0_u8 all also simply print as โ€œ0โ€ without additional type information.

If you want to print the type of something, you can use compile-time tools such as IDEs, or if you want a run-time print-out, use API such as type_name_of_val in std::any - Rust (once itโ€™s stable, in the meantime you can build your own by copying its implementation).

Adding the type to the Debug output for PhantomData can have disadvantages: When types are long and complicated, then this addition could potentially significantly clutter the Debug output for your values. If you need a type-printing PhantomData anyway, you can also always build your own.

1 Like

Thanks for the quick reply! And for including an example that would solve my problem! I really appreciate the effort.

While this has been true until recently, the addition of the generic_const_exprs feature (still unstable - but available in nightly) has made this assumption false. Types can now have much more complicated values that are not obvious when looking at the source code alone. It's useful to have a way of verifying that the type you're constructing has the values you expect. For example consider this abstraction for creating a type that wraps arrays of even and odd length. I would like to verify that the length of that array and the type corresponds to my understanding of Even and Odd parity. Currently the fact that it prints out phantom: PhantomData does me no good:

[src/main.rs:11] [()].create() = ParityWrapper {
    inner: [
        (),
    ],
    phantom: PhantomData,
}
[src/main.rs:12] [0, 1].create() = ParityWrapper {
    inner: [
        0,
        1,
    ],
    phantom: PhantomData,
}

Not everyone wants to use these tools. Providing the users of rust with more options is better in my mind. I'm also sure there are situations where the runtime value and type are both not obvious, so a compile-time tools would fall short here.

I 100% sympathise with this idea - which is why I included it as an argument for not having types print for the general case. I do think PhantomData is a special case though - it only has one value. Most of the time I believe it's quite simple, and if it's not, you'd probably REALLY appreciate the debug output having that complex information along with it. Currently, just printing PhantomData is telling the user nothing really. IMO just printing PhantomData clutters the screen because it provides nothing the user doesn't know.

It's not a case of need but rather a case of nice to have. I think it's a good idea to lower the barrier to entry for folk experimenting with type-level programming as much as possible. Rolling your own version is not something someone who's new to things and learning the landscape is going to do. The Debug trait coupled with the dbg! macro is a nice combination for learning and debugging.

Also, rust-analizer currently does not cut it:

  fn main() {
H     let a = ([()].create());     โ– โ– โ– โ– โ–  if this is intentional, prefix it with an underscore: `_a` ParityWrapper<{unknown}, โ€ฆ>
      dbg!โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
  }       โ”‚1. Go to ParityWrapper (where_bound_propogation::ParityWrapper)โ”‚
          โ”‚                                                               โ”‚
  pub traiโ”‚let a: ParityWrapper<{unknown}, {unknown}>                     โ”‚
  #[deriveโ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
  struct Even;
  #[derive(Debug)]
1 Like