4

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);
        }
    }
}
tower120
  • 5,007
  • 6
  • 40
  • 88
  • 1
    Maybe [`impls` crate](https://github.com/nvzqz/impls#examples) – Alexey S. Larionov Aug 08 '21 at 15:09
  • 1
    That requires [specialization](https://rust-lang.github.io/rfcs/1210-impl-specialization.html), not yet available. – user4815162342 Aug 08 '21 at 15:17
  • @user4815162342 I thought about specialization, how does it help? Function signature still have to be `` - either way, I can't pass value to specialized trait implementation – tower120 Aug 08 '21 at 15:28
  • @AlexeyLarionov how function signature will look like with that crate? – tower120 Aug 08 '21 at 15:33
  • 2
    Does this answer your question? [How can I implement a function differently depending on if a generic type implements a trait or not?](https://stackoverflow.com/questions/51141200/how-can-i-implement-a-function-differently-depending-on-if-a-generic-type-implem) – Frxstrem Aug 08 '21 at 18:05
  • 1
    @tower120 I think specialization would help, but would require some legwork. Basically you'd crate a trait `Test` with one blanket implementation for `Eq` and another for `Eq + Debug` (this is what specialization makes possible). `test` would then require `Test` and would call its method that is no-op when `Debug` is unavailable and prints the contents otherwise. – user4815162342 Aug 08 '21 at 18:21
  • Is it possible to do with `min_specialization`? Since I read that `specialization` feature "unsound". – tower120 Aug 08 '21 at 22:03

2 Answers2

3

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.

Filipe Rodrigues
  • 1,843
  • 2
  • 12
  • 21
-3

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!

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • 3
    But how I accept parameters with optional trait bounds in the first place? How this signature will look `fn test(a:T, b:T)`? – tower120 Aug 08 '21 at 15:59
  • This would only work when `T` is known to implement the trait inside the function (i.e. it's a specific type or a generic type with the trait in its bounds). So in when using a generic function, as in the question, it would always consider `T` to not implement `Debug` even if the actual type did. ([Playground example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d82915b659a4ad4747fbfed887a5ab02)) – Frxstrem Aug 08 '21 at 17:48