diff --git a/src/doc/guide.md b/src/doc/guide.md index dba05e60ed1..0a531a11a65 100644 --- a/src/doc/guide.md +++ b/src/doc/guide.md @@ -4769,13 +4769,13 @@ enum OptionalFloat64 { } ``` -This is really unfortunate. Luckily, Rust has a feature that gives us a better -way: generics. Generics are called **parametric polymorphism** in type theory, -which means that they are types or functions that have multiple forms ("poly" -is multiple, "morph" is form) over a given parameter ("parametric"). +Such repetition is unfortunate. Luckily, Rust has a feature that gives us a +better way: **generics**. Generics are called **parametric polymorphism** in +type theory, which means that they are types or functions that have multiple +forms over a given parameter ("parametric"). -Anyway, enough with type theory declarations, let's check out the generic form -of `OptionalInt`. It is actually provided by Rust itself, and looks like this: +Let's see how generics help us escape `OptionalInt`. `Option` is already +provided in Rust's standard library and looks like this: ```rust enum Option { @@ -4784,25 +4784,27 @@ enum Option { } ``` -The `` part, which you've seen a few times before, indicates that this is -a generic data type. Inside the declaration of our enum, wherever we see a `T`, -we substitute that type for the same type used in the generic. Here's an -example of using `Option`, with some extra type annotations: +The `` part, which you've seen a few times before, indicates that this is a +generic data type. `T` is called a **type parameter**. When we create instances +of `Option`, we need to provide a concrete type in place of the type +parameter. For example, if we wanted something like our `OptionalInt`, we would +need to instantiate an `Option`. Inside the declaration of our enum, +wherever we see a `T`, we replace it with the type specified (or inferred by the +the compiler). ```{rust} let x: Option = Some(5i); ``` -In the type declaration, we say `Option`. Note how similar this looks to -`Option`. So, in this particular `Option`, `T` has the value of `int`. On -the right-hand side of the binding, we do make a `Some(T)`, where `T` is `5i`. -Since that's an `int`, the two sides match, and Rust is happy. If they didn't -match, we'd get an error: +In this particular `Option`, `T` has the value of `int`. On the right-hand side +of the binding, we do make a `Some(T)`, where `T` is `5i`. Since that's an +`int`, the two sides match, and Rust is happy. If they didn't match, we'd get an +error: ```{rust,ignore} let x: Option = Some(5i); -// error: mismatched types: expected `core::option::Option` -// but found `core::option::Option` (expected f64 but found int) +// error: mismatched types: expected `core::option::Option`, +// found `core::option::Option` (expected f64, found int) ``` That doesn't mean we can't make `Option`s that hold an `f64`! They just have to @@ -4813,8 +4815,6 @@ let x: Option = Some(5i); let y: Option = Some(5.0f64); ``` -This is just fine. One definition, multiple uses. - Generics don't have to only be generic over one type. Consider Rust's built-in `Result` type: @@ -4835,20 +4835,20 @@ enum Result { } ``` -if we wanted to. Convention says that the first generic parameter should be -`T`, for 'type,' and that we use `E` for 'error'. Rust doesn't care, however. +Convention says that the first generic parameter should be `T`, for "type," and +that we use `E` for "error." -The `Result` type is intended to -be used to return the result of a computation, and to have the ability to -return an error if it didn't work out. Here's an example: +The `Result` type is intended to be used to return the result of a +computation and to have the ability to return an error if it didn't work +out. Here's an example: ```{rust} let x: Result = Ok(2.3f64); let y: Result = Err("There was an error.".to_string()); ``` -This particular Result will return an `f64` if there's a success, and a -`String` if there's a failure. Let's write a function that uses `Result`: +This particular `Result` will return an `f64` upon success and a `String` if +there's a failure. Let's write a function that uses `Result`: ```{rust} fn inverse(x: f64) -> Result { @@ -4858,17 +4858,18 @@ fn inverse(x: f64) -> Result { } ``` -We don't want to take the inverse of zero, so we check to make sure that we -weren't passed zero. If we were, then we return an `Err`, with a message. If -it's okay, we return an `Ok`, with the answer. +We want to indicate that `inverse(0.0f64)` is undefined or is an erroneous usage +of the function, so we check to make sure that we weren't passed zero. If we +were, we return an `Err` with a message. If it's okay, we return an `Ok` with +the answer. Why does this matter? Well, remember how `match` does exhaustive matches? Here's how this function gets used: ```{rust} # fn inverse(x: f64) -> Result { -# if x == 0.0f64 { return Err("x cannot be zero!".to_string()); } -# Ok(1.0f64 / x) +# if x == 0.0f64 { return Err("x cannot be zero!".to_string()); } +# Ok(1.0f64 / x) # } let x = inverse(25.0f64); @@ -4889,8 +4890,8 @@ println!("{}", x + 2.0f64); // error: binary operation `+` cannot be applied ``` This function is great, but there's one other problem: it only works for 64 bit -floating point values. What if we wanted to handle 32 bit floating point as -well? We'd have to write this: +floating point values. If we wanted to handle 32 bit floating point values we'd +have to write this: ```{rust} fn inverse32(x: f32) -> Result { @@ -4900,9 +4901,9 @@ fn inverse32(x: f32) -> Result { } ``` -Bummer. What we need is a **generic function**. Luckily, we can write one! -However, it won't _quite_ work yet. Before we get into that, let's talk syntax. -A generic version of `inverse` would look something like this: +What we need is a **generic function**. We can do that with Rust! However, it +won't _quite_ work yet. We need to talk about syntax. A first attempt at a +generic version of `inverse` might look something like this: ```{rust,ignore} fn inverse(x: T) -> Result { @@ -4912,24 +4913,34 @@ fn inverse(x: T) -> Result { } ``` -Just like how we had `Option`, we use a similar syntax for `inverse`. -We can then use `T` inside the rest of the signature: `x` has type `T`, and half -of the `Result` has type `T`. However, if we try to compile that example, we'll get -an error: +Just like how we had `Option`, we use a similar syntax for `inverse`. We +can then use `T` inside the rest of the signature: `x` has type `T`, and half of +the `Result` has type `T`. However, if we try to compile that example, we'll get +some errors: ```text error: binary operation `==` cannot be applied to type `T` + if x == 0.0 { return Err("x cannot be zero!".to_string()); } + ^~~~~~~~ +error: mismatched types: expected `_`, found `T` (expected floating-point variable, found type parameter) + Ok(1.0 / x) + ^ +error: mismatched types: expected `core::result::Result`, found `core::result::Result<_, _>` (expected type parameter, found floating-point variable) + Ok(1.0 / x) + ^~~~~~~~~~~ ``` -Because `T` can be _any_ type, it may be a type that doesn't implement `==`, -and therefore, the first line would be wrong. What do we do? +The problem is that `T` is unconstrained: it can be _any_ type. It could be a +`String`, and the expression `1.0 / x` has no meaning if `x` is a `String`. It +may be a type that doesn't implement `==`, and the first line would be +wrong. What do we do? -To fix this example, we need to learn about another Rust feature: traits. +To fix this example, we need to learn about another Rust feature: **traits**. # Traits -Do you remember the `impl` keyword, used to call a function with method -syntax? +Our discussion of **traits** begins with the `impl` keyword. We used it before +to specify methods. ```{rust} struct Circle { @@ -4945,8 +4956,8 @@ impl Circle { } ``` -Traits are similar, except that we define a trait with just the method -signature, then implement the trait for that struct. Like this: +We define a trait in terms of its methods. We then `impl` a trait `for` a type +(or many types). ```{rust} struct Circle { @@ -4966,19 +4977,18 @@ impl HasArea for Circle { } ``` -As you can see, the `trait` block looks very similar to the `impl` block, -but we don't define a body, just a type signature. When we `impl` a trait, -we use `impl Trait for Item`, rather than just `impl Item`. +The `trait` block defines only type signatures. When we `impl` a trait, we use +`impl Trait for Item`, rather than just `impl Item`. -So what's the big deal? Remember the error we were getting with our generic -`inverse` function? +The first of the three errors we got with our generic `inverse` function was +this: ```text error: binary operation `==` cannot be applied to type `T` ``` -We can use traits to constrain our generics. Consider this function, which -does not compile, and gives us a similar error: +We can use traits to constrain generic type parameters. Consider this function, +which does not compile, and gives us a similar error: ```{rust,ignore} fn print_area(shape: T) { @@ -4993,8 +5003,9 @@ error: type `T` does not implement any method in scope named `area` ``` Because `T` can be any type, we can't be sure that it implements the `area` -method. But we can add a **trait constraint** to our generic `T`, ensuring -that it does: +method. But we can add a **trait constraint** to our generic `T`, ensuring that +we can only compile the function if it's called with types which `impl` the +`HasArea` trait: ```{rust} # trait HasArea { @@ -5005,9 +5016,9 @@ fn print_area(shape: T) { } ``` -The syntax `` means `any type that implements the HasArea trait`. -Because traits define function type signatures, we can be sure that any type -which implements `HasArea` will have an `.area()` method. +The syntax `` means "any type that implements the HasArea trait." +Because traits define method signatures, we can be sure that any type which +implements `HasArea` will have an `area` method. Here's an extended example of how this works: @@ -5105,55 +5116,22 @@ impl HasArea for int { It is considered poor style to implement methods on such primitive types, even though it is possible. -This may seem like the Wild West, but there are two other restrictions around -implementing traits that prevent this from getting out of hand. First, traits -must be `use`d in any scope where you wish to use the trait's method. So for -example, this does not work: +## Scoped Method Resolution and Orphan `impl`s -```{rust,ignore} -mod shapes { - use std::f64::consts; +There are two restrictions for implementing traits that prevent this from +getting out of hand. - trait HasArea { - fn area(&self) -> f64; - } +1. **Scope-based Method Resolution**: Traits must be `use`d in any scope where + you wish to use the trait's methods +2. **No Orphan `impl`s**: Either the trait or the type you're writing the `impl` + for must be inside your crate. - struct Circle { - x: f64, - y: f64, - radius: f64, - } - - impl HasArea for Circle { - fn area(&self) -> f64 { - consts::PI * (self.radius * self.radius) - } - } -} - -fn main() { - let c = shapes::Circle { - x: 0.0f64, - y: 0.0f64, - radius: 1.0f64, - }; - - println!("{}", c.area()); -} -``` - -Now that we've moved the structs and traits into their own module, we get an -error: - -```text -error: type `shapes::Circle` does not implement any method in scope named `area` -``` - -If we add a `use` line right above `main` and make the right things public, -everything is fine: +If we organize our crate differently by using modules, we'll need to ensure both +of the conditions are satisfied. Don't worry, you can lean on the compiler since +it won't let you get away with violating them. ```{rust} -use shapes::HasArea; +use shapes::HasArea; // satisfies #1 mod shapes { use std::f64::consts; @@ -5175,8 +5153,8 @@ mod shapes { } } - fn main() { + // use shapes::HasArea; // This would satisfy #1, too let c = shapes::Circle { x: 0.0f64, y: 0.0f64, @@ -5187,18 +5165,25 @@ fn main() { } ``` -This means that even if someone does something bad like add methods to `int`, -it won't affect you, unless you `use` that trait. +Requiring us to `use` traits whose methods we want means that even if someone +does something bad like add methods to `int`, it won't affect us, unless you +`use` that trait. -There's one more restriction on implementing traits. Either the trait or the -type you're writing the `impl` for must be inside your crate. So, we could -implement the `HasArea` type for `int`, because `HasArea` is in our crate. But -if we tried to implement `Float`, a trait provided by Rust, for `int`, we could -not, because both the trait and the type aren't in our crate. +The second condition allows us to `impl` built-in `trait`s for types we define, +or allows us to `impl` our own `trait`s for built-in types, but restricts us +from mixing and matching third party or built-in `impl`s with third party or +built-in types. -One last thing about traits: generic functions with a trait bound use -**monomorphization** ("mono": one, "morph": form), so they are statically -dispatched. What's that mean? Well, let's take a look at `print_area` again: +We could `impl` the `HasArea` trait for `int`, because `HasArea` is in our +crate. But if we tried to implement `Float`, a standard library `trait`, for +`int`, we could not, because neither the `trait` nor the `type` are in our +crate. + +## Monomorphization + +One last thing about generics and traits: the compiler performs +**monomorphization** on generic functions so they are statically dispatched. To +see what that means, let's take a look at `print_area` again: ```{rust,ignore} fn print_area(shape: T) { @@ -5215,10 +5200,11 @@ fn main() { } ``` -When we use this trait with `Circle` and `Square`, Rust ends up generating -two different functions with the concrete type, and replacing the call sites with -calls to the concrete implementations. In other words, you get something like -this: +Because we have called `print_area` with two different types in place of its +type paramater `T`, Rust will generate two versions of the function with the +appropriate concrete types, replacing the call sites with calls to the concrete +implementations. In other words, the compiler will actually compile something +more like this: ```{rust,ignore} fn __print_area_circle(shape: Circle) { @@ -5239,10 +5225,12 @@ fn main() { } ``` -The names don't actually change to this, it's just for illustration. But -as you can see, there's no overhead of deciding which version to call here, -hence 'statically dispatched'. The downside is that we have two copies of -the same function, so our binary is a little bit larger. +These names are for illustration; the compiler will generate its own cryptic +names for internal uses. The point is that there is no runtime overhead of +deciding which version to call. The function to be called is determined +statically, at compile time. Thus, generic functions are **statically +dispatched**. The downside is that we have two similar functions, so our binary +is larger. # Threads