Search code examples
genericsrusttraitsassociated-types

Dynamic dispatch on traits with associated types and generic implementations


I want to have a Rectangle type that is generic over its dimensions which implements an Area trait. The only requirement on the output of this trait is for it to be Displayable. The requirement for generic dimensions is contrived, but I'm treating it as a learning example. The following compiles just fine:

use std::fmt;
use std::ops;

trait Area{
    type Output: fmt::Display;
    fn area(&self) -> Self::Output;
}

struct Rectangle <T, U>{
    width: T,
    height: U,
}

impl <T, U> Area for Rectangle<T, U>
where T: ops::Mul<U> + Copy, U: Copy,
<T as ops::Mul<U>>::Output: fmt::Display
{
    type Output = <T as ops::Mul<U>>::Output;
    fn area(&self) -> Self::Output {
        self.width * self.height
    }
}


fn main(){
    let float_r = Rectangle {width: 1.5_f64, height: 2.5_f64};
    let int_r = Rectangle {width: 2_i32, height: 3_i32};
    println!("Area of rectangle is {}", float_r.area());
    println!("Area of rectangle is {}", int_r.area());
}

I now want to refactor the logging lines into a function that accepts any type which implements Area to later user with e.g a Circle type:

fn log_shape(s: &dyn Area){
    println!("Area of rectangle is {}", s.area());
}

but this does not compile unless I define the Output in the function signature:

error[E0191]: the value of the associated type `Output` (from trait `Area`) must be specified
  --> src/main.rs:24:22
   |
5  |     type Output: fmt::Display;
   |     -------------------------- `Output` defined here
...
24 | fn log_shape(s: &dyn Area){
   |                      ^^^^ help: specify the associated type: `Area<Output = Type>`

For more information about this error, try `rustc --explain E0191`.

however, I don't want to do this, since if I define a circle:

struct Circle<T>{
    radius: T
}

the Output for Circle's implementation of Area is of type <<T as ops::Mul<T>>::Output as ops::Mul<f64>>::Output (corresponding to self.radius * self.radius * pi): which is a different type to that of Rectangle's area.

Is my intended use of associated types in this context misguided? If so, what is the advised way of achieving my desired functionality?


Solution

  • The issue is that when you call s.area(), the return type needs to have a sized, known type, which is why the error wants you to specify it.

    A few ways around this (mostly from the comments):

    1. Print inside a trait method

    Here I made a new trait but you may want to replace your existing trait (playground).

    impl<T, U> Area for Rectangle<T, U>
    where
        T: ops::Mul<U> + Copy,
        U: Copy,
    {
        type Output = <T as ops::Mul<U>>::Output;
        fn area(&self) -> Self::Output {
            self.width * self.height
        }
    }
    
    trait Log {
        fn log(&self);
    }
    
    impl<T> Log for T
    where
        T: Area,
        T::Output: Display,
    {
        fn log(&self) {
            println!("Area of rectangle is {}", self.area());
        }
    }
    
    fn log_shape(s: &dyn Log) {
        s.log()
    }
    

    2. Use static dispatch

    This is more idiomatic and likely faster if your use case allows (playground).

    fn log_shape<A: Area>(s: A) {
        println!("Area of rectangle is {}", s.area());
    }
    

    3. Return a trait object from the trait method

    This is likely slower and has a different flavor, yet similar level, of flexibility to the first method (playground).

    trait Area {
        fn area(&self) -> Box<dyn Display>;
    }
    
    impl<T, U> Area for Rectangle<T, U>
    where
        T: ops::Mul<U> + Copy,
        U: Copy,
        <T as ops::Mul<U>>::Output: Display + 'static,
    {
        fn area(&self) -> Box<dyn Display> {
            let a = self.width * self.height;
            Box::new(a)
        }
    }
    

    In summary: #1 works with trait objects while being performant (no extra allocations). If you have it take a Formatter parameter like in Display::fmt, you can use it to write to any writer. #2 is the most idiomatic and fastest if you can use it. #3 lets you leave the log_shape function unchanged.