Промисы, асинхронность и ожидание в ReScript (с Bun!)
ReScript — это «быстрый, простой, полностью типизированный JavaScript из будущего».
Давайте посмотрим, как использовать обещания, асинхронность и ожидание JavaScript в ReScript, используя Bun v1, чтобы быстро запустить и увидеть наши изменения.
ReScript
ReScript — это строго типизированный язык с синтаксисом, подобным JavaScript, который компилируется в JavaScript.
Начало настройки
Мы будем использовать Bun в качестве менеджера пакетов и запускать наш код во время работы.
- Если у вас еще не установлен Bun, запустите
npm i bun -g
. - Создайте новую папку и откройте VSCode или любую другую IDE по вашему выбору.
- Установите расширение ReScript для вашей IDE.
- Настройте свой проект с помощью
bun init
. Установите точку входаsrc/index.mjs
. - Установите ReScript:
bun install rescript@next @rescript/core
Создайте файл bsconfig.json
для настройки ReScript:
{
"name": "bun-rescript",
"sources": [
{
"dir": "src",
"subdirs": true
}
],
"package-specs": [
{
"module": "es6",
"in-source": true
}
],
"suffix": ".mjs",
"bs-dependencies": [
"@rescript/core"
],
"bsc-flags": [
"-open RescriptCore"
]
}
Создайте файл src/index.res
, который записывает что-то в консоль:
Console.log("starting...")
Запустите bun rescript build -w
в одной вкладке или окне терминала и bun --watch src/index.mjs
в другой.
Теперь у вас есть ReScript, который быстро компилирует файл .res
в файл .mjs
за несколько миллисекунд, а затем Bun запускает этот код за несколько миллисекунд. Это очень хороший цикл быстрой обратной связи, который можно использовать для быстрой разработки.
Промисы
Я предполагаю, что у вас есть базовые знания работы с промисами в JavaScript.
Вот действительно простой пример обещания в ReScript:
let main = () => {
let _ = Promise.resolve(42)->Promise.then(n => Console.log(n)->Promise.resolve)
}
main()
Давайте пройдемся по каждой части кода здесь.
Давайте начнем с понимания того, что происходит с main
частью и частью let _ =
.
let main = () => {
let _ = ...
}
main()
В ReScript каждая строка кода является выражением, а последнее выражение в функции — возвращаемым значением.
let fn = () => {
42 // this function returns 42
}
Примечание. Несмотря на то, что мы не добавляем аннотации типов, ReScript всегда может правильно определить типы, поэтому эта функция имеет сигнатуру типаunit => int
.unit
в ReScript означает, что у нас вообще нет значения, поэтому эта функция не принимает никаких параметров и возвращаетint
.
Любое выражение верхнего уровня должно иметь тип unit
в ReScript, а это означает, что мы не можем вернуть что-либо при вызове функции верхнего уровня, как это происходит с main()
. Итак, мы должны убедиться, что он возвращает тип unit
, и мы можем сделать это, присвоив значение _
. _
— это специальный синтаксис для значения, которое существует, но мы никогда не собираемся его использовать. Если бы мы сделали let x = ...
, компилятор предупредил бы нас, что x
никогда не используется.
Создание обещания выглядит идентично JavaScript:
Promise.resolve(42)
Следующая часть отличается от JS. В ReScript у нас нет цепочки стилей точек, поэтому мы не можем использовать Promise.resolve(42).then(...)
. В ReScript есть каналы, которые мы используем с оператором ->
. Итак, мы берем созданный промис и «перекачиваем» его на следующий шаг — Promise.then
.
Promise.resolve(42)->Promise.then(...)
А внутри Promise.then
мы выходим на консоль и возвращаем результат (то есть unit
) в виде промиса. В ReScript каждый Promise.then
должен возвращать еще один промис. Промисы JavaScript творят чудеса, обрабатывая возврат значения или другого промиса внутри .then
, и поскольку в ReScript тип может быть только одним, мы должны всегда явно возвращать промис. К счастью, в модуле Promise
есть функция thenResolve
, которая может это исправить.
let main = () => {
let _ = Promise.resolve(42)->Promise.thenResolve(n => Console.log(n))
}
main()
Переключение на async/await
Эта функция никогда не может выдать ошибку, поэтому нам не нужно беспокоиться о .catch
, поэтому мы можем безопасно преобразовать ее в синтаксис async
/await
. Если вы хотите избежать ошибок во время выполнения, вам не следует использовать async
/await
, если только вы не хотите обернуть его в блок try
/catch
, который может очень быстро стать некрасивым.
В ReScript async
/await
работает почти так же, как и в JavaScript. Поскольку теперь мы ожидаем, что main()
вернет Promise
, мы можем удалить часть let _ = ...
и заменить ее на await
.
let main = async () => {
await Promise.resolve(42)->Promise.thenResolve(n => Console.log(n))
}
await main()
Давайте заставим его сделать что-нибудь
Вместо того, чтобы возвращать статическое число и регистрировать его, давайте возьмем число и проверим, что оно находится в диапазоне от 0 до 100. Если оно выходит за пределы, мы хотим вернуть его и зарегистрировать ошибку.
let main = async n => {
await Promise.resolve(n)
->Promise.then(n =>
n >= 1 && n <= 100
? Promise.resolve(n)
: Promise.reject(Exn.raiseError("number is out of bounds"))
)
->Promise.thenResolve(n => Console.log(n->Int.toString ++ " is a valid number!"))
}
await main(10)
Мы должны увидеть, что 10 is a valid number!
в консоли Bun, но мы не обработали ошибку должным образом, если присвоили ей неверный номер, и получили исключение во время выполнения.
4 | function raiseError(str) {
5 | throw new Error(str);
^
error: number is out of bounds
at raiseError
Мы можем улучшить это, используя тип Result
ReScript, который представляет собой вариант типа «Ok
» или «Error
».
let main = async n => {
await Promise.resolve(n)
->Promise.thenResolve(n =>
n >= 1 && n <= 100
? Ok(n->Int.toString ++ " is a valid number!")
: Error(n->Int.toString ++ " is out of bounds")
)
->Promise.thenResolve(res =>
switch res {
| Ok(message) => Console.log(message)
| Error(err) => Console.error(err)
}
)
}
await main(1000) // => 1000 is out of bounds
await main(10) // => 10 is a valid number!
Подведение итогов
Теперь у вас должно быть базовое представление о том, как использовать обещания в ReScript. Ключевой момент, на мой взгляд, заключается в том, что нам не нужно выдавать ошибки в наших обещаниях, поскольку у нас есть тип Result
. Для разработчиков и пользователей удобнее фиксировать известные ошибки и корректно их обрабатывать, возвращая их в ответе API или отображая ошибку пользователю в приложении React.
Конечно, случаются неизвестные исключения, но в этом случае мы ожидаем, что номер может быть недействительным. Что, если наша функция была определена здесь и предназначена для использования где-то еще? Давайте перепишем его так, чтобы он возвращал либо число, либо сообщение об ошибке.
let validateQuantity = async n => {
await Promise.resolve(n)->Promise.thenResolve(n =>
n >= 1 && n <= 100
? Ok(n)
: Error(n->Int.toString ++ " is out of bounds and is not a valid quantity.")
)
}
Теперь функция вернет promise<result<int, string>>
, поэтому любой, кто ее использует, знает, что мы ожидаем случай ошибки и можем обработать его соответствующим образом.
Мы даже можем сделать сообщения об ошибках более значимыми, если изменим это на использование сопоставления с образцом:
let validateQuantity = async n => {
await Promise.resolve(n)->Promise.thenResolve(n =>
switch [n >= 1, n <= 100] {
| [false, _] => Error(n->Int.toString ++ " is less than 0 and is not a valid quantity.")
| [_, false] => Error(n->Int.toString ++ " is greater than 100 and is not a valid quantity.")
| _ => Ok(n)
}
)
}
Это позволит нам показывать пользователю значимое сообщение об ошибке, если он попытается сделать что-то недопустимое.
let validateQuantity = async n => {
await Promise.resolve(n)->Promise.thenResolve(n =>
switch [n >= 1, n <= 100] {
| [false, _] => Error(n->Int.toString ++ " is less than 1 and is not a valid quantity.")
| [_, false] => Error(n->Int.toString ++ " is greater than 100 and is not a valid quantity.")
| _ => Ok(n)
}
)
}
let addToCart = async quantity => {
let validatedQuantity = await validateQuantity(quantity)
switch validatedQuantity {
| Ok(n) => Console.log(n->Int.toString ++ " items successfully added to cart!")
| Error(e) => Console.error(e)
}
}
await addToCart(10) // => 10 items successfully added to cart!
await addToCart(1000) // => 1000 is greater than 100 and is not a valid quantity.
await addToCart(0) // => 0 is less than 1 and is not a valid quantity.
ДОПОЛНЕНИЕ от Габриэля Нордеборна, далее следует текст его комментария
Я хотел бы добавить две вещи, которые я считаю крутыми в отношении async/await
в ReScript:
- Вы можете ждать прямо в коммутаторе. Итак, вы могли бы, например, написать
addToCart
так:
let addToCart = async quantity => {
switch await validateQuantity(quantity) {
| Ok(n) => Console.log(n->Int.toString ++ " items successfully added to cart!")
| Error(e) => Console.error(e)
}
}
- Точно так же вы упомянули, что асинхронные действия, которые могут выдать ошибку, требуют набора блоков
try-catch
, которые часто засоряют код. Но в ReScript есть хитрая хитрость, позволяющая упростить это и внутри нашего любимого переключателя. На самом деле вы можете сопоставить шаблон с исключением и обработать это исключение прямо в своем коммутаторе. Представьте, чтоvalidateQuantity
может выбросить. Вы могли бы написать это так:
let addToCart = async quantity => {
switch await validateQuantity(quantity) {
| Ok(n) => Console.log(n->Int.toString ++ " items successfully added to cart!")
| Error(e) => Console.error(e)
| exception Exn.Error(err) => Console.error(err)
}
}
Это скомпилирует в JS try/catch
, которая обработает любую асинхронную ошибку и в то же время позволит вашему коду ReScript оставаться действительно кратким.