DevGang
Авторизоваться

Промисы, асинхронность и ожидание в 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 оставаться действительно кратким.

Источник:

#JavaScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу