Skip to content

Commit ab91835

Browse files
authored
Merge pull request #15 from sb1752/chp8-clarity
explain required vs provided trait methods based on feedback
2 parents 305dd2a + 10886da commit ab91835

File tree

3 files changed

+61
-59
lines changed

3 files changed

+61
-59
lines changed

08_traits_and_reading_bytes.md

Lines changed: 50 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,44 @@ Notice how we're grabbing different ranges of `transaction_bytes`. We have to re
1212

1313
One way to read a byte stream is to leverage Rust's standard library's [`Read`](https://doc.rust-lang.org/std/io/trait.Read.html) trait. The slice data type in Rust implements the `Read` trait. What does this mean? Well, as we will see, it gives us a method, `read`, which will read some bytes from the slice and then store that data into a array. When we call `read` again, it will start from where it left off. In other words, it keeps track of where we are in the stream and modifies the pointer as it reads. This means we don't need to keep track of any indexes.
1414

15-
## Traits
15+
Let's walk through how this works at a high level with a quick example and then dive deeper into what traits are and how they work.
1616

17-
We've mentioned traits a few times now, but haven't gone into detail about what they are and how they work. We'll get some more practice with them later on, but for now it's enough to understand that traits are a way to define shared behavior. You can think of them as a template for a particular behavior. For example, the `Read` trait provides a template for types that want to "read data". It lays out what to expect and what types of functions are available.
17+
In order to use a trait method we have to first bring it into scope with a `use` statement. In this case, we want to bring the `Read` trait into scope with `use std::io::Read`. The next thing we want to do is use the `read` method as intended based on the example from the [documentation](https://doc.rust-lang.org/std/io/trait.Read.html#examples).
1818

19-
Let's take a closer look at [`Read` from the documentation](https://doc.rust-lang.org/std/io/trait.Read.html). It has a required method, `read`, which has the following function signature: `fn read(&mut self, buf: &mut [u8]) -> Result<usize>;`.
19+
You can follow along with this example in [Rust Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=627e8e7c10d530819a80f189cedded13).
20+
```rust
21+
use std::io::Read;
22+
23+
fn main() {
24+
let mut bytes_slice: &[u8] = [1, 0, 0, 0, 2].as_slice();
25+
let mut buffer = [0; 4];
26+
bytes_slice.read(&mut buffer).unwrap();
27+
28+
let version = u32::from_le_bytes(buffer);
29+
30+
println!("Version: {}", version);
31+
}
32+
```
33+
34+
The `mut` keyword before `bytes_slice` tells Rust the variable is mutable. If we don't provide that keyword in a variable declaration, then the compiler will complain that we're attempting to change an immutable variable, which is not allowed.
35+
36+
You might also notice the `&mut` keyword in the argument to the `read` method. This indicates that we're passing in `buffer` as a *mutable reference*. We'll talk more about this means in the next chapter so for now let's not worry about that nuance.
37+
38+
When we run this, it will print the following:
39+
```console
40+
Version: 1
41+
Bytes slice: [2]
42+
```
43+
44+
And this is what we'd expect. The Version is `1`. And the `bytes_slice` variable has been updated and no longer contains the first 4 bytes.
45+
46+
You may notice that the way this works is that you have to first create an array with a fixed size. Calling `read` will then extract the number of bytes equal to the size of the array, store that into a buffer and then update our slice.
47+
48+
## Traits Explanation
49+
50+
So what are traits exactly? Traits are a way to define shared behavior. You can think of them as a template for a particular set of behaviors. For example, the `Read` trait provides a template for types that want to "read data". It lays out what to expect and what types of functions are available.
51+
52+
Let's take a closer look at the `Read` trait [from the documentation](https://doc.rust-lang.org/std/io/trait.Read.html). It has a required method, `read`, which has the following function signature: `fn read(&mut self, buf: &mut [u8]) -> Result<usize>;`.
2053

2154
```rust
2255
...
@@ -28,9 +61,13 @@ pub trait Read {
2861
...
2962
```
3063

31-
The functions themselves are not actually implemented with any logic. You'll notice there's no function body, just the signature. The types that implement this trait are expected to provide the function logic for each of these methods. So the trait is really just a template.
64+
The `read` method itself is not actually implemented with any logic. You'll notice there's no function body, just the signature. The types that "implement" this trait are expected to provide the function logic for any *required* method, or trait methods that have no implementation.
65+
66+
A trait can also provide other methods that a type can get access to once it has implemented the trait. These are known as *provided* methods and are considered *default* implementations since they can also be overwritten. You'll notice for example that there is a [`read_exact` method](https://doc.rust-lang.org/std/io/trait.Read.html#method.read_exact) which is implemented with a call to the [`default_read_exact`](https://doc.rust-lang.org/src/std/io/mod.rs.html#558) method.
3267

33-
Now, if we look at the `slice` type from the documentation, we can see that it [*implements* `Read`](https://doc.rust-lang.org/std/primitive.slice.html#impl-Read-for-%26%5Bu8%5D) meaning it provides the function logic for the given trait template. Let's take a look at the [implementation](https://doc.rust-lang.org/src/std/io/impls.rs.html#235-250):
68+
As long as a type implements the `Read` trait by providing a `read` method, it will have access to these other *provided* methods. A type can also choose to override some or all of these *provided* methods as well and have its own implementations.
69+
70+
Now if we look at the `slice` type from the documentation, we can see that it [*implements* the `Read` trait](https://doc.rust-lang.org/std/primitive.slice.html#impl-Read-for-%26%5Bu8%5D) and provides the function logic for the `read` method. Let's take a look at [the source code](https://doc.rust-lang.org/src/std/io/impls.rs.html#235-250):
3471

3572
```rust
3673
...
@@ -58,54 +95,12 @@ impl Read for &[u8] {
5895
...
5996
```
6097

61-
Don't worry if you don't understand how to read all of this just yet! Simply notice how we *implement* a trait with the `impl` keyword. So `impl Read for &[u8]` is the code block that provides the function logic for the trait. The other thing to notice is how the function signature matches the trait's function signature.
62-
63-
The idea here is that different types, not just the `&[u8]` type can implement the `Read` trait and then be expected to have a similar behavior. The function logic itself for each type might differ, but they are expected to take in the same arguments, return the same type and generally do the same thing, which in this case is to read some data and modify `self` and the buffer. You might notice some patterns here that you are not yet familiar with, such as the `&mut` keyword and asterisk `*` before `self` at the bottom of the function. Don't worry, we'll go into more detail about what these mean in the next lesson.
64-
65-
For now, let's experiment by following the second example from the [documentation for Read](https://doc.rust-lang.org/std/io/trait.Read.html#examples). We'll comment out our original code and experiment with these new lines:
66-
```rust
67-
fn main() {
68-
let bytes_slice: &[u8] = [1, 0, 0, 0, 2].as_slice();
69-
let mut buffer = [0; 4];
70-
bytes_slice.read(&mut buffer).unwrap();
71-
72-
let version = u32::from_le_bytes(buffer);
73-
74-
println!("Version: {}", version);
75-
println!("Bytes Slice: {:?}", bytes_slice);
76-
77-
// let version = read_version("010000000242d5c1d6f7308bbe95c0f6e1301dd73a8da77d2155b0773bc297ac47f9cd7380010000006a4730440220771361aae55e84496b9e7b06e0a53dd122a1425f85840af7a52b20fa329816070220221dd92132e82ef9c133cb1a106b64893892a11acf2cfa1adb7698dcdc02f01b0121030077be25dc482e7f4abad60115416881fe4ef98af33c924cd8b20ca4e57e8bd5feffffff75c87cc5f3150eefc1c04c0246e7e0b370e64b17d6226c44b333a6f4ca14b49c000000006b483045022100e0d85fece671d367c8d442a96230954cdda4b9cf95e9edc763616d05d93e944302202330d520408d909575c5f6976cc405b3042673b601f4f2140b2e4d447e671c47012103c43afccd37aae7107f5a43f5b7b223d034e7583b77c8cd1084d86895a7341abffeffffff02ebb10f00000000001976a9144ef88a0b04e3ad6d1888da4be260d6735e0d308488ac508c1e000000000017a91476c0c8f2fc403c5edaea365f6a284317b9cdf7258700000000");
78-
// println!("Version: {}", version);
79-
}
80-
```
81-
82-
Try running this now with `cargo run`. This fail to compile. You'll get a compile error that the `read` method is not found for `&[u8]`. This is because the trait implementations only become available when the trait is brought into scope with a `use` import. So you just need to add a `use std::io::Read;` line at the top. Let's add that and run this again.
83-
84-
We'll get another compile error:
85-
```rust
86-
error[E0596]: cannot borrow `bytes_slice` as mutable, as it is not declared as mutable
87-
--> src/main.rs:12:5
88-
|
89-
12 | bytes_slice.read(&mut buffer).unwrap();
90-
| ^^^^^^^^^^^ cannot borrow as mutable
91-
|
92-
help: consider changing this to be mutable
93-
|
94-
10 | let mut bytes_slice: &[u8] = [1, 0, 0, 0, 2].as_slice();
95-
| +++
96-
```
97-
98-
This is a straightforward idea that we haven't talked about until now, but in Rust if any variable is going to be modified, we have to explicitly declare it as *mutable* with the `mut` keyword. This just means that the variable is allowed to change. The `read` method will attempt to modify our `bytes_slice` so we have to declare it as *mutable*.
99-
100-
Ok, if we add that and run it again, it should work and will print out the following:
101-
```shell
102-
Version: 1
103-
Bytes Slice: [2]
104-
```
98+
Don't worry if you don't understand what all of this means just yet! Simply notice how we *implement* a trait with the `impl` keyword. So `impl Read for &[u8]` is the code block that provides the function logic for the trait. The other thing to notice is how the function signature for `read` matches the trait's function signature.
10599

106-
We converted the 4 bytes from the buffer into an unsigned 32-bit integer. And notice how the bytes slice has been modified after being read into the buffer. It no longer contains the first 4 elements, `[1, 0, 0, 0]`.
100+
The idea here is that different types, not just the `&[u8]` type can implement the `Read` trait by providing the function logic for any required method and then be expected to have similar behavior and get access to the trait's provided methods.
101+
The function logic itself for each type might differ, but given the template they are expected to take in the same arguments, return the same type and generally do the same thing, which in this case is to read some data and modify `self` and the buffer.
107102

108-
You may notice that the way this works is that you have to first create an array with a known size. Calling `read` will then extract the number of bytes equal to the size of the array, store that into our buffer and then modify our fat pointer reference to the underlying data.
103+
Again, you might notice some patterns in the code above that you are not yet familiar with, such as the `&mut` keyword and asterisk `*` before `self` at the bottom of the function. We'll go into more detail about what these mean in the next lesson.
109104

110105
Let's now update our program to print out the version number leveraging the `Read` trait. We can convert the `transaction_bytes` `Vec` to a `slice` type using the `as_slice` method. Here is the modified `read_version` function.
111106

@@ -129,12 +124,13 @@ fn main() {
129124
}
130125
```
131126

132-
And voila, this will print `Version: 1` as expected!
127+
And voila, this will print `Version: 1` as expected! Great job so far!
133128

134-
But this doesn't seem ideal. How do we grab the modified `bytes_slice` and continue decoding the transaction? What we probably want to do is pass in the `bytes_slice` into this function as an argument and continue using it in the `main` function. We'll talk more about that and associated Rust concepts of references and borrowing in the next section.
129+
How do we grab the modified `bytes_slice` and continue decoding the transaction? What we probably want to do is pass in the `bytes_slice` into this function as an argument and continue using it in the `main` function. We'll talk more about that and associated Rust concepts of references and borrowing in the next section.
135130

136131
### Quiz
137-
*Consider the following block of code in which we create a Vec and then attempt to print it out:*
132+
1. *Take another look at the `Read` trait and the implementation of the `Read` trait for a slice in the documentation. What are the required and provided methods for the trait? What provided methods are being overwritten by the slice?*
133+
2. *Consider the following block of code in which we create a Vec and then attempt to print it out:*
138134
```rust
139135
fn main() {
140136
let vec: Vec::<u8> = vec![0, 0, 0, 0, 0];

09_00_mutable_references.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ fn main() {
1818

1919
Notice how we first decode the hex string to get a `vec` of bytes. We then convert that into a slice and then pass that into the `read_version` function.
2020

21-
We'll now have to modify the `read_version` function in order to accept the correct argument. What do you think is the correct type for the the argument? Take a moment and then check back here.
21+
We'll now have to modify the `read_version` function to accept the correct argument. What do you think is the correct type for the the argument? Take a moment and then check back here.
2222

2323
<hr/>
2424

quiz_solutions/08_solution.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# 8 Solution
22

33
### Quiz
4-
*Consider the following block of code in which we create a Vec and then attempt to print it out:*
4+
1. *Take another look at the `Read` trait and the implementation of the `Read` trait for a slice in the documentation. What are the required and provided methods for the trait? What provided methods are being overwritten by the slice?*
5+
6+
2. *Consider the following block of code in which we create a Vec and then attempt to print it out:*
57
```rust
68
fn main() {
79
let vec: Vec::<u8> = vec![0, 0, 0, 0, 0];
@@ -14,7 +16,9 @@ fn main() {
1416
*3. Try and implement the correct trait for Vec so that it can be printed for standard display purposes.*
1517

1618
### Solution
17-
If we run this and look at the compiler error, we'll get a better sense of what's going on here.
19+
1. According to the [`Read` trait documentation](https://doc.rust-lang.org/std/io/trait.Read.html), `read` is the only required method. The rest, `read_vectored`, `is_read_vectored`, `read_to_end`, `read_to_string`, `read_exact`, `read_buf`, `read_buf_exact`, `by_ref`, `bytes`, `chain`, and `take` are all provided methods with default implementations. The provided methods that the [slice](https://doc.rust-lang.org/src/std/io/impls.rs.html#233-323) overwrites are `read_vectored`, `is_read_vectored`, `read_to_end`, `read_to_string`, `read_exact` and `read_buf`.
20+
21+
2. If we run this and look at the compiler error, we'll get a better sense of what's going on here.
1822
```console
1923
Compiling playground v0.0.1 (/playground)
2024
error[E0277]: `Vec<{integer}>` doesn't implement `std::fmt::Display`
@@ -38,7 +42,7 @@ pub trait Display {
3842

3943
For now, if we wanted to print a `vec` we would have to use `{:?}` inside the println! string slice argument. This will print the `Debug` output and a `vec` does implement `Debug`.
4044

41-
Let's attempt to implement the `Display` trait for a `vec`:
45+
Let's attempt to implement the `Display` trait for a `vec` by implementing the required method `fmt`:
4246
```rust
4347
use std::fmt;
4448

@@ -58,4 +62,6 @@ fn main() {
5862
}
5963
```
6064

61-
The basic idea is that we leverage the `write!` macro which takes the `Formatter` instance and writes some information to it. If any step fails, an error will be returned. Otherwise, if we iterate through the vector and are able to write all values successfully we can simply return the `Ok(())` result. This might still be a bit confusing at this stage, so consider coming back to revisit this solution after you've gone through the course.
65+
The basic idea is that we leverage the `write!` macro which takes the `Formatter` instance and writes some information to it. If any step fails, an error will be returned (we'll talk more about error handling and the `?` operator in chapter 19). If we iterate through the vector and are able to write all values successfully we can simply return the `Ok(())` result, which matches the the expected result type `fmt::Result`.
66+
67+
This might still be a bit confusing at this stage, so consider coming back to revisit this solution after you've gone through the course.

0 commit comments

Comments
 (0)