feat: add async exercises

This commit is contained in:
liv 2025-05-16 15:41:59 +02:00
parent e73fff3bd4
commit b2b3005670
35 changed files with 268 additions and 36 deletions

View File

@ -164,30 +164,34 @@ bin = [
{ name = "threads2_sol", path = "../solutions/20_threads/threads2.rs" },
{ name = "threads3", path = "../exercises/20_threads/threads3.rs" },
{ name = "threads3_sol", path = "../solutions/20_threads/threads3.rs" },
{ name = "macros1", path = "../exercises/21_macros/macros1.rs" },
{ name = "macros1_sol", path = "../solutions/21_macros/macros1.rs" },
{ name = "macros2", path = "../exercises/21_macros/macros2.rs" },
{ name = "macros2_sol", path = "../solutions/21_macros/macros2.rs" },
{ name = "macros3", path = "../exercises/21_macros/macros3.rs" },
{ name = "macros3_sol", path = "../solutions/21_macros/macros3.rs" },
{ name = "macros4", path = "../exercises/21_macros/macros4.rs" },
{ name = "macros4_sol", path = "../solutions/21_macros/macros4.rs" },
{ name = "clippy1", path = "../exercises/22_clippy/clippy1.rs" },
{ name = "clippy1_sol", path = "../solutions/22_clippy/clippy1.rs" },
{ name = "clippy2", path = "../exercises/22_clippy/clippy2.rs" },
{ name = "clippy2_sol", path = "../solutions/22_clippy/clippy2.rs" },
{ name = "clippy3", path = "../exercises/22_clippy/clippy3.rs" },
{ name = "clippy3_sol", path = "../solutions/22_clippy/clippy3.rs" },
{ name = "using_as", path = "../exercises/23_conversions/using_as.rs" },
{ name = "using_as_sol", path = "../solutions/23_conversions/using_as.rs" },
{ name = "from_into", path = "../exercises/23_conversions/from_into.rs" },
{ name = "from_into_sol", path = "../solutions/23_conversions/from_into.rs" },
{ name = "from_str", path = "../exercises/23_conversions/from_str.rs" },
{ name = "from_str_sol", path = "../solutions/23_conversions/from_str.rs" },
{ name = "try_from_into", path = "../exercises/23_conversions/try_from_into.rs" },
{ name = "try_from_into_sol", path = "../solutions/23_conversions/try_from_into.rs" },
{ name = "as_ref_mut", path = "../exercises/23_conversions/as_ref_mut.rs" },
{ name = "as_ref_mut_sol", path = "../solutions/23_conversions/as_ref_mut.rs" },
{ name = "async1", path = "../exercises/21_async/async1.rs" },
{ name = "async1_sol", path = "../solutions/21_async/async1.rs" },
{ name = "async2", path = "../exercises/21_async/async2.rs" },
{ name = "async2_sol", path = "../solutions/21_async/async2.rs" },
{ name = "macros1", path = "../exercises/22_macros/macros1.rs" },
{ name = "macros1_sol", path = "../solutions/22_macros/macros1.rs" },
{ name = "macros2", path = "../exercises/22_macros/macros2.rs" },
{ name = "macros2_sol", path = "../solutions/22_macros/macros2.rs" },
{ name = "macros3", path = "../exercises/22_macros/macros3.rs" },
{ name = "macros3_sol", path = "../solutions/22_macros/macros3.rs" },
{ name = "macros4", path = "../exercises/22_macros/macros4.rs" },
{ name = "macros4_sol", path = "../solutions/22_macros/macros4.rs" },
{ name = "clippy1", path = "../exercises/23_clippy/clippy1.rs" },
{ name = "clippy1_sol", path = "../solutions/23_clippy/clippy1.rs" },
{ name = "clippy2", path = "../exercises/23_clippy/clippy2.rs" },
{ name = "clippy2_sol", path = "../solutions/23_clippy/clippy2.rs" },
{ name = "clippy3", path = "../exercises/23_clippy/clippy3.rs" },
{ name = "clippy3_sol", path = "../solutions/23_clippy/clippy3.rs" },
{ name = "using_as", path = "../exercises/24_conversions/using_as.rs" },
{ name = "using_as_sol", path = "../solutions/24_conversions/using_as.rs" },
{ name = "from_into", path = "../exercises/24_conversions/from_into.rs" },
{ name = "from_into_sol", path = "../solutions/24_conversions/from_into.rs" },
{ name = "from_str", path = "../exercises/24_conversions/from_str.rs" },
{ name = "from_str_sol", path = "../solutions/24_conversions/from_str.rs" },
{ name = "try_from_into", path = "../exercises/24_conversions/try_from_into.rs" },
{ name = "try_from_into_sol", path = "../solutions/24_conversions/try_from_into.rs" },
{ name = "as_ref_mut", path = "../exercises/24_conversions/as_ref_mut.rs" },
{ name = "as_ref_mut_sol", path = "../solutions/24_conversions/as_ref_mut.rs" },
]
[package]
@ -196,6 +200,9 @@ edition = "2024"
# Don't publish the exercises on crates.io!
publish = false
[dependencies]
tokio = { version = "1.45.0", features = ["rt-multi-thread", "macros"] }
[profile.release]
panic = "abort"

View File

@ -0,0 +1,10 @@
# Async
Rust includes built-in support for asynchronous programming. In other languages, this might be known as Promises or
Coroutines. async programming uses async functions, which are powerful, but may require some getting used to,
especially if you haven't used something similar in another language.
The [relevant book chapter][1] is essential reading. The [tokio docs][2] are also very helpful!
[1]: https://doc.rust-lang.org/book/ch17-00-async-await.html
[2]: https://tokio.rs/tokio/tutorial

View File

@ -0,0 +1,44 @@
// Our loyal worker works hard to create a new number.
#[derive(Default)]
struct Worker;
struct NumberContainer {
number: i32,
}
impl Worker {
async fn work(&self) -> NumberContainer {
// Pretend this takes a while...
let new_number = 32;
NumberContainer { number: new_number }
}
}
impl NumberContainer {
async fn extract_number(&self) -> i32 {
// And this too...
self.number
}
}
// TODO: Fix the function signature!
fn run_worker() -> i32 {
// TODO: Make our worker create a new number and return it.
}
fn main() {
// Feel free to experiment here. You may need to make some adjustments
// to this function, though.
}
mod tests {
use super::*;
// Don't worry about this attribute for now.
// If you want to know what this does, read the hint!
#[tokio::test]
async fn test_if_it_works() {
let number = run_worker().await;
assert_eq!(number, 32);
}
}

View File

@ -0,0 +1,42 @@
use tokio::task::JoinSet;
// A MultiWorker can work with the power of 5 normal workers,
// allowing us to create 5 new numbers at once!
struct MultiWorker;
impl MultiWorker {
async fn start_work(&self) -> JoinSet<i32> {
let mut set = JoinSet::new();
for i in 30..35 {
// TODO: `set.spawn` accepts an async function that will return the number
// we want. Implement this function as a closure!
set.spawn(???);
}
set
}
}
async fn run_multi_worker() -> Vec<i32> {
let tasks = MultiWorker.start_work().await;
// TODO: We have a bunch of tasks, how do we run them to completion
// to get at the i32s they create?
}
fn main() {
// Feel free to experiment here. You may need to make some adjustments
// to this function, though.
}
mod tests {
use super::*;
#[tokio::test]
async fn test_if_it_works() {
let mut numbers = run_multi_worker().await;
numbers.sort(); // in case tasks run out-of-order
assert_eq!(numbers, vec![30, 31, 32, 33, 34]);
}
}

View File

@ -22,6 +22,7 @@
| iterators | §13.2-4 |
| smart_pointers | §15, §16.3 |
| threads | §16.1-3 |
| async | §17 |
| macros | §19.5 |
| clippy | §21.4 |
| conversions | n/a |

View File

@ -1079,11 +1079,51 @@ original sending end.
Related section in The Book:
https://doc.rust-lang.org/book/ch16-02-message-passing.html"""
# ASYNC
[[exercises]]
name = "async1"
dir = "21_async"
test = true
hint = """
Async functions are not the same as normal functions -- they have to be marked with a
special bit of syntax, `async fn`. These functions don't immediately return or even
execute, you have to encourage them to do so by calling another special bit of syntax
on them.
Another thing - an async function can't be properly called in a normal function. Think of
it as something contagious -- everything that it touches needs to be marked as such. Keeping
that in mind, what adjustment do you need to make to the function signature?
Aside:
`#[tokio::test]` (and `#[tokio::main]`) are "magic" attributes that automatically
set up what we call an async runtime. The Rust compiler intentionally doesn't supply
a default implementation of this runtime. Tokio is by far the most popular
community-developed runtime, and this macro does a lot of the heavy lifting to let
us use it.
"""
[[exercises]]
name = "async2"
dir = "21_async"
test = true
hint = """
Async functions can be used to run multiple things in parallel, or to more efficiently
run things on multiple cores. Here, we use Tokio's tasks to schedule some work to run
at the same time. We use a `JoinSet`, which is a list of tasks that lets us decide how
to best execute them.
One of the ways to execute tasks is `JoinSet::join_all`, which even gives us a neat
Vec that we can immediately return! You can also do this sequentially, with an iterator.
See if you can also find a way to do it that doesn't use `JoinSet`! You have access to
most of Tokio's task-based functionality here.
"""
# MACROS
[[exercises]]
name = "macros1"
dir = "21_macros"
dir = "22_macros"
test = false
hint = """
When you call a macro, you need to add something special compared to a regular
@ -1091,7 +1131,7 @@ function call."""
[[exercises]]
name = "macros2"
dir = "21_macros"
dir = "22_macros"
test = false
hint = """
Macros don't quite play by the same rules as the rest of Rust, in terms of
@ -1102,7 +1142,7 @@ Unlike other things in Rust, the order of "where you define a macro" versus
[[exercises]]
name = "macros3"
dir = "21_macros"
dir = "22_macros"
test = false
hint = """
In order to use a macro outside of its module, you need to do something
@ -1110,7 +1150,7 @@ special to the module to lift the macro out into its parent."""
[[exercises]]
name = "macros4"
dir = "21_macros"
dir = "22_macros"
test = false
hint = """
You only need to add a single character to make this compile.
@ -1127,7 +1167,7 @@ https://veykril.github.io/tlborm/"""
[[exercises]]
name = "clippy1"
dir = "22_clippy"
dir = "23_clippy"
test = false
strict_clippy = true
hint = """
@ -1144,7 +1184,7 @@ appropriate replacement constant from `std::f32::consts`."""
[[exercises]]
name = "clippy2"
dir = "22_clippy"
dir = "23_clippy"
test = false
strict_clippy = true
hint = """
@ -1157,7 +1197,7 @@ https://doc.rust-lang.org/std/option/#iterating-over-option"""
[[exercises]]
name = "clippy3"
dir = "22_clippy"
dir = "23_clippy"
test = false
strict_clippy = true
hint = "No hints this time!"
@ -1166,20 +1206,20 @@ hint = "No hints this time!"
[[exercises]]
name = "using_as"
dir = "23_conversions"
dir = "24_conversions"
hint = """
Use the `as` operator to cast one of the operands in the last line of the
`average` function into the expected return type."""
[[exercises]]
name = "from_into"
dir = "23_conversions"
dir = "24_conversions"
hint = """
Follow the steps provided right before the `From` implementation."""
[[exercises]]
name = "from_str"
dir = "23_conversions"
dir = "24_conversions"
hint = """
The implementation of `FromStr` should return an `Ok` with a `Person` object,
or an `Err` with an error if the string is not valid.
@ -1196,7 +1236,7 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen
[[exercises]]
name = "try_from_into"
dir = "23_conversions"
dir = "24_conversions"
hint = """
Is there an implementation of `TryFrom` in the standard library that can both do
the required integer conversion and check the range of the input?
@ -1206,6 +1246,6 @@ types?"""
[[exercises]]
name = "as_ref_mut"
dir = "23_conversions"
dir = "24_conversions"
hint = """
Add `AsRef<str>` or `AsMut<u32>` as a trait bound to the functions."""

View File

@ -0,0 +1,45 @@
// Our loyal worker works hard to create a new number.
#[derive(Default)]
struct Worker;
struct NumberContainer {
number: i32,
}
impl Worker {
async fn work(&self) -> NumberContainer {
// Pretend this takes a while...
let new_number = 32;
NumberContainer { number: new_number }
}
}
impl NumberContainer {
async fn extract_number(&self) -> i32 {
// And this too...
self.number
}
}
async fn run_worker() -> i32 {
// TODO: Make our worker create a new number and return it.
Worker.work().await.extract_number().await
}
fn main() {
// Feel free to experiment here. You may need to make some adjustments
// to this function, though.
}
mod tests {
use super::*;
// Don't worry about this attribute for now.
// If you want to know what this does, read the hint!
#[tokio::test]
// TODO: Fix the test function signature
fn test_if_it_works() {
let number = run_worker().await;
assert_eq!(number, 32);
}
}

View File

@ -0,0 +1,43 @@
use tokio::task::JoinSet;
// A MultiWorker can work with the power of 5 normal workers,
// allowing us to create 5 new numbers at once!
struct MultiWorker;
impl MultiWorker {
async fn start_work(&self) -> JoinSet<i32> {
let mut set = JoinSet::new();
for i in 30..35 {
// TODO: `set.spawn` accepts an async function that will return the number
// we want. Implement this function as a closure!
set.spawn(async move { i });
}
set
}
}
async fn run_multi_worker() -> Vec<i32> {
let tasks = MultiWorker.start_work().await;
// TODO: We have a bunch of tasks, how do we run them to completion
// to get at the i32s they create?
tasks.join_all().await
}
fn main() {
// Feel free to experiment here. You may need to make some adjustments
// to this function, though.
}
mod tests {
use super::*;
#[tokio::test]
async fn test_if_it_works() {
let mut numbers = run_multi_worker().await;
numbers.sort(); // in case tasks run out-of-order
assert_eq!(numbers, vec![30, 31, 32, 33, 34]);
}
}