Skip to content

Commit 9bfb0c6

Browse files
Better example
1 parent 27e3116 commit 9bfb0c6

File tree

1 file changed

+62
-1
lines changed

1 file changed

+62
-1
lines changed

_blogposts/2025-09-01-let-unwrap.mdx

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,76 @@ After long discussions we finally decided on an unwrap syntax for both the `opti
1515

1616
### Example
1717

18+
Before showing off this new feauture, let's explore why it is useful. Consider a chain of `async` functions that are dependent on the result of the previous one. The naive way to write this in modern ReScript with `async`/`await` is to just `switch` on the results.
19+
20+
```res
21+
let getUser = async id =>
22+
switch await fetchUser(id) {
23+
| Error(error) => Error(error)
24+
| Ok(res) =>
25+
switch await decodeUser(res) {
26+
| Error(error) => Error(error)
27+
| Ok(decodedUser) =>
28+
switch await ensureUserActive(decodedUser) {
29+
| Error(error) => Error(error)
30+
| Ok() => Ok(decodedUser)
31+
}
32+
}
33+
}
34+
```
35+
36+
Two observations:
37+
1. with every `switch` expression, this function gets nested deeper.
38+
2. The `Error` branch of every `switch` is just an identity mapper (neither wrapper nor contents change)
39+
40+
This means even though `async`/`await` syntax is available in ReScript for some time now, it is also understandable that people created their own `AsyncResult` libraries to handle such things with less lines of code, e.g.:
41+
42+
```res
43+
let getUser = async id =>
44+
switch await fetchUser(id) {
45+
| Error(error) => Error(error)
46+
| Ok(res) =>
47+
switch await decodeUser(res) {
48+
| Error(error) => Error(error)
49+
| Ok(decodedUser) =>
50+
switch await ensureUserActive(decodedUser) {
51+
| Error(error) => Error(error)
52+
| Ok() => Ok(decodedUser)
53+
}
54+
}
55+
}
56+
```
57+
58+
One way to mitigate this was to use some `ResultPromise` library:
59+
60+
```res
61+
module ResultPromise = {
62+
let flatMapOk = async (p: promise<'res>, f) =>
63+
switch await p {
64+
| Ok(x) => await f(x)
65+
| Error(_) as res => res
66+
}
67+
}
68+
69+
let getUserPromises = id =>
70+
fetchUser(id)
71+
->ResultPromise.flatMapOk(user => Promise.resolve(user->decodeUser))
72+
->ResultPromise.flatMapOk(decodedUser => ensureUserActive(decodedUser))
73+
```
74+
75+
While this is much shorter, it is also harder to understand because we have two wrapper types here, `promise` and `result`. And we have to wrap the non-async type in a `Promise.resolve` in order to stay on the same type level.
76+
1877
```rescript
1978
let getUser = async (id) => {
2079
let? Ok(user) = await fetchUser(id)
2180
let? Ok(decodedUser) = decodeUser(user)
22-
Console.log(`Got user ${decodedUser.name}!`)
2381
let? Ok() = await ensureUserActive(decodedUser)
2482
Ok(decodedUser)
2583
}
2684
```
85+
With the new `let-unwrap` syntax, `let?` in short, we now have to follow the happy-path (in the scope of the function). And it's immediately clear that `fetchUser` is an `async` function while `decodeUser` is not. There is no nesting as the `Error` is automatically mapped. But be assured the error case is also handled as the type checker will complain when you don't handle the `Error` returned by the `getUser` function.
86+
87+
<!-- TODO: demonstrate error handling with polymorphic variants a little more -->
2788

2889
This desugars to a **sequence** of `switch`/early-returns that you’d otherwise write by hand, so there’s **no extra runtime cost** and it plays nicely with `async/await`. Same idea works for `option` with `Some(...)` (and the PR also extends support so the left pattern can be `Error(...)`/`None`, not just `Ok(...)`/`Some(...)`).
2990

0 commit comments

Comments
 (0)