Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bunch of improvements on explaining `impl Trait` #1481

Closed
wants to merge 4 commits into from
Closed
Diff settings

Always

Just for now

@@ -279,33 +279,77 @@ the `Summary` trait, like `summarize`.

#### Trait Bounds

The `impl Trait` syntax works for short examples, but is syntax sugar for a
longer form. This is called a 'trait bound', and it looks like this:
The `impl Trait` syntax works for short examples, but except one small thing
it is syntax sugar for a longer form. This is called a 'trait bound', and it
looks like this:

```rust,ignore
pub fn notify<T: Summary>(item: T) {
println!("Breaking news! {}", item.summarize());
//The type name T is now in scope so you can do this
let i:T = item;
println!("Breaking news! {}", i.summarize());
}
```

This is equivalent to the example above, but is a bit more verbose. We place
This is roughly equivalent to the example above, except that it is a bit more
verbose but allow you to refer to the type of `item` with a name `T`. We place
trait bounds with the declaration of the generic type parameter, after a
colon and inside angle brackets. Because of the trait bound on `T`, we can
call `notify` and pass in any instance of `NewsArticle` or `Tweet`. Code that
calls the function with any other type, like a `String` or an `i32`, won’t
compile, because those types don’t implement `Summary`.

When should you use this form over `impl Trait`? While `impl Trait` is nice for
shorter examples, trait bounds are nice for more complex ones. For example,
say we wanted to take two things that implement `Summary`:
shorter examples and makes refactoring slightly easier as you have one less
identifier in the scope, trait bounds allow you to do more by letting you refer
to the types it involves. For example, say we wanted to take two things that
implement `Summary`:

```rust,ignore
//If you don't care whether item1 and item2 have the exact same type,
//use this:
pub fn notify1(item1: impl Summary, item2: impl Summary) {
//Or the roughly equivalent:
pub fn notify2<T1:Summary, T2:Summary>(item1: T1, item2: T2) {
//with the ability to refer to T1 and T2.
//If you need to ensure they have the exact same type,
//use this:
pub fn notify3<T: Summary>(item1: T, item2: T) {
//There is no equivlent in the impl Trait form as you have to name
//the type to be able to refer to it.
```
It is safe to assume that `notify1` and `notify2` doing the same thing, but
it is easy to confuse with `notify1` and `notify3`. In fact they behave quite
differently. For example, you can write
```rust,ignore
notify1(a_news_article, a_tweet);
```
but not
```rust,ignore
pub fn notify(item1: impl Summary, item2: impl Summary) {
pub fn notify<T: Summary>(item1: T, item2: T) {
notify3(a_news_article, a_tweet);
```
because `notify3` requires both items to have the same type!
The version with the bound is a bit easier. In general, you should use whatever
form makes your code the most understandable.
In some sense, making decisions to choose between `impl Trait` and named type is
similar to choose between:
```rust,ignore
let _v1 = calculate();
//The above is equivlent to
let _ = calculate();
//except that you can still use _v afterwards (like use named
//types with trait bounds), but _ is not even a variable name
//(like impl Trait).
```
As usual, both options have their pros and cons. In general, you should use
whatever form makes your code the most understandable.
##### Multiple trait bounds with `+`
@@ -316,14 +360,18 @@ type that implements `Summary` and `Display`. This can grow quite complex!
```rust,ignore
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
//If you don't want to name the types, you can write
fn some_function(t: impl Display + Clone, u: impl Clone + Debug) -> i32 {
```
#### `where` clauses for clearer code
However, there are downsides to using too many trait bounds. Each generic has
its own trait bounds, so functions with multiple generic type parameters can
have lots of trait bound information between a functions name and its
parameter list, making the function signature hard to read. For this reason,
parameter list, making the function signature hard to read (note this argument does
not apply to the `impl Trait` form; if your bound have too many traits, you should
consider seperate each input parameters in seperated lines). For this reason,
Rust has alternate syntax for specifying trait bounds inside a `where` clause
after the function signature. So instead of writing this:
@@ -334,6 +382,11 @@ fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
//The impl Trait form can also be formatted like
fn some_function(
t: impl Display + Clone,
u: impl Clone + Debug)
{
```
This functions signature is less cluttered in that the function name,
@@ -356,14 +409,25 @@ fn returns_summarizable() -> impl Summary {
}
```
This signature says, "I'm going to return something that implements the
`Summary` trait, but I'm not going to tell you the exact type. In our case,
we're returning a `Tweet`, but the caller doesn't know that.
As in the case `impl Trait` syntax in the parameter position, the above
involves a type that is present in the function signature, but not able to be
named or refered to in any other places. However, there is a single type behind
it, and it is determined by the function's return value.
In other words, this signature says, "I'm going to return something that
implements the `Summary` trait, but I'm not going to tell you the name of it,
so you will not be able to know the exact type." In our case, we're returning
a `Tweet`, but the caller doesn't know that.
(It is insteresting to compare this with the case of the parameter above.
Which will be syaing "You can send me anything that implements `Summary`,
and its name is completely irelevent to me and so I will not assume any
relationship between this and anything else you gave me.")
Why is this useful? In chapter 13, we're going to learn about two features
that rely heavily on traits: closures, and iterators. These features create
types that only the compiler knows, or types that are very, very long.
`impl Trait` lets you simply say "this returns an `Iterator`" without
`impl Trait` lets you simply say "this returns an `Iterator`" without
needing to write out a really long type.
This only works if you have a single type that you're returning, however.
@@ -391,8 +455,72 @@ fn returns_summarizable(switch: bool) -> impl Summary {
```
Here, we try to return either a `NewsArticle` or a `Tweet`. This cannot work,
due to restrictions around how `impl Trait` works. To write this code, you'll
have to wait until Chapter 17, "trait objects".
because a single `impl Trait` item, regardless it is in parameter position or
the return position, can only bind to a single (hidden) type in a specific
context. To write this code, one can use "trait objects" when possible, which
we will introduce in Chapter 17:
```rust,ignore
//We will discuss the dyn keyword in Chapter 17!
fn returns_summarizable(switch: bool) -> Box<dyn Summary> {
if switch {
Box::new(NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from("The Pittsburgh Penguins once again are the best
hockey team in the NHL."),
})
} else {
Box::new(Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
})
}
}
```
or try the "new enum" trick like in the following:
```rust,ignore
fn returns_summarizable(switch: bool) -> impl Summary {
//You can define local enums to avoid poluting the module scope
enum SomeSummaries {
NewsArticle(NewsArticle),
Tweet(Tweet)
}
impl Summary for SomeSummaries{
fn summarize(&self) -> String {
match self {
SomeSummaries::NewsArticle(na) => na.summarize(),
SomeSummaries::Tweet(t) => t.summarize()
}
}
}
if switch {
SomeSummaries::NewsArticle(NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from("The Pittsburgh Penguins once again are the best
hockey team in the NHL."),
})
} else {
SomeSummaries::Tweet(Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
})
}
}
```
As you will see in Chapter 17, it is not always possible to use trait objects.
The enum trick usually works though; but it takes much effort and is not easy
to new Rust users. However, this is the best we can have right now.
### Fixing the `largest` Function with Trait Bounds
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.