1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-06-26 10:41:01 +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:
Dustin J. Mitchell
2023-11-29 10:39:24 -05:00
committed by GitHub
parent ea204774b6
commit 6d19292f16
309 changed files with 6807 additions and 4281 deletions

View File

@ -0,0 +1,13 @@
[package]
name = "error-handling"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
thiserror = "*"
anyhow = "*"
[[bin]]
name = "parser"
path = "exercise.rs"

View File

@ -1,19 +0,0 @@
# Converting Error Types
The effective expansion of `?` is a little more complicated than previously indicated:
```rust,ignore
expression?
```
works the same as
```rust,ignore
match expression {
Ok(value) => value,
Err(err) => return Err(From::from(err)),
}
```
The `From::from` call here means we attempt to convert the error type to the
type returned by the function.

View File

@ -1,45 +0,0 @@
# Deriving Error Enums
The [thiserror](https://docs.rs/thiserror/) crate is a popular way to create an
error enum like we did on the previous page:
```rust,editable,compile_fail
use std::{fs, io};
use std::io::Read;
use thiserror::Error;
#[derive(Debug, Error)]
enum ReadUsernameError {
#[error("Could not read: {0}")]
IoError(#[from] io::Error),
#[error("Found no username in {0}")]
EmptyUsername(String),
}
fn read_username(path: &str) -> Result<String, ReadUsernameError> {
let mut username = String::new();
fs::File::open(path)?.read_to_string(&mut username)?;
if username.is_empty() {
return Err(ReadUsernameError::EmptyUsername(String::from(path)));
}
Ok(username)
}
fn main() {
//fs::write("config.dat", "").unwrap();
match read_username("config.dat") {
Ok(username) => println!("Username: {username}"),
Err(err) => println!("Error: {err}"),
}
}
```
<details>
`thiserror`'s derive macro automatically implements `std::error::Error`, and optionally `Display`
(if the `#[error(...)]` attributes are provided) and `From` (if the `#[from]` attribute is added).
It also works for structs.
It doesn't affect your public API, which makes it good for libraries.
</details>

View File

@ -1,41 +0,0 @@
# Dynamic Error Types
Sometimes we want to allow any type of error to be returned without writing our own enum covering
all the different possibilities. `std::error::Error` makes this easy.
```rust,editable,compile_fail
use std::fs;
use std::io::Read;
use thiserror::Error;
use std::error::Error;
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[error("Found no username in {0}")]
struct EmptyUsernameError(String);
fn read_username(path: &str) -> Result<String, Box<dyn Error>> {
let mut username = String::new();
fs::File::open(path)?.read_to_string(&mut username)?;
if username.is_empty() {
return Err(EmptyUsernameError(String::from(path)).into());
}
Ok(username)
}
fn main() {
//fs::write("config.dat", "").unwrap();
match read_username("config.dat") {
Ok(username) => println!("Username: {username}"),
Err(err) => println!("Error: {err}"),
}
}
```
<details>
This saves on code, but gives up the ability to cleanly handle different error cases differently in
the program. As such it's generally not a good idea to use `Box<dyn Error>` in the public API of a
library, but it can be a good option in a program where you just want to display the error message
somewhere.
</details>

View File

@ -0,0 +1,42 @@
---
minutes: 5
---
# Dynamic Error Types
Sometimes we want to allow any type of error to be returned without writing our
own enum covering all the different possibilities. The `std::error::Error`
trait makes it easy to create a trait object that can contain any error.
```rust,editable
use std::error::Error;
use std::fs;
use std::io::Read;
fn read_count(path: &str) -> Result<i32, Box<dyn Error>> {
let mut count_str = String::new();
fs::File::open(path)?.read_to_string(&mut count_str)?;
let count: i32 = count_str.parse()?;
Ok(count)
}
fn main() {
fs::write("count.dat", "1i3").unwrap();
match read_count("count.dat") {
Ok(count) => println!("Count: {count}"),
Err(err) => println!("Error: {err}"),
}
}
```
<details>
The `read_count` function can return `std::io::Error` (from file operations) or
`std::num::ParseIntError` (from `String::parse`).
Boxing errors saves on code, but gives up the ability to cleanly handle different error cases differently in
the program. As such it's generally not a good idea to use `Box<dyn Error>` in the public API of a
library, but it can be a good option in a program where you just want to display the error message
somewhere.
</details>

View File

@ -0,0 +1,20 @@
---
minutes: 20
---
# Exercise: Rewriting with Result
The following implements a very simple parser for an expression language.
However, it handles errors by panicking. Rewrite it to instead use idiomatic
error handling and propagate errors to a return from `main`. Feel free to use
`thiserror` and `anyhow`.
HINT: start by fixing error handling in the `parse` function. Once that is
working correctly, update `Tokenizer` to implement
`Iterator<Item=Result<Token, TokenizerError>>` and handle that in the parser.
```rust,editable
{{#include exercise.rs:types}}
{{#include exercise.rs:panics}}
```

View File

@ -0,0 +1,211 @@
// 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.
// ANCHOR: solution
use thiserror::Error;
// ANCHOR: types
use std::iter::Peekable;
use std::str::Chars;
/// An arithmetic operator.
#[derive(Debug, PartialEq, Clone, Copy)]
enum Op {
Add,
Sub,
}
/// A token in the expression language.
#[derive(Debug, PartialEq)]
enum Token {
Number(String),
Identifier(String),
Operator(Op),
}
/// An expression in the expression language.
#[derive(Debug, PartialEq)]
enum Expression {
/// A reference to a variable.
Var(String),
/// A literal number.
Number(u32),
/// A binary operation.
Operation(Box<Expression>, Op, Box<Expression>),
}
// ANCHOR_END: types
fn tokenize(input: &str) -> Tokenizer {
return Tokenizer(input.chars().peekable());
}
#[derive(Debug, Error)]
enum TokenizerError {
#[error("Unexpected character '{0}' in input")]
UnexpectedCharacter(char),
}
struct Tokenizer<'a>(Peekable<Chars<'a>>);
impl<'a> Iterator for Tokenizer<'a> {
type Item = Result<Token, TokenizerError>;
fn next(&mut self) -> Option<Result<Token, TokenizerError>> {
let Some(c) = self.0.next() else {
return None;
};
match c {
'0'..='9' => {
let mut num = String::from(c);
while let Some(c @ '0'..='9') = self.0.peek() {
num.push(*c);
self.0.next();
}
Some(Ok(Token::Number(num)))
}
'a'..='z' => {
let mut ident = String::from(c);
while let Some(c @ 'a'..='z' | c @ '_' | c @ '0'..='9') = self.0.peek() {
ident.push(*c);
self.0.next();
}
Some(Ok(Token::Identifier(ident)))
}
'+' => Some(Ok(Token::Operator(Op::Add))),
'-' => Some(Ok(Token::Operator(Op::Sub))),
_ => Some(Err(TokenizerError::UnexpectedCharacter(c))),
}
}
}
#[derive(Debug, Error)]
enum ParserError {
#[error("Tokenizer error: {0}")]
TokenizerError(#[from] TokenizerError),
#[error("Unexpected end of input")]
UnexpectedEOF,
#[error("Unexpected token {0:?}")]
UnexpectedToken(Token),
#[error("Invalid number")]
InvalidNumber(#[from] std::num::ParseIntError),
}
fn parse(input: &str) -> Result<Expression, ParserError> {
let mut tokens = tokenize(input);
fn parse_expr<'a>(tokens: &mut Tokenizer<'a>) -> Result<Expression, ParserError> {
let Some(tok) = tokens.next().transpose()? else {
return Err(ParserError::UnexpectedEOF);
};
let expr = match tok {
Token::Number(num) => {
let v = num.parse()?;
Expression::Number(v)
}
Token::Identifier(ident) => Expression::Var(ident),
Token::Operator(_) => return Err(ParserError::UnexpectedToken(tok)),
};
// Look ahead to parse a binary operation if present.
Ok(match tokens.next() {
None => expr,
Some(Ok(Token::Operator(op))) => {
Expression::Operation(Box::new(expr), op, Box::new(parse_expr(tokens)?))
}
Some(Err(e)) => return Err(e.into()),
Some(Ok(tok)) => return Err(ParserError::UnexpectedToken(tok)),
})
}
parse_expr(&mut tokens)
}
fn main() -> anyhow::Result<()> {
let expr = parse("10+foo+20-30")?;
println!("{expr:?}");
Ok(())
}
// ANCHOR_END: solution
/*
// ANCHOR: panics
fn tokenize(input: &str) -> Tokenizer {
return Tokenizer(input.chars().peekable());
}
struct Tokenizer<'a>(Peekable<Chars<'a>>);
impl<'a> Iterator for Tokenizer<'a> {
type Item = Token;
fn next(&mut self) -> Option<Token> {
let Some(c) = self.0.next() else {
return None;
};
match c {
'0'..='9' => {
let mut num = String::from(c);
while let Some(c @ '0'..='9') = self.0.peek() {
num.push(*c);
self.0.next();
}
Some(Token::Number(num))
}
'a'..='z' => {
let mut ident = String::from(c);
while let Some(c @ 'a'..='z' | c @ '_' | c @ '0'..='9') = self.0.peek() {
ident.push(*c);
self.0.next();
}
Some(Token::Identifier(ident))
}
'+' => Some(Token::Operator(Op::Add)),
'-' => Some(Token::Operator(Op::Sub)),
_ => panic!("Unexpected character {c}"),
}
}
}
fn parse(input: &str) -> Expression {
let mut tokens = tokenize(input);
fn parse_expr<'a>(tokens: &mut Tokenizer<'a>) -> Expression {
let Some(tok) = tokens.next() else {
panic!("Unexpected end of input");
};
let expr = match tok {
Token::Number(num) => {
let v = num.parse().expect("Invalid 32-bit integer'");
Expression::Number(v)
}
Token::Identifier(ident) => Expression::Var(ident),
Token::Operator(_) => panic!("Unexpected token {tok:?}"),
};
// Look ahead to parse a binary operation if present.
match tokens.next() {
None => expr,
Some(Token::Operator(op)) => {
Expression::Operation(Box::new(expr), op, Box::new(parse_expr(tokens)))
}
Some(tok) => panic!("Unexpected token {tok:?}"),
}
}
parse_expr(&mut tokens)
}
fn main() {
let expr = parse("10+foo+20-30");
println!("{expr:?}");
}
// ANCHOR_END: panics
*/

View File

@ -1,23 +0,0 @@
# Catching the Stack Unwinding
By default, a panic will cause the stack to unwind. The unwinding can be caught:
```rust,editable
use std::panic;
fn main() {
let result = panic::catch_unwind(|| {
"No problem here!"
});
println!("{result:?}");
let result = panic::catch_unwind(|| {
panic!("oh no!");
});
println!("{result:?}");
}
```
- This can be useful in servers which should keep running even if a single
request crashes.
- This does not work if `panic = 'abort'` is set in your `Cargo.toml`.

View File

@ -1,5 +1,11 @@
---
minutes: 3
---
# Panics
Rust handles fatal errors with a "panic".
Rust will trigger a panic if a fatal error happens at runtime:
```rust,editable,should_panic
@ -11,4 +17,35 @@ fn main() {
* Panics are for unrecoverable and unexpected errors.
* Panics are symptoms of bugs in the program.
* Runtime failures like failed bounds checks can panic
* Assertions (such as `assert!`) panic on failure
* Purpose-specific panics can use the `panic!` macro.
* A panic will "unwind" the stack, dropping values just as if the functions had returned.
* Use non-panicking APIs (such as `Vec::get`) if crashing is not acceptable.
<details>
By default, a panic will cause the stack to unwind. The unwinding can be caught:
```rust,editable
use std::panic;
fn main() {
let result = panic::catch_unwind(|| {
"No problem here!"
});
println!("{result:?}");
let result = panic::catch_unwind(|| {
panic!("oh no!");
});
println!("{result:?}");
}
```
- Catching is unusual; do not attempt to implement exceptions with `catch_unwind`!
- This can be useful in servers which should keep running even if a single
request crashes.
- This does not work if `panic = 'abort'` is set in your `Cargo.toml`.
</details>

View File

@ -1,33 +0,0 @@
# Structured Error Handling with `Result`
We have already seen the `Result` enum. This is used pervasively when errors are
expected as part of normal operation:
```rust,editable
use std::fs;
use std::io::Read;
fn main() {
let file = fs::File::open("diary.txt");
match file {
Ok(mut file) => {
let mut contents = String::new();
file.read_to_string(&mut contents);
println!("Dear diary: {contents}");
},
Err(err) => {
println!("The diary could not be opened: {err}");
}
}
}
```
<details>
* As with `Option`, the successful value sits inside of `Result`, forcing the developer to
explicitly extract it. This encourages error checking. In the case where an error should never happen,
`unwrap()` or `expect()` can be called, and this is a signal of the developer intent too.
* `Result` documentation is a recommended read. Not during the course, but it is worth mentioning.
It contains a lot of convenience methods and functions that help functional-style programming.
</details>

View File

@ -0,0 +1,6 @@
# Solution
<!-- compile_fail because `mdbook test` does not allow use of `thiserror` -->
```rust,editable,compile_fail
{{#include exercise.rs:solution}}
```

View File

@ -1,14 +1,23 @@
# Adding Context to Errors
---
minutes: 5
---
The widely used [anyhow](https://docs.rs/anyhow/) crate can help you add
contextual information to your errors and allows you to have fewer
custom error types:
# `thiserror` and `anyhow`
The [`thiserror`](https://docs.rs/thiserror/) and [`anyhow`](https://docs.rs/anyhow/)
crates are widley used to simplify error handling. `thiserror` helps
create custom error types that implement `From<T>`. `anyhow` helps with error
handling in functions, including adding contextual information to your errors.
```rust,editable,compile_fail
use std::{fs, io};
use std::io::Read;
use anyhow::{Context, Result, bail};
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[error("Found no username in {0}")]
struct EmptyUsernameError(String);
fn read_username(path: &str) -> Result<String> {
let mut username = String::with_capacity(100);
fs::File::open(path)
@ -16,7 +25,7 @@ fn read_username(path: &str) -> Result<String> {
.read_to_string(&mut username)
.context("Failed to read")?;
if username.is_empty() {
bail!("Found no username in {path}");
bail!(EmptyUsernameError(path));
}
Ok(username)
}
@ -32,6 +41,8 @@ fn main() {
<details>
* The `Error` derive macro is provided by `thiserror`, and has lots of useful
attributes like `#[error]` to help define a useful error type.
* `anyhow::Result<V>` is a type alias for `Result<V, anyhow::Error>`.
* `anyhow::Error` is essentially a wrapper around `Box<dyn Error>`. As such it's again generally not
a good choice for the public API of a library, but is widely used in applications.

View File

@ -1,4 +1,29 @@
# Converting Error Types
---
minutes: 5
---
# Try Conversions
The effective expansion of `?` is a little more complicated than previously indicated:
```rust,ignore
expression?
```
works the same as
```rust,ignore
match expression {
Ok(value) => value,
Err(err) => return Err(From::from(err)),
}
```
The `From::from` call here means we attempt to convert the error type to the
type returned by the function. This makes it easy to encapsulate errors into
higher-level errors.
## Example
```rust,editable
use std::error::Error;
@ -47,10 +72,15 @@ fn main() {
<details>
Key points:
The return type of the function has to be compatible with the nested functions it calls. For instance,
a function returning a `Result<T, Err>` can only apply the `?` operator on a function returning a
`Result<AnyT, Err>`. It cannot apply the `?` operator on a function returning an `Option<AnyT>` or `Result<T, OtherErr>`
unless `OtherErr` implements `From<Err>`. Reciprocally, a function returning an `Option<T>` can only apply the `?` operator
on a function returning an `Option<AnyT>`.
You can convert incompatible types into one another with the different `Option` and `Result` methods
such as `Option::ok_or`, `Result::ok`, `Result::err`.
* The `username` variable can be either `Ok(string)` or `Err(error)`.
* Use the `fs::write` call to test out the different scenarios: no file, empty file, file with username.
It is good practice for all error types that don't need to be `no_std` to implement `std::error::Error`, which requires `Debug` and `Display`. The `Error` crate for `core` is only available in [nightly](https://github.com/rust-lang/rust/issues/103765), so not fully `no_std` compatible yet.
@ -58,4 +88,6 @@ It's generally helpful for them to implement `Clone` and `Eq` too where possible
life easier for tests and consumers of your library. In this case we can't easily do so, because
`io::Error` doesn't implement them.
A common alternative to a `From` implementation is `Result::map_err`, especially when the conversion only happens in one place.
</details>

View File

@ -1,7 +1,13 @@
# Propagating Errors with `?`
---
minutes: 5
---
The try-operator `?` is used to return errors to the caller. It lets you turn
the common
# Try Operator
Runtime errors like connection-refused or file-not-found are handled with the
`Result` type, but matching this type on every call can be cumbersome. The
try-operator `?` is used to return errors to the caller. It lets you turn the
common
```rust,ignore
match some_expression {
@ -45,16 +51,12 @@ fn main() {
<details>
Simplify the `read_username` function to use `?`.
Key points:
* The `username` variable can be either `Ok(string)` or `Err(error)`.
* Use the `fs::write` call to test out the different scenarios: no file, empty file, file with username.
* The return type of the function has to be compatible with the nested functions it calls. For instance,
a function returning a `Result<T, Err>` can only apply the `?` operator on a function returning a
`Result<AnyT, Err>`. It cannot apply the `?` operator on a function returning an `Option<AnyT>` or `Result<T, OtherErr>`
unless `OtherErr` implements `From<Err>`. Reciprocally, a function returning an `Option<T>` can only apply the `?` operator
on a function returning an `Option<AnyT>`.
* You can convert incompatible types into one another with the different `Option` and `Result` methods
such as `Option::ok_or`, `Result::ok`, `Result::err`.
* Note that `main` can return a `Result<(), E>` as long as it implements `std::process:Termination`. In practice, this means that `E` implements `Debug`. The executable will print the `Err` variant and return a nonzero exit status on error.
</details>