function "optional trait bound" in rust

Viewed 712

I want to have something like optional trait bound for function. Where if T implements that type - do something.

fn test<T: Eq + ?Debug>(a:T, b:T){
    if a!=b{
        println!("Not equal!");
        if (T impl Debug){
            println!("{:?} != {:?}", a, b);
        }
    }
}
2 Answers

As @user4815162342 commented, using specialization, this is possible.

I'll provide a slightly different approach from what they specified in their comment, to keep the same if ... { ... } setup that you had in your original code.

The idea is to have a trait AsMaybeDebug with an associated type Debug, which always implements Debug and a function to go from &Self to Option<&Self::Debug>:

trait AsMaybeDebug {
    type Debug: Debug;
    fn as_maybe_debug(&self) -> Option<&Self::Debug>;
}

After this we make a default impl for all T, with the debug type being !, the never type, and always return None.

impl<T> AsMaybeDebug for T {
    default type Debug = !;
    default fn as_maybe_debug(&self) -> Option<&Self::Debug> {
        None
    }
}

Instead of the never type, you could choose any type that always implemented Debug but still returning None.

Afterwards we specialize for T: Debug by returning Self:

impl<T: Debug> AsMaybeDebug for T {
    type Debug = Self;
    fn as_maybe_debug(&self) -> Option<&Self::Debug> {
        Some(self)
    }
}

Finally in test we just call as_maybe_debug and check if T: Debug

fn test<T: Eq>(a: T, b: T){
    if a != b {
        println!("Not equal!");
        
        if let (Some(a), Some(b)) = (a.as_maybe_debug(), b.as_maybe_debug()) {
            println!("{:?} != {:?}", a, b);
        }
    }
}

You can check in the playground both that it works and that the assembly generated for test_non_debug doesn't have any debugging calls, only the single call to std::io::_print.

It unfortunately isn't possible to retrieve the original a or b inside the if after calling as_maybe_debug.

This is due to <Self as AsMaybeDebug>::Debug not being convertible back to Self. This can be fixed, but not easily as it requires updates from the standard library.

Requiring AsMaybeDebug::Debug: AsRef<Self> doesn't work for 2 reasons:

    1. There is no impl<T> AsRef<T> for T yet, this is due to specialization still being incomplete, I assume.
    1. There is no impl<T> AsRef<T> for ! yet. Not sure if this impl can be made even with specialization or not, but it would be required.

Also, although the specialization can be unsound, I believe that the trait and it's impls cannot be used for unsoundness, you would need a specific setup to be able to generate unsoundness from it, which this lacks.

As mentioned in the comments, you're looking for the impls crate, which does exactly what you want.

if impls!(T: Debug) {
  ...
}

Just for the sake of completeness, here's how you do it without an external crate dependency. I'm paraphrasing from the way the impls developer explains the trick.

Let's say we want to check whether some type implements Debug. First, let's define the "base case".

trait NotDebug {
  const IMPLS: bool = false;
}

We'll also provide a blanket implementation so that all types (which don't have a better answer) have a IMPLS constant equal to false.

impl<T> NotDebug for T {}

Now, let's make a simple type with a single generic type parameter.

struct IsDebug<T>(std::marker::PhantomData<T>);

PhantomData is conceptually nonexistent and exists only to anchor the generic type T to our IsDebug. We can think of IsDebug as being effectively a singleton struct.

Now, we would like IsDebug::<T>::IMPLS to be true if (and only if) T implements Debug. Currently, IsDebug::<T>::IMPLS is always false, by a blanket implementation of NotDebug. But we can specify an inherent impl that applies conditionally.

impl<T: Debug> IsDebug<T> {
  const IMPLS: bool = true;
}

Since this is an impl on IsDebug itself, not on a trait implementation, it takes precedent over the NotDebug blanket implementation. In any case where T: Debug, the inherent impl kicks in and we get true. In any other case, the inherent impl fails, so we get the fallback blanket implementation which gives false.

Try it in the Rust Playground!

Related