Skip to content

Commit b4e4b11

Browse files
committed
Coroutines!
1 parent f3c192a commit b4e4b11

File tree

2 files changed

+187
-24
lines changed

2 files changed

+187
-24
lines changed

tutorials/coroutines.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# Coroutines
2+
3+
[Coroutines](https://en.cppreference.com/w/cpp/language/coroutines) are an underutilized feature of C++20 that not many understand. Under the hood, they are very complex, but luckily Geode makes it quite simple for you. Geode lets you leverage the power of coroutines to write clean asynchronous code, tackle Result propagation, and build Python-style generators with ease.
4+
5+
## Task and CoTask
6+
7+
For most asynchronous tasks, Geode provides the Task class. See [`Tasks`](/tutorials/tasks) for more information. Any function that returns a Task can be converted into a coroutine by simply using `co_await` on a different task.
8+
9+
Here's an example of a simple coroutine:
10+
11+
```cpp
12+
#include <Geode/utils/web.hpp>
13+
#include <Geode/utils/coro.hpp>
14+
15+
Task<int> getResponseCode() {
16+
auto res = co_await web::WebRequest().get("https://google.com");
17+
18+
co_return res.code();
19+
}
20+
```
21+
22+
By using `co_await`, we no longer have to set up an event listener or anything too complicated. The best part is, you can use multiple co_await statements within the same place instead of nesting a thousand callbacks!
23+
24+
There are a few specific things you should be aware of when using this syntax:
25+
* The body of the coroutine is ran in the main thread, possibly only in the next frame.
26+
* If the task the coroutine is waiting on is cancelled, the whole coroutine is cancelled
27+
* If the task returned by the coroutine is cancelled, any pending task that is running is cancelled
28+
29+
You can also send progress values using `co_yield`
30+
31+
```cpp
32+
Task<std::string, int> someTask() {
33+
for (int i = 0; i < 10; i++) {
34+
co_yield i;
35+
}
36+
co_return "done!";
37+
}
38+
```
39+
40+
One problem that stems from using Task for asynchronous handling is that it cannot support void. Sometimes you want a function to run asynchronously without returning a value, and for that we introduce `coro::CoTask`. CoTask is exactly like Task but it allows void:
41+
42+
```cpp
43+
#include <Geode/utils/web.hpp>
44+
#include <Geode/utils/coro.hpp>
45+
46+
coro::CoTask<void> logResponseCode() {
47+
auto req = web::WebRequest();
48+
auto res = co_await req.get("https://google.com");
49+
50+
log::info("Response code: {}", res.code());
51+
}
52+
```
53+
54+
Creating a new function for just the asynchronous bits might get tedious. Luckily, you don't have to with the `$async` macro:
55+
56+
```cpp
57+
void logResponseCode(std::string const& url) {
58+
log::info("Starting request...");
59+
60+
$async(url) {
61+
auto req = web::WebRequest();
62+
auto res = co_await req.get(url);
63+
64+
log::info("Response code: {}", res.code());
65+
};
66+
}
67+
```
68+
69+
Under the hood, `$async` sets a coroutine lambda and immediately invokes it. Any arguments within the macro body are lambda captures, so you could just as easily put `=` in there too.
70+
71+
## Result propagation
72+
73+
The most annoying part of working with Results is propagation. In Rust, it's as easy as using the `?` operator, but we don't have such nice things in C++. Lucky for us, Geode implements coroutines for Result that allow for easy propagation:
74+
75+
```cpp
76+
#include <Geode/utils/coro.hpp>
77+
#include <Geode/Result.hpp>
78+
#include <matjson.hpp>
79+
80+
// Parse {"myarray": [1, 2, 3]} and return sum
81+
Result<int> parseMyJson(matjson::Value& root) {
82+
auto name = co_await root.get("myarray");
83+
auto numbers = co_await name.asArray();
84+
85+
int output = 0;
86+
for (auto& number : numbers) {
87+
output += co_await number.asInt();
88+
}
89+
90+
co_return Ok(output);
91+
}
92+
```
93+
94+
Here, `co_await` isn't really awaiting anything at all. Instead it's being used as a way to suspend execution and return if the underlying Result contains an error. If the Result is Ok, it extracts the value and continues execution. This allows you to write freely without having to manually check for errors everywhere.
95+
96+
With the help of another macro, `$try`, you can extend this functionality into non-coroutines as well:
97+
98+
```cpp
99+
// Default value of 0
100+
int parseMyJson(matjson::Value& root) {
101+
auto res = $try<int> {
102+
auto name = co_await root.get("name");
103+
auto numbers = co_await name.asArray();
104+
105+
int output = 0;
106+
for (auto& number : numbers) {
107+
output += co_await number.asInt();
108+
}
109+
co_return Ok(output);
110+
};
111+
112+
return res.unwrapOr(0);
113+
}
114+
```
115+
116+
Here, once again the `$try` macro sets up a coroutine lambda that gets immediately invoked. This time, since the lambda is guaranteed to be evaluated immediately, `$try` automatically captures everything by reference. The `int` template value represents the Ok output. You can use this to chain multiple Result evaluations together and do error checking as a group.
117+
118+
## Generators
119+
120+
When you need to perform operations on a series of values, it's sometimes preferred to lazily evaluate them instead of collecting everything into a vector. The `coro::Generator` object allows you to create coroutines that lazily yield values, just like Python generators.
121+
122+
Here's what a basic fibbonacci generator looks like:
123+
124+
```cpp
125+
#include <Geode/utils/coro.hpp>
126+
127+
coro::Generator<int> range(int start, int end) {
128+
for (int i = start; i < end; ++i) {
129+
co_yield i;
130+
}
131+
}
132+
133+
for (int i : range(0, 10)) {
134+
log::info("My number: {}", i);
135+
}
136+
```
137+
138+
This will log numbers 0 through 9. Generators have a `begin()` and `end()` function that returns an STL-compatible iterator, meaning you can use them with any read-only standard library function that takes in an iterator:
139+
140+
```cpp
141+
auto gen = range(0, 6);
142+
auto sum = std::accumulate(gen.begin(), gen.end(), 0, std::plus()); // Sums all numbers from 0 to 199
143+
```
144+
145+
One benefit of generators over returning vectors is that they can be infinite. You don't need to worry about a massive memory footprint from yielding arbitrary amounts of values, your only roadblock is time.
146+
147+
Here's an example of an infinite fibbonacci generator:
148+
149+
```cpp
150+
#include <Geode/utils/coro.hpp>
151+
152+
coro::Generator<int> fibbonacci() {
153+
int a = 0;
154+
int b = 1;
155+
while (true) {
156+
co_yield a;
157+
auto next = a + b;
158+
a = b;
159+
b = next;
160+
}
161+
}
162+
163+
164+
for (int num : fibbonacci()) {
165+
if (num > 1000) break;
166+
log::info("My number: {}", num);
167+
}
168+
```
169+
170+
The caller of the generator is the one that determines when the sequence ends, not the callee, giving you lots of flexibility.
171+
172+
Generators have two helper functions that quickly allow you to apply transformations: map and filter. These transformation functions yield more generators, allowing you to chain them as you please. You can use them on any generator to transform their output like so:
173+
174+
```cpp
175+
// Prints 0, -1, -2, ...
176+
for (int num : range(0, 10).map(std::negate())) {
177+
log::info("My number: {}", num);
178+
}
179+
180+
// Prints only even numbers
181+
for (int num : range(0, 10).filter([](int n) { return n % 2 == 0; })) {
182+
log::info("My number: {}", num);
183+
}
184+
```
185+
186+
You can use the `coro::makeGenerator` function to construct a generator based on a vector

tutorials/tasks.md

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -277,27 +277,4 @@ Task<std::string> newTask =
277277
278278
## Coroutines
279279
280-
Tasks can be used in [C++20 coroutines](https://en.cppreference.com/w/cpp/language/coroutines), easily allowing for multiple asynchronous calls to happen within the same code. Note that this may have a little performance overhead compared to regular Task code.
281-
282-
```cpp
283-
Task<int> someTask() {
284-
auto response = co_await web::WebRequest().get("https://example.com");
285-
co_return response.code();
286-
}
287-
```
288-
289-
There are a few specific things you should be aware of when using this syntax:
290-
* The body of the coroutine is ran in the main thread, possibly only in the next frame.
291-
* If the task the coroutine is waiting on is cancelled, the whole coroutine is cancelled
292-
* If the task returned by the coroutine is cancelled, any pending task that is running is cancelled
293-
294-
You can also send progress values using `co_yield`
295-
296-
```cpp
297-
Task<std::string, int> someTask() {
298-
for (int i = 0; i < 10; i++) {
299-
co_yield i;
300-
}
301-
co_return "done!";
302-
}
303-
```
280+
Tasks can be used in [C++20 coroutines](https://en.cppreference.com/w/cpp/language/coroutines), easily allowing for multiple asynchronous calls to happen within the same code. Note that this may have a little performance overhead compared to regular Task code. See [Coroutines](/tutorials/coroutines) for more information.

0 commit comments

Comments
 (0)