How is "impl Display" different from "foo<T: Display>(x: T) -> T"?


#1
fn foo<T: Display>(x: T) -> T {
    return x;
}

fn foo2(x: impl Display) -> impl Display {
    return x;
}

Can anyone tell me the difference? thanks a lot.


#2

There’s no meaningful difference in how the arguments acts, insofar as I’m aware.

Callers know what the return type of foo is, so they can do more than just display it.

Callers have no idea what the return type of foo2 is, so they can’t do anything other than just display it.


#3

Thanks a lot. I wrote a code snippet to show what I’ve found

fn foo<T: Display>(x: T) -> T {
    return x;
}

fn foo2(x: impl Display) -> impl Display {
    return x;
}

fn main() {
    let x = "abc";
    let xx : &str = foo(x);
    let xx2 : &str = foo2(x); // compile error: expected &str, found anonymized type
    let xx3 : impl Display = foo2(x); // compile error: `impl Trait` not allowed outside of function and inherent method return types
}

#4

Right.

With just look at the type of function foo, you can conclude:

  • The return type must have the same type of its operator.
  • The function body cannot assume anything on top of the fact that the parameter is Display.
  • Because Display contains only one method, namely fmt, and it takes self by reference, you can conclude this is functionaly equivalent to
fn foo<T: Display>(x: &T){...}

except that in the usage site it is slightly different.

On the other hand the signature of foo2 tells the same thing of its parameter, but for the return type, it only tells you it is also a Display but you have no idea what it is. So it can return you any value that Display and just use the input parameter in a println! macro or similar.

Examples. You can write

fn foo2(x: impl Display) -> impl Display {
    println!("{}",x);
    100
}

but the closest thing you can do for foo is something like

fn foo<T: Display>(x: T) -> T {
    println!("{}", x);
    x
}

in other words, you are not able to return anything other than x (unless unsafely, of cause). More interestingly, although you can say fn foo<T:Display>(mut x: T) -> T {...}, you will not be able to mutate x as Display does not contain any mutable method.


#5

Thank you very much for you detailed explaination.

It seems that I have another question.

I found that "// compile error:impl Traitnot allowed outside of function and inherent method return types" is quite confusing.

What is the officaly way to store the return value of impl Trait into a struct? I found this: https://users.rust-lang.org/t/how-do-you-store-the-result-of-an-impl-trait-return-type/14808 But it is way more complex than I expected :frowning:


#6

Is impl Trait not supposed to be store in the struct at all?


#7

That link is about storing it in a struct or enum. In your example you can just do let xx2 = foo2(x).


#8

I also want to store the value into struct/enum. I have C++/Java backgound, so I do expect things can be easily store in the local variable and also in struct/enum.

So:

  • Is there an easy way to do this?
  • If not, why? Is there any philosophy behind this design choice?

#9

Just as a general note: Rust is not C++, nor is it Java. Approaching it in terms of “design for C++/Java, then translate to Rust” is going to cause you problems.

Anyway, impl Trait isn’t fully implemented yet. There are cases where it simply is not going to work because of that, and there’s not much you can do other than wait.

In the case of using it in structs, I’m not sure that’s on the cards. At least, not directly. The closest is probably going to be defining a named, existential type in the module and using that as both a struct field, and the return type from the function.

Until then, you’re probably going to have to use either boxing, or generics.


#10

I also want to store the value into struct/enum. I have C++/Java backgound, so I do expect things can be easily store in the local variable and also in struct /enum.

The point of impl Display in return position is, that the caller doesn’t know the exact type, but only knows that it implements Display.

To put the impl Display into a struct you’ve to box it:

let x = Box::new(foo2(1));

#11

Sure, I certainly don’t expect Rust be exactly like Java/C++ and I love to learn to approache the problem in Rust way, that’s why I asked “philosophy” question. ;D Thanks for your answers .:laughing:


#12

Thank you very much. That is very convenient. To summarize, here is the code snippet:

    fn foo<T: Display>(x: T) -> T {
        return x;
    }

    fn foo2(x: impl Display) -> impl Display {
        return x;
    }

    fn main() {
        let x = "abc";
        let xx : &str = foo(x);
        // let xx2 : &str = foo2(x); // compile error: expected &str, found anonymized type
        // let xx3 : impl Display = foo2(x); // compile error: `impl Trait` not allowed outside of function and inherent method return types
        let xx4 : Box<Display> = Box::new(foo2(x));  // success!
        println!("{}", xx4);                         // success!
    }

#13
  • -> T means the caller chooses the actual type, known to the caller, unknown in the function.
  • -> impl Tr means the function chooses actual type, known to the function, unknown to the caller.

OTOH:

  • fn<T: Tr>(_: T)
  • fn(_: impl Tr)

are identical.


#14

This might be helpful: https://rust-lang-nursery.github.io/edition-guide/2018/transitioning/traits/impl-trait.html


#15

Basically, it’s just not done yet. RFC 2071 proposed adding a variant of impl Trait where you give the type an explicit name; it was accepted a whole eight months ago, but nobody has actually implemented it.

(On the other hand, if you use the existing impl Trait syntax rather than this proposed variant, I don’t think there’s any consensus on whether there should ever be a way to name that type, e.g. a typeof operator.)

For what it’s worth, C++ has a similar problem with lambdas, which similarly have an unnameable type and cannot be stored in a struct without making the struct generic.


#16

I haven’t dug very deep yet but isn’t there also a difference in terms of monomorphisation with the plain type and dynamic dispatch with the impl Trait ?


#17

impl Trait never adds dynamic dispatch. In return position it hides the type from the caller only in the sense that the compiler will complain if the caller tries to depend on it, not in the sense that any runtime indirection is introduced. In argument position it’s just sugar for existing generics, so same monomorphization rules.


#18

Thank you !