Создание Future используя функции-комбинаторы
Используя функции модуля Future можно создать базовые и получить скомбинированные вариации Future. Разберем базовые функции создания.
// Создает Future, которое моментально завершается с переданным значением
let ready = Future.ready "Hello, world!"
// То же что и `Future.ready ()`, только в единственном экземпляре
let unit' = Future.unit'
// Future, которая никогда не завершается
let never = Future.never<_>
// Future, которое выполнит функцию при своем запуске и вернет её результат.
let lazy' = Future.lazy (fun () -> printfn "Hello, world!")
Вышеописанные функции позволяют создать базовые, наиболее простые Future. Они довольно просты и не проявляют свойств асинхронности, и тем не менее, могут быть крайне полезны когда вам необходима Future заглушка или простой способ преобразовать результат или действие в асинхронный примитив.
Все Future можно комбинировать друг с другом используя комбинаторы. Ключевым является понимание комбинатора Future.bind, который позволяет передать результат одного асинхронного вычисления по цепочке в следующее. Рассмотрим его на простом псевдо примере чтения из одного места и записи в другое.
Future.bind имеет сигнатуру (binder: 'a -> Future<'b>) -> fut: Future<'a> -> Future<'b>
и
создает Future которое передаст результат fut в binder
и дождется результата возвращенного из него Future<'b>.
Можно привести в качестве аналога .then из мира JS.
В примере ниже readAndWriteFuture будет иметь следующее поведение при запуске: дождется завершения Future, полученным вызовом readFileAsync, которое читает файл "my-file.txt"; затем создаст новое Future записи в файл через writeFileAsync и дождется его завершения.
// readFileAsync: filePath: string -> Future<string>
// writeFileAsync: filePath: string -> content: string -> Future<unit>
let readAndWriteFuture =
readFileAsync "my-file.txt"
|> Future.bind (fun content -> writeFileAsync "other-file.txt" content)
Также Future.bind могут объединяться в цепочку друг с другом, например так:
let doManyWork =
doWork1 ()
|> Future.bind (fun () -> doWork2 ())
|> Future.bind (fun () -> doWork3 ())
|> ...
|> Future.bind (fun () -> doWorkN ())
let doManyWorkWithResults =
doWork1 ()
|> Future.bind (fun val1 -> doWork2 val1)
|> Future.bind (fun val2 -> doWork3 val2)
|> ...
|> Future.bind (fun valPrevN -> doWorkN valPrevN)
Однако, ситуация сильно усложняется, если единицы работы зависят от результатов друг друга.
let doManyWorkWithCrossResults =
doWork1 ()
|> Future.bind (fun val1 ->
doWork2 val1
|> Future.bind (fun val2 ->
doWork3 val1 val2
|> Future.bind (fun val3 -> ...)))
Future.bind позволяет соединять асинхронные вычисления в последовательную цепочку, и выполнять асинхронную операцию за операцией. Этот процесс можно упростить используя F# Computation Expressions, о чем будет описано ниже. Однако перед этим стоит рассмотреть еще несколько комбинаторов.
// Преобразование значения
let map = Future.map (fun n -> n.ToString()) (Future.ready 12)
// Игнорирование значения
let unitFuture = Future.ignore (Future.ready 12)
// Параллельный запуск с ожиданием обоих (ждет 1000 мс)
let merge = Future.merge (Future.sleepMs 1000) (Future.sleepMs 500)
// Параллельный запуск с получением первого выполненного значения и отменой оставшегося
// (Ждет 500 мс)
let first = Future.first (Future.sleepMs 1000) (Future.sleepMs 500)
// Преобразует Future<Future<'a>> в Future<'a>
let join = Future.join (Future.ready (Future.ready 12))
// Ловит исключение вложенной Future, возвращает Result<'a, exn>
let catch = Future.catch (Future.lazy (fun () -> failwith "exception"))