diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index cad5520..e897227 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,3 +1,9 @@ # FSharp.Control.Futures [Introduction](ru/README.md) + + +# Basics + +- [Creating](ru/basics/creating.md) +- [Running](ru/basics/running.md) diff --git a/docs/ru/README.md b/docs/ru/README.md index b8f349e..0ed3ef4 100644 --- a/docs/ru/README.md +++ b/docs/ru/README.md @@ -1,27 +1,25 @@ - # Futures -Futures это экспериментальная F# библиотека асинхронного программирования, +FSharp.Control.Futures это экспериментальная F# библиотека асинхронного программирования, вдохновленная Rust трейтом Future. -## Features -- Дизайн Future позволяет отменять любые асинхронные операции - без телодвижений со стороны программиста и CancellationToken-ов -- Модель опроса делает возможным полное отсутствие блокировок в комбинаторах - и не требует выделения память под обратные вызовы. -- Явные точки прерывания -- Future является "холодной" (вычисление начинается только после запуска). - - -Библиотека предоставляет асинхронные аналоги следующих примитивов +Концептуально Future являются таким же примитивом асинхронного программирования +как C# Task или F# Async, поэтому если вы знакомы с ними, +начать работать с Future'ами должно быть максимально просто. -| Асинхронная версия | Синхронная версия | -| ------------------ |-------------------| -| Future<'a> | unit -> 'a | -| Stream<'a> | IEnumerator<'a> | +## Особенности дизайна Future +- Future является "холодной" (вычисление начинается только после явного запуска). +- Возможность отмеы без явной передачи CancellationToken. +- Всегда явные точки прерывания. +- Отсутствие блокировок в базовых комбинаторах. +- Не требует выделения памяти под обратные вызовы, только выделения самих Future. ----- -1: если замыкание внутри Thread-safe +## Сравнение Task, Async, Future +| | Task | Async | Future | +|--------------------|--------------------------|---------------------------|---------------------| +| Тип | Горячие | Холодные | Холодные | +| Отмена | Явный CancellationToken | Неявный CancellationToken | Вызов метода отмены | +| Хвостовая рекурсия | Нет | Да | Да | diff --git a/docs/ru/basics/creating.md b/docs/ru/basics/creating.md new file mode 100644 index 0000000..32ad7de --- /dev/null +++ b/docs/ru/basics/creating.md @@ -0,0 +1,218 @@ +# Создание объекта Future + +Создать асинхронное вычисление в лице Future можно большим количеством способов. + + +## Использование функций-комбинаторов + +Используя функции модуля Future можно создать базовые и получить скомбинированные +вариации Future. Разберем базовые функции создания. + +```fsharp +// Создает 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 и дождется его завершения. + +```fsharp +// readFileAsync: filePath: string -> Future +// writeFileAsync: filePath: string -> content: string -> Future +let readAndWriteFuture = + readFileAsync "my-file.txt" + |> Future.bind (fun content -> writeFileAsync "other-file.txt" content) +``` + +Также Future.bind могут объединяться в цепочку друг с другом, например так: +```fsharp +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) +``` + +Однако, ситуация сильно усложняется, если единицы работы зависят от результатов друг друга. +```fsharp +let doManyWorkWithCrossResults = + doWork1 () + |> Future.bind (fun val1 -> + doWork2 val1 + |> Future.bind (fun val2 -> + doWork3 val1 val2 + |> Future.bind (fun val 3 -> ...))) +``` + +Future.bind позволяет соединять асинхронные вычисления в последовательную цепочку, +и выполнять асинхронную операцию за операцией. Этот процесс можно упростить используя +F# Computation Expressions, о чем будет описано ниже. +Однако перед этим стоит рассмотреть еще несколько комбинаторов. + +```fsharp +// Преобразование значения +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> +let join = Future.join (Future.ready (Future.ready 12)) + +// Ловит исключение вложенной Future, возвращает Result<'a, exn> +let catch = Future.catch (Future.lazy (fun () -> failwith "exception")) +``` + +
+Future это уже объект машины состояний. По этой причине множественное использование +(комбинирование или запуск) одного экземпляра Future недопустимы. +То есть следующий код, запускающий fut дважды недопустим +```fsharp +let fut = someAsyncWork () +let doubleFut = fut |> Future.bind (fun () -> fut) +``` +Вместо этого всегда пересоздавайте Future при необходимости двойного использования: +```fsharp +let doubleFut = someAsyncWork () |> Future.bind (fun () -> someAsyncWork ()) +``` +
+ + +## Использование Future CE + +Future имеет свой CE, который используется также как async или task CE встроенные в F#. +Более подробно о CE вы можете прочитать на [сайте](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions). + +Например, мы можем заменить базовые функции создания на future CE: +```fsharp +let ready = future { return "Hello, world!" } // ~ Future.ready "Hello, world!" +let lazy' = future { return (foo ()) } // ~ Future.lazy' (fun () -> foo ()) +``` + +Наиболее важным свойством CE является упрощение работы с bind. +Пример чтения-записи можно переписать используя CE так: + +```fsharp +// readFileAsync: filePath: string -> Future +// writeFileAsync: filePath: string -> content: string -> Future +let readAndWriteFuture = futur { + let! content = readFileAsync "my-file.txt" + return! writeFileAsync "other-file.txt" content +} +``` + +Видимым преимуществом CE является возможность "уплощить" цепочка bind, зависимых между собой. +Пример множественно зависимых bind можно переписать так: + +```fsharp +let doManyWorkWithCrossResults = future { + let! val1 = doWork1 () + let! val2 = doWork2 val1 + let! val3 = doWork3 val1 val2 + ... + let! valN = doWorkN val1 val2 ... valPrevN +} +``` + +Также CE добавляют синтаксис и для Future.merge или Future.catch комбинаторов. + +```fsharp +let parallelCE = future { + let! val1 = doWork1 () + and! val2 = doWork2 () + and! val3 = doWork3 () +} +``` + +```fsharp +let catchCE = future { + try + do! doWork () + with ex -> + printfn $"{ex}" +} +``` + +```fsharp +let tryFinally = future { + try + do! doWork () + finally + do finallize () +} +``` + + +## Преобразование из Async и Task + +Существующие Async и Task можно преобразовать в Future и использовать результат их работы. +Исходные Async и Task будут запущены на своих родных системах запуска, но их результат будет +передан через возвращенную Future. + +```fsharp +let asyncToFuture = Future.ofAsync (async { ... }) +let taskToFuture = Future.ofTask (task { ... }) +``` + +Возможны и обратные преобразования. При этом Future будут запущены на механизме запуска +соответствующего примитива при запуске этого примитива. + +```fsharp +let futureToAsync = Future.ofAsync (async { ... }) +let futureToTask = Future.ofTask (task { ... }) +``` + + +## Ручная реализация Future + +Future это всего-лишь интерфейс с методами Poll и Drop. +Можно создать свою Future просто реализовав их. + +Ручная реализация Future корректным образом не такая тривиальная задача, +требующая ручной реализации конечного или не очень автомата. +Поэтому не рекомендуется делать это, только если Вы не разрабатываете +API для использования механизма асинхронности на низком уровне. + +Объяснения и более подробные примеры следует искать в более продвинутых главах. + + diff --git a/docs/ru/basics/running.md b/docs/ru/basics/running.md new file mode 100644 index 0000000..7415843 --- /dev/null +++ b/docs/ru/basics/running.md @@ -0,0 +1,53 @@ +# Запуск Future + + +## Запуск на текущем потоке + +Future можно запустить на текущем потоке используя `Future.runBlocking`. +Переданная Future запустится, а вызывающий поток будет заблокирован +пока не получится результат. +```fsharp +let fut = future { + let! name = Future.ready "Alex" + do! Future.sleepMs 1000 + return $"Hello, {name}!" +} + +let phrase = fut |> Future.runBlocking +printfn $"{phrase}" +``` + + +## Запуск используя Runtime + +Future можно запустить на Runtime. +Runtime это планировщик для нескольких параллельно выполняющихся Future, +не используя Future.merge и снимая его ограничения +(Future скомбинированные используя Future.merge никогда не выполняются по-настоящему параллельно). + +Запустить Future на планировщике можно используя его метод Spawn. + +```fsharp +let fut = future { ... } +let fTask = ThreadPoolRuntime.Instance.Spawn(fut) +``` + +Spawn возвращает объект запущенной задачи (IFutureTask<'a>). +Используя экземпляр запущенной задачи можно преобразовать её в ожидающую выполнения +Future используя Await, или прервать её выполнение через Abort. +Если задача была прервана, ожидающая Future выбросит исключение при своем запуске. + +```fsharp +future { + let fTask = ThreadPoolRuntime.Instance.Spawn(future { ... }) + do! doOtherWork () + let! fTaskResult = fTask.Await() +} +``` + +
+IFutureTask.Await может быть вызван только один раз. +
+ +По-умолчанию Await создает Future, вызывающую Abort при своем Drop. +Это можно переопределить вызвав Await с флагом background=true (`fTask.Await(true)`). diff --git a/src/FSharp.Control.Futures/Future.fs b/src/FSharp.Control.Futures/Future.fs index dddc0ea..5dae0d8 100644 --- a/src/FSharp.Control.Futures/Future.fs +++ b/src/FSharp.Control.Futures/Future.fs @@ -275,6 +275,11 @@ module Future = |> inspect (fun _ -> do finalizer ()) |> map (fun x -> match x with Ok r -> r | Error ex -> raise ex) + // let inline tryFinallyM (body: Future<'a>) (finalizer: unit -> Future): Future<'a> = + // catch body + // |> inspectM (fun _ -> finalizer ()) + // |> map (fun x -> match x with Ok r -> r | Error ex -> raise ex) + /// Creates a Future that returns control flow to the runtime once /// Future that returns control flow to the runtime once let inline yieldWorkflow () : Future = @@ -338,6 +343,9 @@ type FutureBuilder() = member inline _.TryFinally(body: unit -> Future<'a>, finalizer: unit -> unit): Future<'a> = Future.tryFinally (Future.delay body) finalizer + // member inline _.TryFinally(body: unit -> Future<'a>, finalizer: unit -> Future): Future<'a> = + // Future.tryFinallyM (Future.delay body) finalizer + member inline _.Using<'d, 'a when 'd :> IDisposable>(disposable: 'd, body: 'd -> Future<'a>): Future<'a> = let body' = fun () -> body disposable let disposer = fun () -> diff --git a/src/FSharp.Control.Futures/Transforms.fs b/src/FSharp.Control.Futures/Transforms.fs index d4966f8..6fa00e0 100644 --- a/src/FSharp.Control.Futures/Transforms.fs +++ b/src/FSharp.Control.Futures/Transforms.fs @@ -158,8 +158,8 @@ module FutureApmTransforms = let ctx = { new NotFeaturedContext() with override this.Wake() = - if asyncResult.IsCompleted then invalidOp "Cannot call Wait when Future is Ready" - startPollOnContext this } + if not asyncResult.IsCompleted then + startPollOnContext this } startPollOnContext ctx