This is a fairly basic Rust syntax issue that I've run into several times. Based on unknowable runtime conditions, I will return one of several different return types from the same function. Also, this return type must use methods from two traits. How can I express this in Rust?

Solution

fn foo(runtime_condition: bool) -> Box<dyn BoilerTrait> {
  if runtime_condition {
    Box::new(Type1)
  } else {
    Box::new(Type2)
  }
}
  • BoilerTrait is a trait that requires the traits I actually care about (ex: UsefulTrait and Debug).
  • Type1 and Type2 are concrete types that both implement trait BoilerTrait
  • I can use methods from any of the traits required by BoilerTrait on return value of foo() transparently

Failure 1: return impl Trait

I mistakenly thought impl Trait in return position would be the way to handle this case. Here's code that does NOT compile:

fn foo(runtime_condition: bool) -> impl BoilerTrait {
    if runtime_condition {
        Box::new(Type1)
    } else {
        Box::new(Type2)
    }
}

The compiler complains:

error[E0308]: if and else have incompatible types
...
   = note: expected type `std::boxed::Box<Type1>`
              found type `std::boxed::Box<Type2>`

For the return impl Trait syntax, the compiler will only allow us to return one concrete type.

Why Can There Be Only One?

I think there are two advantages if I understand this right:

  1. Static dispatch and runtime performance improvements
  2. More flexible public API contract for consumers of a library

It's a middleground for library authors to avoid committing to a specific type in their public API without sacrificing the performance improvements of static dispatch. I think you might even be able to swap out the return type at compile time using cfg!() macros or what-not. Cool!

But, that's not my use case.

Failure 2: Multi-Trait Object

More code that doesn't quite compile:

fn foo(runtime_condition: bool) -> Box<dyn UsefulTrait+Debug> {
  if runtime_condition {
    Box::new(Type1)
  } else {
    Box::new(Type2)
  }
}

The compiler tells us:

error[E0225]: only auto traits can be used as additional traits in a trait object
  --> src/main.rs:12:33

I have no idea why you can't name multiple normal traits in a trait object. That's why we have to make a super trait BoilerTrait that requires the traits you actually want.

Use Type Annotations for Temporaries

When trying something more complicated than the examples above, temporary values weren't automatically recognized as the trait object I had in mind...It seems like the type inference could be better at this case, but in rust 1.40.0 you need explicit type annotations for this case.

First, the code that does NOT compile

fn foo(runtime_condition: bool) -> Box<dyn BoilerTrait> {
    let temporary = if runtime_condition {
        Box::new(Type1)
    } else {
        Box::new(Type2)
    };
    temporary
}

The compiler complains that the conditional returns different types. This is confusing because, the exact same expressions were returned just fine a moment ago without the let binding.

In our previous examples, without the temporary, the final expression from the conditional is returned from the function, which is explicitly annotated as -> Box<dyn BoilerTrait> so the compiler doesn't have to do any guessing about the type. However, it does have to guess about the type of temporary because there's no type annotation when the value is bound. Once the compiler decided that temporary is going to be a concrete type, we've lost the trait object.

The solution is easy, just add type annotation to the let binding:

let temporary: Box<dyn BoilerTrait> = ...;

Full Solution

Here's a fuller working code example and a playground:

// useful trait 1
use std::fmt::Debug;

// useful trait 2
trait UsefulTrait {
    fn useful(&self) {}
}
trait BoilerTrait:UsefulTrait + Debug {}

#[derive(Debug)]
struct Type1;
impl UsefulTrait for Type1 {}
impl BoilerTrait for Type1 {}

#[derive(Debug)]
struct Type2;
impl UsefulTrait for Type2 {}
impl BoilerTrait for Type2 {}

fn foo(runtime_condition: bool) -> Box<dyn BoilerTrait> {
    let temporary: Box<dyn BoilerTrait> = if runtime_condition {
        Box::new(Type1)
    } else {
        Box::new(Type2)
    };
    temporary
}

fn main() {
    // Call method from trait UsefulTrait
    foo(true).useful();

    // Call method from Debug trait
    dbg!(foo(false));
    dbg!(foo(true));
}