Skip to content

feat: add async exercises #2243

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 31 additions & 24 deletions dev/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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"

Expand Down
10 changes: 10 additions & 0 deletions exercises/21_async/README.md
Original file line number Diff line number Diff line change
@@ -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
44 changes: 44 additions & 0 deletions exercises/21_async/async1.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
42 changes: 42 additions & 0 deletions exercises/21_async/async2.rs
Original file line number Diff line number Diff line change
@@ -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]);
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions exercises/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
64 changes: 52 additions & 12 deletions rustlings-macros/info.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1079,19 +1079,59 @@ 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
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
Expand All @@ -1102,15 +1142,15 @@ 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
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.
Expand All @@ -1127,7 +1167,7 @@ https://veykril.github.io/tlborm/"""

[[exercises]]
name = "clippy1"
dir = "22_clippy"
dir = "23_clippy"
test = false
strict_clippy = true
hint = """
Expand All @@ -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 = """
Expand All @@ -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!"
Expand All @@ -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.
Expand All @@ -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?
Expand All @@ -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."""
45 changes: 45 additions & 0 deletions solutions/21_async/async1.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading