7

When defining a generic struct, is there a way in Rust to use different implementation of a method according to which trait is implemented by the given generic type T?

For example:

struct S<T> {
    value: T,
}

impl<T> S<T> {
    fn print_me(&self) {
        println!("I cannot be printed");
    }
}

impl<T: std::fmt::Display> S<T> {
    fn print_me(&self) {
        println!("{}", self.value);
    }
}

fn main() {
    let s = S { value: 2 };
    s.print_me();
}
nyarlathotep108
  • 5,275
  • 2
  • 26
  • 64
  • 5
    Unfortunately no. That [will be possible](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2015&gist=ee68f072be71d2159f58b408679b5418) once specialization lands, but it might [take a while](https://github.com/rust-lang/rust/issues/31844). Also, you probably meant `s.print_me()` instead of than `s.do_it()`. – user4815162342 Nov 22 '20 at 15:44
  • @user4815162342 you are right, edited – nyarlathotep108 Nov 22 '20 at 15:51
  • 2
    If you are okay with making `print_me` a macro, this is possible today on stable rust: https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md – msrd0 Nov 22 '20 at 16:54

1 Answers1

9

There is an unstable feature known as specialization which permits multiple impl blocks to apply to the same type as long as one of the blocks is more specific than the other:

#![feature(specialization)]

struct Printer<T>(T);

trait Print {
    fn print(&self);
}

// specialized implementation
impl<T: fmt::Display> Print for Printer<T> {
    fn print(&self) {
        println!("{}", self.0);
    }
}

// default implementation
impl<T> Print for Printer<T> {
    default fn print(&self) {
        println!("I cannot be printed");
    }
}


struct NotDisplay;

fn main() {
    let not_printable = Printer(NotDisplay);
    let printable = Printer("Hello World");

    not_printable.print();
    printable.print();
}

// => I cannot be printed
// => Hello World

On stable Rust, we'll need some other mechanism to accomplish specialization. Rust has another language feature capable of doing this: method resolution autoref. The compiler's rule is that if a method can be dispatched without autoref then it will be. Only if a method cannot be dispatched without autoref, the compiler will insert an autoref and attempt to resolve it again. So in this example:

impl Print for Value {
    fn print(self) {
        println!("Called on Value");
    }
}

impl Print for &Value {
    fn print(self) {
        println!("Called on &Value");
    }
}

The implementation for Value will be prioritized over &Value. Knowing this rule, we can mimic specialization in stable Rust:

struct Printer<T>(T);

trait Print {
    fn print(&self);
}

// specialized implementation
impl<T: fmt::Display> Print for Printer<T> {
    fn print(&self) {
        println!("{}", self.0);
    }
}

trait DefaultPrint {
    fn print(&self);
}

// default implementation
//
// Note that the Self type of this impl is &Printer<T> and so the 
// method argument is actually &&T! 
// That makes this impl lower priority during method
// resolution than the implementation for `Print` above.
impl<T> DefaultPrint for &Printer<T> {
    fn print(&self) {
        println!("I cannot be printed");
    }
}

struct NotDisplay;

fn main() {
    let not_printable = Printer(NotDisplay);
    let printable = Printer("Hello World");
    
    (&not_printable).print();
    (&printable).print();
}

// => I cannot be printed
// => Hello World

The compiler will try to use the Print implementation first. If it can't (because the type is not Display), it will then use the more general implementation of DefaultPrint.

The way that this technique applies method resolution cannot be described by a trait bound, so it will not work for regular methods, as we would have to choose between one of the traits (DefaultPrint or Print):

fn print<T: ???>(value: T) {
    (&value).print()
}

However, this trick can be very useful to macros, which do not need to spell out trait bounds:

macro_rules! print {
    ($e:expr) => {
        (&$e).print()
    };
}


print!(not_printable); // => I cannot be printed
print!(printable); // => Hello World
Ibraheem Ahmed
  • 11,652
  • 2
  • 48
  • 54
  • To add a couple of things to this: This doesn't actually need a macro to work, that's just sugar. And it won't work on generics to functions: if defining fn run(something : T) { let checker = S { value: something }; print_me!(checker); } Then run("hello"); will still result in "I cannot be printed." – DanielV Apr 19 '21 at 19:15
  • 1
    @DanielV Good points, I'll update the answer. – Ibraheem Ahmed Apr 19 '21 at 20:48