DEV Community

loading...

Rust impl Trait For Programmers

hertz4 profile image Sam Pagenkopf ・4 min read

Header

Table of Contents

  1. Introduction
  2. Examples
    1. Return impl Trait
    2. Argument impl Trait
    3. Relationship to dyn/dynamic types
  3. Commentary
    1. Argument position controversy
    2. Future of impl Trait
    3. Where's the type theory?
    4. Applied example (return position)

Introduction

Rust's impl Trait is a little useful.

impl Trait gives the ability to hide types in functions. It is best in -> return position, but is also usable in (argument) position.

Use it when you know what Trait you want something for, but you don't care what it really is.

Examples

Return impl Trait

Here it is:

    use std::fmt::Display;

    // First case: compiler wants more
    fn <T: Display>does_not_work() -> T {"Whoops!"}
    // Second case: compiler satisfied
    fn actually_works() -> impl Display {"Not whoops!"}

Without impl Trait, it looks like this:

    fn old_explicit_way_that_works() -> String {"Duh"}
    fn slow_way_that_works() -> Box<dyn Display> {Box::new("Hello")}

Sometimes, the old way requires you to write a big, awful type that has nothing to do with how it is actually used. This solves that.

Argument impl Trait

The following two things are almost identical:

    fn visible_type_parameter<T: Trait>(x: T) {}
    fn hidden_type_parameter(x: impl T) {}

It is sometimes helpful to specify T using turbofish_aka_explicit_type::<DesiredType>. This is not currntly possible with impl T. Don't overthink this one; I favor type parameters. See commentary for a debate.

Relationship to dyn/dynamic types

Runtime Trait objects specified by &'lifetime dyn Trait or Box<dyn Trait> can be converted to compile time types. This is a conversion from dynamic to static typing.

    trait Trait {}
    struct Type;
    impl Trait for Type {}

    fn dynamic_type(x: &dyn Trait) {}

    fn static_explicit_type(x: &Type) {}
    fn static_implicit_hidden_type(x: &impl Trait) {}
    fn static_implicit_visible_type<T: Trait>(x: &T) {}

    fn main() {
        let x = Type;
        dynamic_type(&x as &Type);
        static_explicit_type(&x);
        static_implicit_hidden_type(&x);
    //    static_implicit_hidden_type::<Type>(&x); doesn't work (yet?)
        static_implicit_visible_type(&x);
        static_implicit_visible_type::<Type>(&x);
    }

This only works when &dyn Trait refers to a single type; i.e. you can only return one type from one function.

Commentary

Argument position controversy

https://github.com/rust-lang/rfcs/pull/2444
I can't summarize this issue easily. Read for yourself, especially the last post, to get a sense.

Future of impl Trait

Official tracking issue:
https://github.com/rust-lang/rust/issues/34511
It's January 2019. Here's what I see:

Where's the type theory?

People have tried to explain it this way (existential? universal?), but take a step back to look at rust. impl Trait is a single step forward in type inference.

Recognize that I'm not using a powerful set of tools for understanding type systems that does exist. Give this article a read:
https://varkor.github.io/blog/2018/07/03/existential-types-in-rust.html

Applied example (return position)

Naive attempt:

    use std::fmt::Display;

    fn main() {
        println!("{}", make_value(0));
        println!("{}", make_value(1));
    }

    fn make_value<T: Display>(index: usize) -> T {
        match index {
            0 => "Hello, World",
            1 => "Hello, World (1)",
            _ => panic!(),
        }
    }

Fails because…

    error[E0282]: type annotations needed
     --> /home/sam/code/trash/impl-return.rs:4:20
      |
    4 |     println!("{}", make_value(0));
      |                    ^^^^^^^^^^ cannot infer type for `T`

    error[E0308]: match arms have incompatible types
      --> /home/sam/code/trash/impl-return.rs:9:5
       |
    9  | /     match index {
    10 | |         0 => "Hello, World",
       | |              -------------- match arm with an incompatible type
    11 | |         1 => "Hello, World (1)",
    12 | |         _ => panic!(),
    13 | |     }
       | |_____^ expected type parameter, found reference
       |
       = note: expected type `T`
                  found type `&'static str`

    error: aborting due to 2 previous errors

    Some errors occurred: E0282, E0308.
    For more information about an error, try `rustc --explain E0282`.

Okay, so I do this:

    use std::fmt::Display;

    fn main() {
        println!("{}", make_value::<&'static str>(0));
        println!("{}", make_value::<&'static str>(1));
    }

    fn make_value<T: Display>(index: usize) -> T {
        match index {
            0 => "Hello, World",
            1 => "okay",
            _ => panic!(),
        }
    }

Another dead end:

    error[E0308]: match arms have incompatible types
      --> /home/sam/code/trash/impl-return.rs:9:5
       |
    9  | /     match index {
    10 | |         0 => "Hello, World",
       | |              -------------- match arm with an incompatible type
    11 | |         1 => "okay",
    12 | |         _ => panic!(),
    13 | |     }
       | |_____^ expected type parameter, found reference
       |
       = note: expected type `T`
                  found type `&'static str`

    error: aborting due to previous error

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

Yet this succeeds:

    use std::fmt::Display;

    fn make_value(index: usize) -> impl Display {
        match index {
            0 => "Hello, World",
            1 => "Hello, World (1)",
            _ => panic!(),
        }
    }

(Generated using org-mode)

Discussion

pic
Editor guide
Collapse
josephpenafiel profile image
Joseph Penafiel

Hello, thank you for sharing your knowledge. I’m learning rust, and I see that you were able to return string slices (which are references) from the make_value function. Why did it work without having to annotate lifetime parameters? Does “impl Display” puts them implicitly?

Collapse
hertz4 profile image
Sam Pagenkopf Author

I found this error message that explains things well:
github.com/rust-lang/rust/issues/5...

The lifetime is 'static by default, but writing Display + '_ tells the compiler to infer a shorter-lasting lifetime.