You've already forked comprehensive-rust
mirror of
https://github.com/google/comprehensive-rust.git
synced 2025-06-24 17:56:45 +02:00
Comprehensive Rust v2 (#1073)
I've taken some work by @fw-immunant and others on the new organization of the course and condensed it into a form amenable to a text editor and some computational analysis. You can see the inputs in `course.py` but the interesting bits are the output: `outline.md` and `slides.md`. The idea is to break the course into more, smaller segments with exercises at the ends and breaks in between. So `outline.md` lists the segments, their duration, and sums those durations up per-day. It shows we're about an hour too long right now! There are more details of the segments in `slides.md`, or you can see mostly the same stuff in `course.py`. This now contains all of the content from the v1 course, ensuring both that we've covered everything and that we'll have somewhere to redirect every page. Fixes #1082. Fixes #1465. --------- Co-authored-by: Nicole LeGare <dlegare.1001@gmail.com> Co-authored-by: Martin Geisler <mgeisler@google.com>
This commit is contained in:
committed by
GitHub
parent
ea204774b6
commit
6d19292f16
9
src/pattern-matching/Cargo.toml
Normal file
9
src/pattern-matching/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "pattern-matching"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "eval"
|
||||
path = "exercise.rs"
|
@ -1,37 +0,0 @@
|
||||
# Destructuring Arrays
|
||||
|
||||
You can destructure arrays, tuples, and slices by matching on their elements:
|
||||
|
||||
```rust,editable
|
||||
{{#include ../../third_party/rust-by-example/destructuring-arrays.rs}}
|
||||
```
|
||||
|
||||
|
||||
<details>
|
||||
|
||||
* Destructuring of slices of unknown length also works with patterns of fixed length.
|
||||
|
||||
|
||||
```rust,editable
|
||||
fn main() {
|
||||
inspect(&[0, -2, 3]);
|
||||
inspect(&[0, -2, 3, 4]);
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
fn inspect(slice: &[i32]) {
|
||||
println!("Tell me about {slice:?}");
|
||||
match slice {
|
||||
&[0, y, z] => println!("First is 0, y = {y}, and z = {z}"),
|
||||
&[1, ..] => println!("First is 1 and the rest were ignored"),
|
||||
_ => println!("All elements were ignored"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Create a new pattern using `_` to represent an element.
|
||||
* Add more values to the array.
|
||||
* Point out that how `..` will expand to account for different number of elements.
|
||||
* Show matching against the tail with patterns `[.., b]` and `[a@..,b]`
|
||||
|
||||
</details>
|
@ -1,16 +0,0 @@
|
||||
# Destructuring Structs
|
||||
|
||||
You can also destructure `structs`:
|
||||
|
||||
```rust,editable
|
||||
{{#include ../../third_party/rust-by-example/destructuring-structs.rs}}
|
||||
```
|
||||
<details>
|
||||
|
||||
* Change the literal values in `foo` to match with the other patterns.
|
||||
* Add a new field to `Foo` and make changes to the pattern as needed.
|
||||
* The distinction between a capture and a constant expression can be hard to
|
||||
spot. Try changing the `2` in the second arm to a variable, and see that it subtly
|
||||
doesn't work. Change it to a `const` and see it working again.
|
||||
|
||||
</details>
|
@ -1,4 +1,17 @@
|
||||
# Destructuring Enums
|
||||
---
|
||||
minutes: 10
|
||||
---
|
||||
|
||||
# Destructuring
|
||||
|
||||
Like tuples, structs and enums can also be destructured by matching:
|
||||
|
||||
## Structs
|
||||
|
||||
```rust,editable
|
||||
{{#include ../../third_party/rust-by-example/destructuring-structs.rs}}
|
||||
```
|
||||
## Enums
|
||||
|
||||
Patterns can also be used to bind variables to parts of your values. This is how
|
||||
you inspect the structure of your types. Let us start with a simple `enum` type:
|
||||
@ -32,8 +45,20 @@ arm, `half` is bound to the value inside the `Ok` variant. In the second arm,
|
||||
|
||||
<details>
|
||||
|
||||
# Structs
|
||||
|
||||
* Change the literal values in `foo` to match with the other patterns.
|
||||
* Add a new field to `Foo` and make changes to the pattern as needed.
|
||||
* The distinction between a capture and a constant expression can be hard to
|
||||
spot. Try changing the `2` in the second arm to a variable, and see that it subtly
|
||||
doesn't work. Change it to a `const` and see it working again.
|
||||
|
||||
# Enums
|
||||
|
||||
Key points:
|
||||
* The `if`/`else` expression is returning an enum that is later unpacked with a `match`.
|
||||
* You can try adding a third variant to the enum definition and displaying the errors when running the code. Point out the places where your code is now inexhaustive and how the compiler tries to give you hints.
|
||||
* The values in the enum variants can only be accessed after being pattern matched. The pattern binds references to the fields in the "match arm" after the `=>`.
|
||||
* Demonstrate what happens when the search is inexhaustive. Note the advantage the Rust compiler provides by confirming when all cases are handled.
|
||||
|
||||
</details>
|
46
src/pattern-matching/exercise.md
Normal file
46
src/pattern-matching/exercise.md
Normal file
@ -0,0 +1,46 @@
|
||||
---
|
||||
minutes: 30
|
||||
---
|
||||
|
||||
# Exercise: Expression Evaluation
|
||||
|
||||
Let's write a simple recursive evaluator for arithmetic expressions. Start with
|
||||
an enum defining the binary operations:
|
||||
|
||||
```rust
|
||||
{{#include exercise.rs:Operation}}
|
||||
|
||||
{{#include exercise.rs:Expression}}
|
||||
|
||||
{{#include exercise.rs:Res}}
|
||||
|
||||
{{#include exercise.rs:eval}}
|
||||
todo!()
|
||||
}
|
||||
|
||||
{{#include exercise.rs:tests}}
|
||||
```
|
||||
|
||||
The `Box` type here is a smart pointer, and will be covered in detail later in
|
||||
the course. An expression can be "boxed" with `Box::new` as seen in the tests.
|
||||
To evaluate a boxed expression, use the deref operator to "unbox" it:
|
||||
`eval(*boxed_expr)`.
|
||||
|
||||
Some expressions cannot be evaluated and will return an error. The `Res`
|
||||
type represents either a successful value or an error with a message. This is
|
||||
very similar to the standard-library `Result` which we will see later.
|
||||
|
||||
Copy and paste the code into the Rust playground, and begin implementing
|
||||
`eval`. The final product should pass the tests. It may be helpful to use
|
||||
`todo!()` and get the tests to pass one-by-one. You can also skip a test
|
||||
temporarily with
|
||||
`#[ignore]`:
|
||||
|
||||
```none
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_value() { .. }
|
||||
```
|
||||
|
||||
If you finish early, try writing a test that results in an integer overflow.
|
||||
How could you handle this with `Res::Err` instead of a panic?
|
152
src/pattern-matching/exercise.rs
Normal file
152
src/pattern-matching/exercise.rs
Normal file
@ -0,0 +1,152 @@
|
||||
// Copyright 2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![allow(dead_code)]
|
||||
// ANCHOR: solution
|
||||
// ANCHOR: Operation
|
||||
/// An operation to perform on two subexpressions.
|
||||
#[derive(Debug)]
|
||||
enum Operation {
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div,
|
||||
}
|
||||
// ANCHOR_END: Operation
|
||||
|
||||
// ANCHOR: Expression
|
||||
/// An expression, in tree form.
|
||||
#[derive(Debug)]
|
||||
enum Expression {
|
||||
/// An operation on two subexpressions.
|
||||
Op {
|
||||
op: Operation,
|
||||
left: Box<Expression>,
|
||||
right: Box<Expression>,
|
||||
},
|
||||
|
||||
/// A literal value
|
||||
Value(i64),
|
||||
}
|
||||
// ANCHOR_END: Expression
|
||||
|
||||
// ANCHOR: Res
|
||||
/// The result of evaluating an expression.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Res {
|
||||
/// Evaluation was successful, with the given result.
|
||||
Ok(i64),
|
||||
/// Evaluation failed, with the given error message.
|
||||
Err(String),
|
||||
}
|
||||
// Allow `Ok` and `Err` as shorthands for `Res::Ok` and `Res::Err`.
|
||||
use Res::{Err, Ok};
|
||||
// ANCHOR_END: Res
|
||||
|
||||
// ANCHOR: eval
|
||||
fn eval(e: Expression) -> Res {
|
||||
// ANCHOR_END: eval
|
||||
match e {
|
||||
Expression::Op { op, left, right } => {
|
||||
let left = match eval(*left) {
|
||||
Ok(v) => v,
|
||||
Err(msg) => return Err(msg),
|
||||
};
|
||||
let right = match eval(*right) {
|
||||
Ok(v) => v,
|
||||
Err(msg) => return Err(msg),
|
||||
};
|
||||
Ok(match op {
|
||||
Operation::Add => left + right,
|
||||
Operation::Sub => left - right,
|
||||
Operation::Mul => left * right,
|
||||
Operation::Div => {
|
||||
if right == 0 {
|
||||
return Err(String::from("division by zero"));
|
||||
} else {
|
||||
left / right
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Expression::Value(v) => Ok(v),
|
||||
}
|
||||
}
|
||||
|
||||
// ANCHOR: tests
|
||||
#[test]
|
||||
fn test_value() {
|
||||
assert_eq!(eval(Expression::Value(19)), Ok(19));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sum() {
|
||||
assert_eq!(
|
||||
eval(Expression::Op {
|
||||
op: Operation::Add,
|
||||
left: Box::new(Expression::Value(10)),
|
||||
right: Box::new(Expression::Value(20)),
|
||||
}),
|
||||
Ok(30)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recursion() {
|
||||
let term1 = Expression::Op {
|
||||
op: Operation::Mul,
|
||||
left: Box::new(Expression::Value(10)),
|
||||
right: Box::new(Expression::Value(9)),
|
||||
};
|
||||
let term2 = Expression::Op {
|
||||
op: Operation::Mul,
|
||||
left: Box::new(Expression::Op {
|
||||
op: Operation::Sub,
|
||||
left: Box::new(Expression::Value(3)),
|
||||
right: Box::new(Expression::Value(4)),
|
||||
}),
|
||||
right: Box::new(Expression::Value(5)),
|
||||
};
|
||||
assert_eq!(
|
||||
eval(Expression::Op {
|
||||
op: Operation::Add,
|
||||
left: Box::new(term1),
|
||||
right: Box::new(term2),
|
||||
}),
|
||||
Ok(85)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error() {
|
||||
assert_eq!(
|
||||
eval(Expression::Op {
|
||||
op: Operation::Div,
|
||||
left: Box::new(Expression::Value(99)),
|
||||
right: Box::new(Expression::Value(0)),
|
||||
}),
|
||||
Err(String::from("division by zero"))
|
||||
);
|
||||
}
|
||||
// ANCHOR_END: tests
|
||||
|
||||
fn main() {
|
||||
let expr = Expression::Op {
|
||||
op: Operation::Sub,
|
||||
left: Box::new(Expression::Value(20)),
|
||||
right: Box::new(Expression::Value(10)),
|
||||
};
|
||||
println!("expr: {:?}", expr);
|
||||
println!("result: {:?}", eval(expr));
|
||||
}
|
124
src/pattern-matching/let-control-flow.md
Normal file
124
src/pattern-matching/let-control-flow.md
Normal file
@ -0,0 +1,124 @@
|
||||
---
|
||||
minutes: 10
|
||||
---
|
||||
|
||||
# Let Control Flow
|
||||
|
||||
Rust has a few control flow constructs which differ from other languages. They
|
||||
are used for pattern matching:
|
||||
|
||||
- `if let` expressions
|
||||
- `while let` expressions
|
||||
- `match` expressions
|
||||
# `if let` expressions
|
||||
|
||||
The [`if let`
|
||||
expression](https://doc.rust-lang.org/reference/expressions/if-expr.html#if-let-expressions)
|
||||
lets you execute different code depending on whether a value matches a pattern:
|
||||
|
||||
```rust,editable
|
||||
fn sleep_for(secs: f32) {
|
||||
let dur = if let Ok(dur) = std::time::Duration::try_from_secs_f32(secs) {
|
||||
dur
|
||||
} else {
|
||||
std::time::Duration::from_millis(500)
|
||||
};
|
||||
std::thread::sleep(dur);
|
||||
println!("slept for {:?}", dur);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
sleep_for(-10.0);
|
||||
sleep_for(0.8);
|
||||
}
|
||||
```
|
||||
|
||||
For the common case of matching a pattern and returning from the function, use
|
||||
[`let
|
||||
else`](https://doc.rust-lang.org/rust-by-example/flow_control/let_else.html).
|
||||
The "else" case must diverge (`return`, `break`, or panic - anything but
|
||||
falling off the end of the block).
|
||||
|
||||
```rust,editable
|
||||
fn hex_or_die_trying(maybe_string: Option<String>) -> Result<u32, String> {
|
||||
let s = if let Some(s) = maybe_string {
|
||||
s
|
||||
} else {
|
||||
return Err(String::from("got None"));
|
||||
};
|
||||
|
||||
let first_byte_char = if let Some(first_byte_char) = s.chars().next() {
|
||||
first_byte_char
|
||||
} else {
|
||||
return Err(String::from("got empty string"));
|
||||
};
|
||||
|
||||
if let Some(digit) = first_byte_char.to_digit(16) {
|
||||
Ok(digit)
|
||||
} else {
|
||||
Err(String::from("not a hex digit"))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("result: {:?}", hex_or_die_trying(Some(String::from("foo"))));
|
||||
}
|
||||
```
|
||||
|
||||
Like with `if let`, there is a [`while let`](https://doc.rust-lang.org/reference/expressions/loop-expr.html#predicate-pattern-loops)
|
||||
variant which repeatedly tests a value against a pattern:
|
||||
|
||||
<!-- mdbook-xgettext: skip -->
|
||||
```rust,editable
|
||||
fn main() {
|
||||
let mut name = String::from("Comprehensive Rust 🦀");
|
||||
while let Some(c) = name.pop() {
|
||||
println!("character: {c}");
|
||||
}
|
||||
// (There are more efficient ways to reverse a string!)
|
||||
}
|
||||
```
|
||||
|
||||
Here
|
||||
[`String::pop`](https://doc.rust-lang.org/stable/std/string/struct.String.html#method.pop)
|
||||
returns `Some(c)` until the string is empty, after which it will return `None`.
|
||||
The `while let` lets us keep iterating through all items.
|
||||
|
||||
<details>
|
||||
|
||||
## if-let
|
||||
|
||||
* Unlike `match`, `if let` does not have to cover all branches. This can make it more concise than `match`.
|
||||
* A common usage is handling `Some` values when working with `Option`.
|
||||
* Unlike `match`, `if let` does not support guard clauses for pattern matching.
|
||||
|
||||
## let-else
|
||||
|
||||
`if-let`s can pile up, as shown. The `let-else` construct supports flattening this nested code.
|
||||
Rewrite the awkward version for students, so they can see the transformation.
|
||||
|
||||
The rewritten version is:
|
||||
```rust
|
||||
fn hex_or_die_trying(maybe_string: Option<String>) -> Result<u32, String> {
|
||||
let Some(s) = maybe_string else {
|
||||
return Err(String::from("got None"));
|
||||
};
|
||||
|
||||
let Some(first_byte_char) = s.chars().next() else {
|
||||
return Err(String::from("got empty string"));
|
||||
};
|
||||
|
||||
let Some(digit) = first_byte_char.to_digit(16) else {
|
||||
return Err(String::from("not a hex digit"));
|
||||
};
|
||||
|
||||
return Ok(digit);
|
||||
}
|
||||
```
|
||||
|
||||
# while-let
|
||||
|
||||
* Point out that the `while let` loop will keep going as long as the value matches the pattern.
|
||||
* You could rewrite the `while let` loop as an infinite loop with an if statement that breaks when there is no value to unwrap for `name.pop()`. The `while let` provides syntactic sugar for the above scenario.
|
||||
|
||||
</details>
|
@ -1,18 +0,0 @@
|
||||
# Match Guards
|
||||
|
||||
When matching, you can add a _guard_ to a pattern. This is an arbitrary Boolean
|
||||
expression which will be executed if the pattern matches:
|
||||
|
||||
```rust,editable
|
||||
{{#include ../../third_party/rust-by-example/match-guards.rs}}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
Key Points:
|
||||
* Match guards as a separate syntax feature are important and necessary when we wish to concisely express more complex ideas than patterns alone would allow.
|
||||
* They are not the same as separate `if` expression inside of the match arm. An `if` expression inside of the branch block (after `=>`) happens after the match arm is selected. Failing the `if` condition inside of that block won't result in other arms
|
||||
of the original `match` expression being considered.
|
||||
* You can use the variables defined in the pattern in your if expression.
|
||||
* The condition defined in the guard applies to every expression in a pattern with an `|`.
|
||||
</details>
|
5
src/pattern-matching/solution.md
Normal file
5
src/pattern-matching/solution.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Solution
|
||||
|
||||
```rust,editable
|
||||
{{#include exercise.rs:solution}}
|
||||
```
|
Reference in New Issue
Block a user