Skip to content

Commit

Permalink
Fixes based on feedback from quinedot.
Browse files Browse the repository at this point in the history
  • Loading branch information
jwalton committed Apr 28, 2023
1 parent 8b39a22 commit d9ba562
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 50 deletions.
44 changes: 31 additions & 13 deletions docs/ch10/ch10-02-traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Note that the trait only defines the method signatures - the contract, if you wi

In languages like TypeScript and Go, if we have an interface, and we have a type that defines all the same methods that the interface declares, then the type implements that interface. There's no need to explicitly mark that the type implements the interface. This is called "duck typing", because, as the saying goes, "if it walks like a duck, and it quacks like a duck, then it must be a duck."

Not so in Rust. Here we must explicitly declare that a type implements a trait. The syntax is `impl [TRAIT] for [STRUCT] {}`, and inside the curly braces we place all the methods we wish to implement:
Not so in Rust. Here we must explicitly declare that a type implements a trait. The syntax is `impl [TRAIT] for [STRUCT] {}`, and inside the curly braces we place all the methods we need to implement:

```rust
pub struct NewsArticle {
Expand Down Expand Up @@ -71,6 +71,12 @@ Other crates can use the `Summary` trait and implement it on their own types, ju

This restriction is in place because of something called the _orphan rule_. Let's suppose there's a `color` crate out there. You implement a library crate that uses `color`, but you notice one of the types in `color` doesn't implement the `Display` trait and you want to `println!` a color, so you implement the `Display` trait on that type. Now suppose I'm writing a separate library crate, and I do the same thing. Now suppose someone adds your crate and my crate to their application. At this point, the Rust compiler has two competing implementations for `Display` on this type, so which one does it use? Since Rust has no way to know which is the "correct" one, Rust just stops this from ever happening by forcing the crate to own at least one of the type or trait.

:::note

The orphan rule is actually [slightly more complicated](https://rust-lang.github.io/rfcs/2451-re-rebalancing-coherence.html#concrete-orphan-rules) than mentioned above. Once generics start getting involved, it's possible to use a foreign trait and foreign type, given that one of the generic types is local. See the above link for full details.

:::

## Default Implementations

Remember how we said a trait just had signatures and no implementations? Well, we lied a little. Sometimes it's handy to be able to define default behavior for a method:
Expand Down Expand Up @@ -104,35 +110,36 @@ Some implementations might only implement `summarize_author()`, while some might

## Traits as Parameters

We can use a trait as a type for a function parameter or variable using the `impl` keyword:
When we define a generic function, we can limit what kinds of concrete types are allowed to be used in place of the generic type using a _trait bound_:

```rust
pub fn notify(item: &impl Summary) {
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
```

This is actually syntactic sugar for _trait bound_ syntax:
Here we're declaring a generic function, but we're setting bounds on the type of T. Whatever you pass in for T has to satisfy the `Summary` trait. This is a common thing to do, so there's a shortcut to specify this using the `impl` keyword:

```rust
pub fn notify<T: Summary>(item: &T) {
// This is syntactic sugar for the example above.
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
```

Here we're declaring a generic function, but we're setting bounds on the type of T. Whatever you pass in for T has to satisfy the `Summary` trait. We can specify more than one trait bound:
We can specify more than one trait bound:

```rust
// Using the `impl` syntax:
pub fn notify(item: &(impl Summary + Display)) {...}

// Using a trait bound:
pub fn notify<T: Summary + Display>(item: &T) {...}

// Using the `impl` syntax:
pub fn notify(item: &(impl Summary + Display)) {...}
```

Here whatever we pass in for `T` must satisfy both our own `Summary` trait and the `Display` trait from the standard library (so we can use `{}` to display the item with `println!` or `format!`).

This can get a bit hard to read if you have a lot of traits bounds. There ends up being a lot of clutter between the name of the function and the parameters. Borrowing a page from SQL, we can also write trait bounds using the `with` syntax. These two examples are equivalent:
This can get a bit hard to read if you have a lot of traits bounds. There ends up being a lot of clutter between the name of the function and the parameters. Borrowing a page from SQL, we can also write trait bounds using a `where` clause. These two examples are equivalent:

```rust
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {...}
Expand All @@ -146,7 +153,7 @@ where

## Returning Types that Implement Traits

In addition to using traits as parameters, we can of course also return them. This lets us hide the concrete type from the caller:
We can hide the concrete type returned by a function using an [_opaque type_](https://rustc-dev-guide.rust-lang.org/opaque-types-type-alias-impl-trait.html). This lets us hide the concrete type from the caller (and allows you to change the concrete type later without affecting callers):

```rust
fn returns_summarizable() -> impl Summary {
Expand All @@ -161,9 +168,20 @@ fn returns_summarizable() -> impl Summary {
}
```

Specifying a trait as a return type can be very handy when using closures and iterators. Sometimes when using an iterator, the type inferred by the compiler can be quite long, and writing the full type out by hand would be a lot of work without much benefit. Being able to supply a trait here is much more concise.
Note that even though this `impl` syntax looks similar to the shortcut we used to specify a trait bound above, this is not at all the same. This function is not generic. There is still a single concrete type being returned by this function (in this case `Tweet`), but callers are limited to only using the interface provided by the trait (in this case `Summary`).

The concrete type here is inferred by the compiler, but it's important to realize there is still one. If you were to add an `if` statement to this function, you would not be able to return a `Tweet` in one branch and a `NewsArticle` in the other. (We'll see how to overcome this with trait objects and dynamic dispatch in [chapter 17](../ch17-object-oriented-features.md#172---using-trait-objects-that-allow-for-values-of-different-types).)

This syntax is useful if we want to return something that has a concrete type that can't be written down, like a closure:

```rust
fn thing_returning_closure() -> impl Fn(i32) -> bool {
println!("here's a closure for you!");
|x: i32| x % 3 == 0
}
```

If you're coming from another language, you might think that `returns_summarizable()` would be able to return a `Tweet` in one branch and a `NewsArticle` in another, but it can't. This restriction is imposed by how this is implemented in the compiler. We'll see how to overcome this with trait objects in [chapter 17](../ch17-object-oriented-features.md#172---using-trait-objects-that-allow-for-values-of-different-types).
We haven't talked about iterators yet, but sometimes when using an iterator, the type inferred by the compiler can be quite long, and writing the full type out by hand would be a lot of work without much benefit. Being able to supply an opaque type here is much more concise.

## Using Trait Bounds to Conditionally Implement Methods

Expand Down
14 changes: 10 additions & 4 deletions docs/ch10/ch10-03-lifetimes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# 10.3 - Validating References with Lifetimes

Every reference in Rust has a _lifetime_ where the reference is valid. This has to do with [ownership][chap4], so it's a feature that's somewhat unique to Rust.
:::info

TODO: This section needs some rework. If you want to get deep into how lifetimes work from the compiler's perspective, [this](https://doc.rust-lang.org/nomicon/subtyping.html) is a good read.

:::

Every value in Rust has a _lifetime_ - a point in the code where the value is created, and a point in the code where the value is destroyed. Every reference in Rust has two lifetimes - the lifetime of the reference itself (from where it's created until where it is last used) and a lifetime of the value it points to. The lifetime of the reference obviously needs to be shorter than the lifetime of the value, otherwise the reference will point to freed memory.

Just as rustc infers the type of many of our parameters, in most cases Rust can infer the lifetime of a reference (usually from when it is created until it's last use in a function). Just as we can explicitly annotate a variable's type, we can also explicitly annotate the lifetime of a reference in cases where the compiler can't infer what we want.

Expand All @@ -21,7 +27,7 @@ fn main() {
} // ---------+
```

If you try this, it won't compile. The variable `r` is in scope for the entire `main()` function, but it's a reference to `x` which will be dropped when we reach the end of the inner scope. After we reach the end of that inner scope, `r` is now a reference to freed memory, so Rust's _borrow checker_ won't let us use it.
This won't compile. The variable `r` is in scope for the entire `main()` function, but it's a reference to `x` which will be dropped when we reach the end of the inner scope. After we reach the end of that inner scope, `r` is now a reference to freed memory, so Rust's _borrow checker_ won't let us use it.

More formally, we can say that `r` and `x` have different lifetimes, which we've marked in the comments of this example, using the labels `'a` and `'b` (strange names, but this is actually a bit of foreshadowing). The borrow checker sees that `r` has a lifetime of `'a`, but references memory that has the lifetime `'b`, and since `'b` is shorter than `'a`, the borrow checker won't allow this.

Expand Down Expand Up @@ -110,7 +116,7 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
}
```

Think about this a bit like a generic function (the syntax is similar for a good reason). We're saying here there exists some lifetime which we're going to call `'a`, and the variables `x` and `y` both life at least as long as this hypothetical `'a`'. They don't have to both be the same lifetime, they just both have to be valid at the start and end of `'a`. Then in the case of this function we're making the claim that the value we return is going to be valid for this same lifetime. At compile time, the compiler will see how long the passed in `x` lives, how long the passed in `y` lives, and then it will verify that the result of this function isn't used anywhere outside of that lifetime.
Think about this a bit like a generic function (the syntax is similar for a good reason). We're saying here there exists some lifetime which we're going to call `'a`, and the variables `x` and `y` both life at least as long as this hypothetical `'a`. They don't have to both be the same lifetime, they just both have to be valid at the start and end of `'a`. Then in the case of this function we're making the claim that the value we return is going to be valid for this same lifetime. At compile time, the compiler will see how long the passed in `x` lives, how long the passed in `y` lives, and then it will verify that the result of this function isn't used anywhere outside of that lifetime.

Putting this a bit more succinctly, we're telling the compiler that the return value of `longest()` will live at least as long as the shorter lifetime of `x` and `y`. When the rust compiler analyzes a call to `longest()` it can now mark it as an error if the two parameters passed in don't adhere to this constraint.

Expand Down Expand Up @@ -239,7 +245,7 @@ fn first_word(s: &str) -> &str {

How come this compiles without lifetime annotations? Why don't we have to tell the compiler that the return value has the same lifetime as `s`? Actually, in the pre-1.0 days of Rust, lifetime annotations would have been mandatory here. But there are certain cases where Rust can now work out the lifetime on it's own. We call this _lifetime elision_, and say that the compiler _elides_ these lifetime annotations for us.

What the compiler does is to assign a different lifecycle to every reference in the parameter list (`'a` for the first one, `'b` for the second, and so on...). If there is exactly one input lifetime parameter, that lifecycle is automatically assigned to all output parameters. If there is more than one input lifetime parameter but one of them is for `&self`, then the lifetime of `self` is assigned to all output parameters. Otherwise, the compiler will error.
What the compiler does is to assign a different lifetime to every reference in the parameter list (`'a` for the first one, `'b` for the second, and so on...). If there is exactly one input lifetime parameter, that lifetime is automatically assigned to all output parameters. If there is more than one input lifetime parameter but one of them is for `&self`, then the lifetime of `self` is assigned to all output parameters. Otherwise, the compiler will error.

In the case above, there's only one lifetime that `first_word` could really be returning; if `first_word` created a new `String` and tried to return a reference to it, the new `String` would be dropped when we leave the function and the reference would be invalid. The only sensible reference for it to return comes from `s`, so Rust infers this for us. (It _could_ be a static lifetime, but if it were we'd have to explicitly annotate it as such.)

Expand Down
2 changes: 1 addition & 1 deletion docs/ch13-functional-language-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ fn iterator_demonstration() {
}
```

Calling `next` on an iterator changes it's internal state, which is why the `self` parameter on `next` is marked `&mut`. This means we need to declare `v1_iter` as `mut` here as well. In the example above where we used a for loop, you might notice we didn't make `v1_iter` mutable. This is because the `for` loop took ownership of the iterator and made it mutable - sneaky Rust.
Calling `next` on an iterator changes it's internal state, which is why the `self` parameter on `next` is marked `&mut`. This means we need to declare `v1_iter` as `mut` here as well. In the example above where we used a for loop, you might notice we didn't make `v1_iter` mutable. This is because the `for` loop [took ownership of the iterator](https://doc.rust-lang.org/std/iter/index.html#for-loops-and-intoiterator) and made it mutable - sneaky Rust.

Another thing to note is that the iterator returned by `iter` returns immutable references to the underlying collection. There's an `iter_mut` that returns mutable references, if we want to modify some or all of the members of a collection. There's also an `into_iter` which takes ownership of the receiver (`into` because it converts the underlying collection into an iterator, and you won't be able to access the underlying collection anymore) and returns owned values. For example, if you called `v1.into_iter` above, you'd get back an iterator of owned values, and wouldn't be able to use `v1` anymore.

Expand Down
Loading

0 comments on commit d9ba562

Please sign in to comment.