Usage of `impl trait`

fn foo(is_number: bool) -> impl Display {
    if is_number {
        42_u32
    } else {
       String::from("this is a string")
    }
}

fn main() {
    foo(true);
}

Syntax wise above code looks like it should work. But it fails to do so . I also understand that this can be solved by Box<dyn triat>. But this made me think what exactly is the usage of impl trait when there is already dyn trait

The restriction of impl Trait that prevents this is that there must still be a single concrete type[1] in place for the impl Trait type. One impl Trait type isn’t the same as another one (e.g. from a different function) even if the trait bound is equal. This is what powers the advantages over Box<dyn Trait> or having a known fixed size, and efficient non-dynamic dispatch: there still exists a concrete type. If you don’t want that, going back to Box<dyn Trait> is usually what you want.

I’ve seen prior discussion about a middle ground: instead of going fully into unsized (or boxed) type dispatched using a vtable, the lighter-weight alternative to unify two types is via an enum, and handwriting that can be slightly boilerplaty. So people have been discussion whether Rust needs a feature along the lines of

// fake syntax to demonstrate the rough idea
fn foo(is_number: bool) -> enum impl Display {
    if is_number {
        enum { 42_u32 }
    } else {
        enum { String::from("this is a string") }
    }
}

that would do that for you.

For common traits, including Display there’s a commonly used pre-existing enum that does something like this, called Either in the crate either. With it, you can write

fn foo(is_number: bool) -> impl std::fmt::Display {
    use either::Either::*;
    if is_number {
        Left(42_u32)
    } else {
        Right(String::from("this is a string"))
    }
}

fn main() {
    foo(true);
}

and have that work easily.


Regarding the exact usage of impl Trait, given its restrictions: The main motivation why you might want it is

  • types that cannot be named, e.g. types involving closure types, or async block/fn types
  • type names that get excessively lengthy
  • not wanting to expose the exact type being returned, e.g. to hide implementation details and/or to improve options for future changes without breakage

  1. possibly depending on generic type or lifetime arguments ↩︎

6 Likes

For a specific example, functions that return iterators often hit all three of these categories:

  • If you use filter or map in the internal implementation, the resulting type contains an (unnameable) closure
  • Every iterator adapter you call wraps its argument type, so you can easily end up constructing something with a name like Zip<vec::IntoIter<T>, Enumerate<Take<btree_map::Values<'a,K,U>>>>
  • While iterator adapter chains are quick and easy to implement, they aren't always the most efficient choice. Return types like this, however, restate the implementation and lock you out of implementing a more efficient implementation in the future (w/o a breaking change).

If you instead specify the type above as impl Iterator<Item=(T, (usize, &'a U))>, it's much clearer to the caller what to expect and you leave yourself free to replace the implementation with something better.

6 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.