Skip to content

Commit ca364ed

Browse files
Review
1 parent 5c74840 commit ca364ed

File tree

1 file changed

+79
-3
lines changed

1 file changed

+79
-3
lines changed

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

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Two observations:
3535
1. with every `switch` expression, this function gets nested deeper.
3636
2. The `Error` branch of every `switch` is just an identity mapper (neither wrapper nor contents change).
3737

38-
The only alternative in ReScript was always to use some specialized methods:
38+
The only alternative in ReScript was always to use some specialized functions:
3939

4040
```res
4141
let getUserPromises = id =>
@@ -46,12 +46,60 @@ let getUserPromises = id =>
4646

4747
**Note**: `Result.flatMapOkAsync` among some other async result helper functions are brand new in ReScript 12 as well!
4848

49-
This is arguably better, more concise, but 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. Also we are giving up on our precious `async`/`await` syntax here.
49+
This is arguably better, more concise, but 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. Also we are giving up on our precious `async`/`await` syntax here. Furthermore, those functions result in two more function calls.
50+
51+
```js
52+
function getUserPromises(id) {
53+
return Stdlib_Result.flatMapOkAsync(
54+
Stdlib_Result.flatMapOkAsync(fetchUser(id), (user) =>
55+
Promise.resolve(decodeUser(user))
56+
),
57+
ensureUserActive
58+
);
59+
}
60+
```
61+
62+
Let's have a look at the generated JS from the initial example in comparison:
63+
64+
```js
65+
async function getUserNestedSwitches(id) {
66+
let error = await fetchUser(id);
67+
if (error.TAG !== "Ok") {
68+
return {
69+
TAG: "Error",
70+
_0: error._0,
71+
};
72+
}
73+
let error$1 = decodeUser(error._0);
74+
if (error$1.TAG !== "Ok") {
75+
return {
76+
TAG: "Error",
77+
_0: error$1._0,
78+
};
79+
}
80+
let decodedUser = error$1._0;
81+
let error$2 = await ensureUserActive(decodedUser);
82+
if (error$2.TAG === "Ok") {
83+
return {
84+
TAG: "Ok",
85+
_0: decodedUser,
86+
};
87+
} else {
88+
return {
89+
TAG: "Error",
90+
_0: error$2._0,
91+
};
92+
}
93+
}
94+
```
95+
5096

5197
## Introducing `let?`
5298

5399
Let's rewrite the above example again with our new syntax:
54100

101+
<CodeTab labels={["ReScript", "JS Output"]} experiments="LetUnwrap">
102+
55103
```rescript
56104
let getUser = async (id) => {
57105
let? Ok(user) = await fetchUser(id)
@@ -62,11 +110,37 @@ let getUser = async (id) => {
62110
}
63111
```
64112

113+
```js
114+
async function getUser(id) {
115+
let e = await fetchUser(id);
116+
if (e.TAG !== "Ok") {
117+
return e;
118+
}
119+
let e$1 = decodeUser(e._0);
120+
if (e$1.TAG !== "Ok") {
121+
return e$1;
122+
}
123+
let decodedUser = e$1._0;
124+
console.log(`Got user ` + decodedUser.name + `!`);
125+
let e$2 = await ensureUserActive(decodedUser);
126+
if (e$2.TAG === "Ok") {
127+
return {
128+
TAG: "Ok",
129+
_0: decodedUser,
130+
};
131+
} else {
132+
return e$2;
133+
}
134+
}
135+
```
136+
137+
</CodeTab>
138+
65139
This strikes a balance between conciseness and simplicity that we really think fits ReScript well.
66140

67141
With `let?`, we can now safely focus on the the happy-path in the scope of the function. 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.
68142

69-
This desugars to a **sequence** of early-returns that you’d otherwise write by hand, so there’s **no extra runtime cost** and it plays nicely with `async/await` as the example above suggests.
143+
This desugars to a **sequence** of early-returns that you’d otherwise write by hand, so there’s **no extra runtime cost** and it plays nicely with `async/await` as the example above suggests. Check the `JS Output` tab to see for yourself!
70144

71145
Of course, it also works for `option` with `Some(...)`.
72146

@@ -98,6 +172,8 @@ let? Ok(user) = await fetchUser("1")
98172
// ^^^^^^^ ERROR: `let?` is not allowed for top-level bindings.
99173
```
100174

175+
**Note**: `result` and `option` types cannot be mixed in a `let?` function!
176+
101177
### A full example with error handling
102178

103179
<CodeTab labels={["ReScript", "JS Output"]} experiments="LetUnwrap">

0 commit comments

Comments
 (0)