1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-06-26 02:31:00 +02:00

Format all Markdown files with dprint (#1157)

This is the result of running `dprint fmt` after removing `src/` from
the list of excluded directories.

This also reformats the Rust code: we might want to tweak this a bit in
the future since some of the changes removes the hand-formatting. Of
course, this formatting can be seen as a mis-feature, so maybe this is
good overall.

Thanks to mdbook-i18n-helpers 0.2, the POT file is nearly unchanged
after this, meaning that all existing translations remain valid! A few
messages were changed because of stray whitespace characters:

     msgid ""
     "Slices always borrow from another object. In this example, `a` has to remain "
    -"'alive' (in scope) for at least as long as our slice. "
    +"'alive' (in scope) for at least as long as our slice."
     msgstr ""

The formatting is enforced in CI and we will have to see how annoying
this is in practice for the many contributors. If it becomes annoying,
we should look into fixing dprint/check#11 so that `dprint` can annotate
the lines that need fixing directly, then I think we can consider more
strict formatting checks.

I added more customization to `rustfmt.toml`. This is to better emulate
the dense style used in the course:

- `max_width = 85` allows lines to take up the full width available in
our code blocks (when taking margins and the line numbers into account).
- `wrap_comments = true` ensures that we don't show very long comments
in the code examples. I edited some comments to shorten them and avoid
unnecessary line breaks — please trim other unnecessarily long comments
when you see them! Remember we're writing code for slides 😄
- `use_small_heuristics = "Max"` allows for things like struct literals
and if-statements to take up the full line width configured above.

The formatting settings apply to all our Rust code right now — I think
we could improve this with https://github.com/dprint/dprint/issues/711
which lets us add per-directory `dprint` configuration files. However,
the `inherit: true` setting is not yet implemented (as far as I can
tell), so a nested configuration file will have to copy most or all of
the top-level file.
This commit is contained in:
Martin Geisler
2023-12-31 00:15:07 +01:00
committed by GitHub
parent f43e72e0ad
commit c9f66fd425
302 changed files with 3067 additions and 2622 deletions

View File

@ -5,8 +5,8 @@ edition = "2021"
publish = false
[dependencies]
thiserror = "*"
anyhow = "*"
thiserror = "*"
[[bin]]
name = "parser"

View File

@ -5,8 +5,8 @@ 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.
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;
@ -34,9 +34,10 @@ fn main() {
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
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.
Make sure to implement the `std::error::Error` trait when defining a custom

View File

@ -101,7 +101,9 @@ enum ParserError {
fn parse(input: &str) -> Result<Expression, ParserError> {
let mut tokens = tokenize(input);
fn parse_expr<'a>(tokens: &mut Tokenizer<'a>) -> Result<Expression, ParserError> {
fn parse_expr<'a>(
tokens: &mut Tokenizer<'a>,
) -> Result<Expression, ParserError> {
let tok = tokens.next().ok_or(ParserError::UnexpectedEOF)??;
let expr = match tok {
Token::Number(num) => {
@ -114,9 +116,11 @@ fn parse(input: &str) -> Result<Expression, ParserError> {
// 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(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)),
})
@ -187,9 +191,11 @@ fn parse(input: &str) -> Expression {
// 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(Token::Operator(op)) => Expression::Operation(
Box::new(expr),
op,
Box::new(parse_expr(tokens)),
),
Some(tok) => panic!("Unexpected token {tok:?}"),
}
}

View File

@ -15,13 +15,14 @@ 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.
- 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>
@ -31,9 +32,7 @@ By default, a panic will cause the stack to unwind. The unwinding can be caught:
use std::panic;
fn main() {
let result = panic::catch_unwind(|| {
"No problem here!"
});
let result = panic::catch_unwind(|| "No problem here!");
println!("{result:?}");
let result = panic::catch_unwind(|| {
@ -43,7 +42,8 @@ fn main() {
}
```
- Catching is unusual; do not attempt to implement exceptions with `catch_unwind`!
- 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`.

View File

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

View File

@ -2,16 +2,18 @@
minutes: 5
---
# `thiserror` and `anyhow`
# `thiserror` and `anyhow`
The [`thiserror`](https://docs.rs/thiserror/) and [`anyhow`](https://docs.rs/anyhow/)
crates are widely 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.
The [`thiserror`](https://docs.rs/thiserror/) and
[`anyhow`](https://docs.rs/anyhow/) crates are widely 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 anyhow::{bail, Context, Result};
use std::{fs, io::Read};
use std::fs;
use std::io::Read;
use thiserror::Error;
#[derive(Clone, Debug, Eq, Error, PartialEq)]
@ -34,7 +36,7 @@ fn main() {
//fs::write("config.dat", "").unwrap();
match read_username("config.dat") {
Ok(username) => println!("Username: {username}"),
Err(err) => println!("Error: {err:?}"),
Err(err) => println!("Error: {err:?}"),
}
}
```
@ -43,20 +45,23 @@ fn main() {
## `thiserror`
* The `Error` derive macro is provided by `thiserror`, and has lots of useful
- The `Error` derive macro is provided by `thiserror`, and has lots of useful
attributes to help define error types in a compact way.
* The `std::error::Error` trait is derived automatically.
* The message from `#[error]` is used to derive the `Display` trait.
- The `std::error::Error` trait is derived automatically.
- The message from `#[error]` is used to derive the `Display` trait.
## `anyhow`
* `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.
* `anyhow::Result<V>` is a type alias for `Result<V, anyhow::Error>`.
* Actual error type inside of it can be extracted for examination if necessary.
* Functionality provided by `anyhow::Result<T>` may be familiar to Go developers, as it provides
similar usage patterns and ergonomics to `(T, error)` from Go.
* `anyhow::Context` is a trait implemented for the standard `Result` and `Option` types.
`use anyhow::Context` is necessary to enable `.context()` and `.with_context()` on those types.
- `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.
- `anyhow::Result<V>` is a type alias for `Result<V, anyhow::Error>`.
- Actual error type inside of it can be extracted for examination if necessary.
- Functionality provided by `anyhow::Result<T>` may be familiar to Go
developers, as it provides similar usage patterns and ergonomics to
`(T, error)` from Go.
- `anyhow::Context` is a trait implemented for the standard `Result` and
`Option` types. `use anyhow::Context` is necessary to enable `.context()` and
`.with_context()` on those types.
</details>

View File

@ -4,7 +4,8 @@ minutes: 5
# Try Conversions
The effective expansion of `?` is a little more complicated than previously indicated:
The effective expansion of `?` is a little more complicated than previously
indicated:
```rust,ignore
expression?
@ -43,7 +44,7 @@ impl Display for ReadUsernameError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::IoError(e) => write!(f, "IO error: {e}"),
Self::EmptyUsername(filename) => write!(f, "Found no username in {filename}"),
Self::EmptyUsername(path) => write!(f, "Found no username in {path}"),
}
}
}
@ -73,13 +74,13 @@ fn main() {
<details>
The `?` operator must return a value compatible with the return type of the
function. For `Result`, it means that the error types have to be compatible.
A function that returns `Result<T, ErrorOuter>` can only use `?` on a value of
function. For `Result`, it means that the error types have to be compatible. A
function that returns `Result<T, ErrorOuter>` can only use `?` on a value of
type `Result<U, ErrorInner>` if `ErrorOuter` and `ErrorInner` are the same type
or if `ErrorOuter` implements `From<ErrorInner>`.
A common alternative to a `From` implementation is `Result::map_err`,
especially when the conversion only happens in one place.
A common alternative to a `From` implementation is `Result::map_err`, especially
when the conversion only happens in one place.
There is no compatibility requirement for `Option`. A function returning
`Option<T>` can use the `?` operator on `Option<U>` for arbitrary `T` and `U`

View File

@ -5,7 +5,7 @@ minutes: 5
# 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
`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
@ -25,8 +25,8 @@ some_expression?
We can use this to simplify our error handling code:
```rust,editable
use std::{fs, io};
use std::io::Read;
use std::{fs, io};
fn read_username(path: &str) -> Result<String, io::Error> {
let username_file_result = fs::File::open(path);
@ -55,8 +55,12 @@ 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.
* 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.
- 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.
- 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>