diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4ac8d90b..6bf63d49 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -17,24 +17,7 @@ jobs: uses: actions/setup-dotnet@v3 # build it, test it, pack it - name: Run dotnet build (release) + # see issue #105 + # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble + shell: cmd run: ./build.cmd - - # deploy: - # name: deploy - # runs-on: ubuntu-latest - # if: github.ref == 'refs/heads/main' - # steps: - # # checkout the code - # - name: checkout-code - # uses: actions/checkout@v3 - # with: - # fetch-depth: 0 - # # setup dotnet based on global.json - # - name: setup-dotnet - # uses: actions/setup-dotnet@v3 - # # push it to nuget - # - name: deploy - # run: make cd - # env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 234f2b0b..196dac9e 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -20,6 +20,9 @@ jobs: uses: actions/setup-dotnet@v3 # build it, test it, pack it - name: Run dotnet build (release) + # see issue #105 + # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble + shell: cmd run: ./build.cmd test-release: @@ -36,6 +39,9 @@ jobs: uses: actions/setup-dotnet@v3 # build it, test it, pack it - name: Run dotnet test - release + # see issue #105 + # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble + shell: cmd run: ./build.cmd ci -release - name: Publish test results - release uses: dorny/test-reporter@v1 @@ -45,23 +51,3 @@ jobs: # this path glob pattern requires forward slashes! path: ./src/FSharp.Control.TaskSeq.Test/TestResults/test-results-release.trx reporter: dotnet-trx - - # deploy: - # name: deploy - # runs-on: ubuntu-latest - # if: github.ref == 'refs/heads/main' - # steps: - # # checkout the code - # - name: checkout-code - # uses: actions/checkout@v3 - # with: - # fetch-depth: 0 - # # setup dotnet based on global.json - # - name: setup-dotnet - # uses: actions/setup-dotnet@v3 - # # push it to nuget - # - name: deploy - # run: make cd - # env: - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 00000000..bff72a75 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,32 @@ +name: Pack & Publish Nuget + +on: + push: + branches: + - main + +jobs: + publish: + name: Publish nuget (if new version) + runs-on: windows-latest + steps: + # checkout the code + - name: checkout-code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + # setup dotnet based on global.json + - name: setup-dotnet + uses: actions/setup-dotnet@v3 + # build it, test it, pack it, publish it + - name: Run dotnet build (release, for nuget) + # see issue #105 + # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble + run: ./build.cmd + - name: Nuget publish + # skip-duplicate ensures that the 409 error received when the package was already published, + # will just issue a warning and won't have the GH action fail. + # NUGET_PUBLISH_TOKEN_TASKSEQ is valid until approx. 8 Nov 2023 and will need to be updated by then. + # do so under https://github.com/fsprojects/FSharp.Control.TaskSeq/settings/secrets/actions + # select button "Add repository secret" or update the existing one under "Repository secrets" + run: dotnet nuget push packages\FSharp.Control.TaskSeq.*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_PUBLISH_TOKEN_TASKSEQ }} --skip-duplicate diff --git a/.github/workflows/test-report.yaml b/.github/workflows/test-report.yaml new file mode 100644 index 00000000..1f472625 --- /dev/null +++ b/.github/workflows/test-report.yaml @@ -0,0 +1,31 @@ +name: ci-report + +# See Dorny instructions for why we need a separate yaml for creating a test report +# for public repositories that accept forks: +# https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories + +on: + workflow_run: + workflows: ['ci-test'] # runs after CI workflow + types: + - completed +jobs: + test-report-release: + runs-on: windows-latest + steps: + - uses: dorny/test-reporter@v1 + with: + artifact: test-results-release # artifact name + name: Report release tests # Name of the check run which will be created + path: '*.trx' # Path to test results (inside artifact .zip) + reporter: dotnet-trx # Format of test results + + test-report-debug: + runs-on: windows-latest + steps: + - uses: dorny/test-reporter@v1 + with: + artifact: test-results-debug # artifact name + name: Report debug tests # Name of the check run which will be created + path: '*.trx' # Path to test results (inside artifact .zip) + reporter: dotnet-trx # Format of test results diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5d5042af..aca9b8d7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,20 +12,26 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 + # setup dotnet based on global.json - name: setup-dotnet uses: actions/setup-dotnet@v3 + # build it, test it - name: Run dotnet test - release + # see issue #105 + # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble + shell: cmd run: ./build.cmd ci -release - - name: Publish test results - release - uses: dorny/test-reporter@v1 - if: always() + + # upload test results + - uses: actions/upload-artifact@v3 + if: success() || failure() with: - name: Report release tests + name: test-results-release # this path glob pattern requires forward slashes! path: ./src/FSharp.Control.TaskSeq.Test/TestResults/test-results-release.trx - reporter: dotnet-trx + test-debug: name: Test Debug Build @@ -36,17 +42,22 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 + # setup dotnet based on global.json - name: setup-dotnet uses: actions/setup-dotnet@v3 + # build it, test it - name: Run dotnet test - debug + # see issue #105 + # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble + shell: cmd run: ./build.cmd ci -debug - - name: Publish test results - debug - uses: dorny/test-reporter@v1 - if: always() + + # upload test results + - uses: actions/upload-artifact@v3 + if: success() || failure() with: - name: Report debug tests + name: test-results-debug # this path glob pattern requires forward slashes! path: ./src/FSharp.Control.TaskSeq.Test/TestResults/test-results-debug.trx - reporter: dotnet-trx diff --git a/.gitignore b/.gitignore index f5910e87..72d94d66 100644 --- a/.gitignore +++ b/.gitignore @@ -349,3 +349,4 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ *.ncrunchproject +nuget-api-key.txt diff --git a/README.md b/README.md index 7da1e2fe..8629ccaf 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ [![build][buildstatus_img]][buildstatus] [![test][teststatus_img]][teststatus] +[![Nuget](https://img.shields.io/nuget/vpre/FSharp.Control.TaskSeq)](https://www.nuget.org/packages/FSharp.Control.TaskSeq/) # TaskSeq -An implementation [`IAsyncEnumerable<'T>`][3] as a `taskSeq` CE for F# with accompanying `TaskSeq` module. +An implementation of [`IAsyncEnumerable<'T>`][3] as a computation expression: `taskSeq { ... }` with an accompanying `TaskSeq` module, that allows seamless use of asynchronous sequences similar to F#'s native `seq` and `task` CE's. -The `IAsyncEnumerable` interface was added to .NET in `.NET Core 3.0` and is part of `.NET Standard 2.1`. The main use-case was for iterative asynchronous enumeration over some resource. For instance, an event stream or a REST API interface with pagination, where each page is a [`MoveNextAsync`][4] call on the [`IAsyncEnumerator<'T>`][5] given by a call to [`GetAsyncEnumerator()`][6]. It has been relatively challenging to work properly with this type and dealing with each step being asynchronous, and the enumerator implementing [`IAsyncDisposable`][7] as well, which requires careful handling. +Latest version [can be installed from Nuget][nuget]. ----------------------------------------- @@ -18,13 +19,18 @@ The `IAsyncEnumerable` interface was added to .NET in `.NET Core 3.0` and is par More info: https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one#table-of-contents --> -- [Feature planning](#feature-planning) -- [Implementation progress](#implementation-progress) - - [`taskSeq` CE](#taskseq-ce) - - [`TaskSeq` module functions](#taskseq-module-functions) +- [Overview](#overview) + - [Module functions](#module-functions) + - [`taskSeq` computation expressions](#taskseq-computation-expressions) + - [Installation](#installation) + - [Examples](#examples) +- [Status & planning](#status--planning) + - [Implementation progress](#implementation-progress) + - [Progress `taskSeq` CE](#progress-taskseq-ce) + - [Progress and implemented `TaskSeq` module functions](#progress-and-implemented-taskseq-module-functions) - [More information](#more-information) - - [Futher reading `IAsyncEnumerable`](#futher-reading-iasyncenumerable) - - [Futher reading on resumable state machines](#futher-reading-on-resumable-state-machines) + - [Further reading `IAsyncEnumerable`](#further-reading-iasyncenumerable) + - [Further reading on resumable state machines](#further-reading-on-resumable-state-machines) - [Further reading on computation expressions](#further-reading-on-computation-expressions) - [Building & testing](#building--testing) - [Prerequisites](#prerequisites) @@ -38,27 +44,144 @@ The `IAsyncEnumerable` interface was added to .NET in `.NET Core 3.0` and is par ----------------------------------------- -## Feature planning +## Overview -Not necessarily in order of importance: +The `IAsyncEnumerable` interface was added to .NET in `.NET Core 3.0` and is part of `.NET Standard 2.1`. The main use-case was for iterative asynchronous enumeration over some resource. For instance, an event stream or a REST API interface with pagination, asynchronous reading over a list of files and accumulating the results, where each action can be modeled as a [`MoveNextAsync`][4] call on the [`IAsyncEnumerator<'T>`][5] given by a call to [`GetAsyncEnumerator()`][6]. + +Since the introduction of `task` in F# the call for a native implementation of _task sequences_ has grown, in particular because proper iterating over an `IAsyncEnumerable` has proven challenging, especially if one wants to avoid mutable variables. This library is an answer to that call and implements the same _resumable state machine_ approach with `taskSeq`. + +### Module functions + +As with `seq` and `Seq`, this library comes with a bunch of well-known collection functions, like `TaskSeq.empty`, `isEmpty` or `TaskSeq.map`, `iter`, `collect`, `fold` and `TaskSeq.find`, `pick`, `choose`, `filter`. Where applicable, these come with async variants, like `TaskSeq.mapAsync` `iterAsync`, `collectAsync`, `foldAsync` and `TaskSeq.findAsync`, `pickAsync`, `chooseAsync`, `filterAsync`, which allows the applied function to be asynchronous. + +[See below](#current-set-of-taskseq-utility-functions) for a full list of currently implemented functions and their variants. + +### `taskSeq` computation expressions + +The `taskSeq` computation expression can be used just like using `seq`. On top of that, it adds support for working with tasks through `let!` and +looping over a normal or asynchronous sequence (one that implements `IAsyncEnumerable<'T>'`). You can use `yield!` and `yield` and there's support +for `use` and `use!`, `try-with` and `try-finally` and `while` loops within the task sequence expression: + +### Installation + +Dotnet Nuget + +```cmd +dotnet add package FSharp.Control.TaskSeq +``` + +For a specific project: + +```cmd +dotnet add myproject.fsproj package FSharp.Control.TaskSeq +``` + +F# Interactive (FSI): + +```f# +// latest version +> #r "nuget: FSharp.Control.TaskSeq" + +// or with specific version +> #r "nuget: FSharp.Control.TaskSeq, 0.2.2" +``` + +Paket: + +```cmd +dotnet paket add FSharp.Control.TaskSeq --project +``` + +Package Manager: + +```cmd +PM> NuGet\Install-Package FSharp.Control.TaskSeq +``` + +As package reference in `fsproj` or `csproj` file: + +```xml + + +``` + +### Examples + +```f# +open System.IO +open FSharp.Control + +// singleton is fine +let helloTs = taskSeq { yield "Hello, World!" } + +// cold-started, that is, delay-executed +let f() = task { + // using toList forces execution of whole sequence + let! hello = TaskSeq.toList helloTs // toList returns a Task<'T list> + return List.head hello +} + +// can be mixed with normal sequences +let oneToTen = taskSeq { yield! [1..10] } + +// can be used with F#'s task and async in a for-loop +let f() = task { for x in oneToTen do printfn "Number %i" x } +let g() = async { for x in oneToTen do printfn "Number %i" x } + +// returns a delayed sequence of IAsyncEnumerable +let allFilesAsLines() = taskSeq { + let files = Directory.EnumerateFiles(@"c:\temp") + for file in files do + // await + let! contents = File.ReadAllLinesAsync file + // return all lines + yield! contents +} + +let write file = + allFilesAsLines() + + // synchronous map function on asynchronous task sequence + |> TaskSeq.map (fun x -> x.Replace("a", "b")) + + // asynchronous map + |> TaskSeq.mapAsync (fun x -> task { return "hello: " + x }) + + // asynchronous iter + |> TaskSeq.iterAsync (fun data -> File.WriteAllTextAsync(fileName, data)) + + +// infinite sequence +let feedFromTwitter user pwd = taskSeq { + do! loginToTwitterAsync(user, pwd) + while true do + let! message = getNextNextTwitterMessageAsync() + yield message +} +``` + +## Status & planning + +This project has stable features currently, but before we go full "version one", we'd like to complete the surface area. This section covers the status of that, with a full list of implmented functions below. Here's the short list: - [x] Stabilize and battle-test `taskSeq` resumable code. **DONE** - [x] A growing set of module functions `TaskSeq`, see below for progress. **DONE & IN PROGRESS** - [x] Packaging and publishing on Nuget, **DONE, PUBLISHED SINCE: 7 November 2022**. See https://www.nuget.org/packages/FSharp.Control.TaskSeq - [x] Add `Async` variants for functions taking HOF arguments. **DONE** - [ ] Add generated docs to -- [ ] Expand surface area based on `AsyncSeq`. -- [ ] User requests? +- [ ] Expand surface area based on `AsyncSeq`. **ONGOING** + +### Implementation progress -## Implementation progress +As of 9 November 2022: [Nuget package available][21]. In this phase, we will frequently update the package. Current: -As of 6 November 2022: +[![Nuget](https://img.shields.io/nuget/vpre/FSharp.Control.TaskSeq)](https://www.nuget.org/packages/FSharp.Control.TaskSeq/) -### `taskSeq` CE +### Progress `taskSeq` CE -The _resumable state machine_ backing the `taskSeq` CE is now finished and _restartability_ (not to be confused with _resumability_) has been implemented and stabilized. Full support for empty task sequences is done. Focus is now on adding functionality there, like adding more useful overloads for `yield` and `let!`. Suggestions are welcome! +The _resumable state machine_ backing the `taskSeq` CE is now finished and _restartability_ (not to be confused with _resumability_) has been implemented and stabilized. Full support for empty task sequences is done. Focus is now on adding functionality there, like adding more useful overloads for `yield` and `let!`. [Suggestions are welcome!][issues]. -### `TaskSeq` module functions +### Progress and implemented `TaskSeq` module functions We are working hard on getting a full set of module functions on `TaskSeq` that can be used with `IAsyncEnumerable` sequences. Our guide is the set of F# `Seq` functions in F# Core and, where applicable, the functions provided from `AsyncSeq`. Each implemented function is documented through XML doc comments to provide the necessary context-sensitive help. @@ -153,7 +276,7 @@ The following is the progress report: | ❓ | `rev` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | | | `scan` | `scan` | `scanAsync` | | | 🚫 | `scanBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | -| | `singleton` | `singleton` | | | +| ✅ [#90][] | `singleton` | `singleton` | | | | | `skip` | `skip` | | | | | `skipWhile` | `skipWhile` | `skipWhileAsync` | | | ❓ | `sort` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | @@ -200,14 +323,14 @@ The following is the progress report: ## More information -### Futher reading `IAsyncEnumerable` +### Further reading `IAsyncEnumerable` - A good C#-based introduction [can be found in this blog][8]. - [An MSDN article][9] written shortly after it was introduced. - Converting a `seq` to an `IAsyncEnumerable` [demo gist][10] as an example, though `TaskSeq` contains many more utility functions and uses a slightly different approach. - If you're looking for using `IAsyncEnumerable` with `async` and not `task`, the excellent [`AsyncSeq`][11] library should be used. While `TaskSeq` is intended to consume `async` just like `task` does, it won't create an `AsyncSeq` type (at least not yet). If you want classic Async and parallelism, you should get this library instead. -### Futher reading on resumable state machines +### Further reading on resumable state machines - A state machine from a monadic perspective in F# [can be found here][12], which works with the pre-F# 6.0 non-resumable internals. - The [original RFC for F# 6.0 on resumable state machines][13] @@ -283,8 +406,7 @@ Command modifiers, like `release` and `debug`, can be specified with `-` or `/` build help ``` -For more info, see this PR: https://github.com/abelbraaksma/TaskSeq/pull/29. - +For more info, see this PR: . ## Work in progress @@ -294,285 +416,93 @@ On top of that, this library adds a set of `TaskSeq` module functions, with thei ## Current set of `TaskSeq` utility functions -The following is the current surface area of the `TaskSeq` utility functions. This is just a dump of the signatures with doc comments -to be used as a quick ref. +The following are the current surface area of the `TaskSeq` utility functions, ordered alphabetically. ```f# module TaskSeq = - open System.Collections.Generic - open System.Threading.Tasks - open FSharp.Control.TaskSeqBuilders - - /// Initialize an empty taskSeq. - val empty<'T> : taskSeq<'T> - - /// - /// Returns if the task sequence contains no elements, otherwise. - /// - val isEmpty: taskSeq: taskSeq<'T> -> Task - - /// Returns taskSeq as an array. This function is blocking until the sequence is exhausted and will properly dispose of the resources. - val toList: t: taskSeq<'T> -> 'T list - - /// Returns taskSeq as an array. This function is blocking until the sequence is exhausted and will properly dispose of the resources. - val toArray: taskSeq: taskSeq<'T> -> 'T[] - - /// Returns taskSeq as a seq, similar to Seq.cached. This function is blocking until the sequence is exhausted and will properly dispose of the resources. - val toSeqCached: taskSeq: taskSeq<'T> -> seq<'T> - - /// Unwraps the taskSeq as a Task>. This function is non-blocking. - val toArrayAsync: taskSeq: taskSeq<'T> -> Task<'T[]> - - /// Unwraps the taskSeq as a Task>. This function is non-blocking. - val toListAsync: taskSeq: taskSeq<'T> -> Task<'T list> - - /// Unwraps the taskSeq as a Task>. This function is non-blocking. - val toResizeArrayAsync: taskSeq: taskSeq<'T> -> Task> - - /// Unwraps the taskSeq as a Task>. This function is non-blocking. - val toIListAsync: taskSeq: taskSeq<'T> -> Task> - - /// Unwraps the taskSeq as a Task>. This function is non-blocking, - /// exhausts the sequence and caches the results of the tasks in the sequence. - val toSeqCachedAsync: taskSeq: taskSeq<'T> -> Task> - - /// Create a taskSeq of an array. - val ofArray: array: 'T[] -> taskSeq<'T> - - /// Create a taskSeq of a list. - val ofList: list: 'T list -> taskSeq<'T> - - /// Create a taskSeq of a seq. - val ofSeq: sequence: seq<'T> -> taskSeq<'T> - - /// Create a taskSeq of a ResizeArray, aka List. - val ofResizeArray: data: ResizeArray<'T> -> taskSeq<'T> - - /// Create a taskSeq of a sequence of tasks, that may already have hot-started. - val ofTaskSeq: sequence: seq<#Task<'T>> -> taskSeq<'T> - - /// Create a taskSeq of a list of tasks, that may already have hot-started. - val ofTaskList: list: #Task<'T> list -> taskSeq<'T> - - /// Create a taskSeq of an array of tasks, that may already have hot-started. - val ofTaskArray: array: #Task<'T> array -> taskSeq<'T> - - /// Create a taskSeq of a seq of async. - val ofAsyncSeq: sequence: seq> -> taskSeq<'T> - - /// Create a taskSeq of a list of async. - val ofAsyncList: list: Async<'T> list -> taskSeq<'T> - - /// Create a taskSeq of an array of async. - val ofAsyncArray: array: Async<'T> array -> taskSeq<'T> - - /// Iterates over the taskSeq applying the action function to each item. This function is non-blocking - /// exhausts the sequence as soon as the task is evaluated. - val iter: action: ('T -> unit) -> taskSeq: taskSeq<'T> -> Task - - /// Iterates over the taskSeq applying the action function to each item. This function is non-blocking, - /// exhausts the sequence as soon as the task is evaluated. - val iteri: action: (int -> 'T -> unit) -> taskSeq: taskSeq<'T> -> Task - - /// Iterates over the taskSeq applying the async action to each item. This function is non-blocking - /// exhausts the sequence as soon as the task is evaluated. - val iterAsync: action: ('T -> #Task) -> taskSeq: taskSeq<'T> -> Task - - /// Iterates over the taskSeq, applying the async action to each item. This function is non-blocking, - /// exhausts the sequence as soon as the task is evaluated. - val iteriAsync: action: (int -> 'T -> #Task) -> taskSeq: taskSeq<'T> -> Task - - /// Maps over the taskSeq, applying the mapper function to each item. This function is non-blocking. - val map: mapper: ('T -> 'U) -> taskSeq: taskSeq<'T> -> taskSeq<'U> - - /// Maps over the taskSeq with an index, applying the mapper function to each item. This function is non-blocking. - val mapi: mapper: (int -> 'T -> 'U) -> taskSeq: taskSeq<'T> -> taskSeq<'U> - - /// Maps over the taskSeq, applying the async mapper function to each item. This function is non-blocking. - val mapAsync: mapper: ('T -> #Task<'U>) -> taskSeq: taskSeq<'T> -> taskSeq<'U> - - /// Maps over the taskSeq with an index, applying the async mapper function to each item. This function is non-blocking. - val mapiAsync: mapper: (int -> 'T -> #Task<'U>) -> taskSeq: taskSeq<'T> -> taskSeq<'U> - - /// Applies the given function to the items in the taskSeq and concatenates all the results in order. - val collect: binder: ('T -> #taskSeq<'U>) -> taskSeq: taskSeq<'T> -> taskSeq<'U> - - /// Applies the given function to the items in the taskSeq and concatenates all the results in order. - val collectSeq: binder: ('T -> #seq<'U>) -> taskSeq: taskSeq<'T> -> taskSeq<'U> - - /// Applies the given async function to the items in the taskSeq and concatenates all the results in order. - val collectAsync: binder: ('T -> #Task<'TSeqU>) -> taskSeq: taskSeq<'T> -> taskSeq<'U> when 'TSeqU :> taskSeq<'U> - - /// Applies the given async function to the items in the taskSeq and concatenates all the results in order. - val collectSeqAsync: binder: ('T -> #Task<'SeqU>) -> taskSeq: taskSeq<'T> -> taskSeq<'U> when 'SeqU :> seq<'U> - - /// - /// Returns the first element of the , or if the sequence is empty. - /// - /// Thrown when the sequence is empty. - val tryHead: taskSeq: taskSeq<'T> -> Task<'T option> - - /// - /// Returns the first element of the . - /// - /// Thrown when the sequence is empty. - val head: taskSeq: taskSeq<'T> -> Task<'T> - - /// - /// Returns the last element of the , or if the sequence is empty. - /// - /// Thrown when the sequence is empty. - val tryLast: taskSeq: taskSeq<'T> -> Task<'T option> - - /// - /// Returns the last element of the . - /// - /// Thrown when the sequence is empty. - val last: taskSeq: taskSeq<'T> -> Task<'T> - - /// - /// Returns the nth element of the , or if the sequence - /// does not contain enough elements, or if is negative. - /// Parameter is zero-based, that is, the value 0 returns the first element. - /// - val tryItem: index: int -> taskSeq: taskSeq<'T> -> Task<'T option> - - /// - /// Returns the nth element of the , or if the sequence - /// does not contain enough elements, or if is negative. - /// - /// Thrown when the sequence has insufficient length or - /// is negative. - val item: index: int -> taskSeq: taskSeq<'T> -> Task<'T> - - /// - /// Returns the only element of the task sequence, or if the sequence is empty of - /// contains more than one element. - /// - val tryExactlyOne: source: taskSeq<'T> -> Task<'T option> - - /// - /// Returns the only element of the task sequence. - /// - /// Thrown when the input sequence does not contain precisely one element. - val exactlyOne: source: taskSeq<'T> -> Task<'T> - - /// - /// Applies the given function to each element of the task sequence. Returns - /// a sequence comprised of the results "x" for each element where - /// the function returns Some(x). - /// If is asynchronous, consider using . - /// + val append: source1: #taskSeq<'T> -> source2: #taskSeq<'T> -> taskSeq<'T> + val appendSeq: source1: #taskSeq<'T> -> source2: #seq<'T> -> taskSeq<'T> + val box: source: taskSeq<'T> -> taskSeq + val cast: source: taskSeq -> taskSeq<'T> val choose: chooser: ('T -> 'U option) -> source: taskSeq<'T> -> taskSeq<'U> - - /// - /// Applies the given asynchronous function to each element of the task sequence. Returns - /// a sequence comprised of the results "x" for each element where - /// the function returns . - /// If does not need to be asynchronous, consider using . - /// val chooseAsync: chooser: ('T -> #Task<'U option>) -> source: taskSeq<'T> -> taskSeq<'U> - - /// - /// Returns a new collection containing only the elements of the collection - /// for which the given function returns . - /// If is asynchronous, consider using . - /// + val collect: binder: ('T -> #taskSeq<'U>) -> source: taskSeq<'T> -> taskSeq<'U> + val collectAsync: binder: ('T -> #Task<'TSeqU>) -> source: taskSeq<'T> -> taskSeq<'U> when 'TSeqU :> taskSeq<'U> + val collectSeq: binder: ('T -> #seq<'U>) -> source: taskSeq<'T> -> taskSeq<'U> + val collectSeqAsync: binder: ('T -> #Task<'SeqU>) -> source: taskSeq<'T> -> taskSeq<'U> when 'SeqU :> seq<'U> + val concat: sources: taskSeq<#taskSeq<'T>> -> taskSeq<'T> + val contains<'T when 'T: equality> : value: 'T -> source: taskSeq<'T> -> Task + val delay: generator: (unit -> taskSeq<'T>) -> taskSeq<'T> + val empty<'T> : taskSeq<'T> + val exactlyOne: source: taskSeq<'T> -> Task<'T> + val except<'T when 'T: equality> : itemsToExclude: taskSeq<'T> -> source: taskSeq<'T> -> taskSeq<'T> + val exceptOfSeq<'T when 'T: equality> : itemsToExclude: seq<'T> -> source: taskSeq<'T> -> taskSeq<'T> + val exists: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task + val existsAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task val filter: predicate: ('T -> bool) -> source: taskSeq<'T> -> taskSeq<'T> - - /// - /// Returns a new collection containing only the elements of the collection - /// for which the given asynchronous function returns . - /// If does not need to be asynchronous, consider using . - /// val filterAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> taskSeq<'T> - - /// - /// Applies the given function to successive elements of the task sequence - /// in , returning the first result where the function returns . - /// If is asynchronous, consider using . - /// - val tryPick: chooser: ('T -> 'U option) -> source: taskSeq<'T> -> Task<'U option> - - /// - /// Applies the given asynchronous function to successive elements of the task sequence - /// in , returning the first result where the function returns . - /// If does not need to be asynchronous, consider using . - /// - val tryPickAsync: chooser: ('T -> #Task<'U option>) -> source: taskSeq<'T> -> Task<'U option> - - /// - /// Returns the first element of the task sequence in for which the given function - /// returns . Returns if no such element exists. - /// If is asynchronous, consider using . - /// - val tryFind: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task<'T option> - - /// - /// Returns the first element of the task sequence in for which the given asynchronous function - /// returns . Returns if no such element exists. - /// If does not need to be asynchronous, consider using . - /// - val tryFindAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task<'T option> - - - /// - /// Applies the given function to successive elements of the task sequence - /// in , returning the first result where the function returns . - /// If is asynchronous, consider using . - /// Thrown when every item of the sequence - /// evaluates to when the given function is applied. - /// - val pick: chooser: ('T -> 'U option) -> source: taskSeq<'T> -> Task<'U> - - /// - /// Applies the given asynchronous function to successive elements of the task sequence - /// in , returning the first result where the function returns . - /// If does not need to be asynchronous, consider using . - /// Thrown when every item of the sequence - /// evaluates to when the given function is applied. - /// - val pickAsync: chooser: ('T -> #Task<'U option>) -> source: taskSeq<'T> -> Task<'U> - - /// - /// Returns the first element of the task sequence in for which the given function - /// returns . - /// If is asynchronous, consider using . - /// - /// Thrown if no element returns when - /// evaluated by the function. val find: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task<'T> - - /// - /// Returns the first element of the task sequence in for which the given - /// asynchronous function returns . - /// If does not need to be asynchronous, consider using . - /// - /// Thrown if no element returns when - /// evaluated by the function. val findAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task<'T> - - /// - /// Zips two task sequences, returning a taskSeq of the tuples of each sequence, in order. May raise ArgumentException - /// if the sequences are or unequal length. - /// - /// The sequences have different lengths. - val zip: taskSeq1: taskSeq<'T> -> taskSeq2: taskSeq<'U> -> IAsyncEnumerable<'T * 'U> - - /// - /// Applies the function to each element in the task sequence, - /// threading an accumulator argument of type through the computation. - /// If the accumulator function is asynchronous, consider using . - /// - val fold: folder: ('State -> 'T -> 'State) -> state: 'State -> taskSeq: taskSeq<'T> -> Task<'State> - - /// - /// Applies the asynchronous function to each element in the task sequence, - /// threading an accumulator argument of type through the computation. - /// If the accumulator function does not need to be asynchronous, consider using . - /// - val foldAsync: folder: ('State -> 'T -> #Task<'State>) -> state: 'State -> taskSeq: taskSeq<'T> -> Task<'State> - + val findIndex: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task + val findIndexAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task + val fold: folder: ('State -> 'T -> 'State) -> state: 'State -> source: taskSeq<'T> -> Task<'State> + val foldAsync: folder: ('State -> 'T -> #Task<'State>) -> state: 'State -> source: taskSeq<'T> -> Task<'State> + val head: source: taskSeq<'T> -> Task<'T> + val indexed: source: taskSeq<'T> -> taskSeq + val init: count: int -> initializer: (int -> 'T) -> taskSeq<'T> + val initAsync: count: int -> initializer: (int -> #Task<'T>) -> taskSeq<'T> + val initInfinite: initializer: (int -> 'T) -> taskSeq<'T> + val initInfiniteAsync: initializer: (int -> #Task<'T>) -> taskSeq<'T> + val isEmpty: source: taskSeq<'T> -> Task + val item: index: int -> source: taskSeq<'T> -> Task<'T> + val iter: action: ('T -> unit) -> source: taskSeq<'T> -> Task + val iterAsync: action: ('T -> #Task) -> source: taskSeq<'T> -> Task + val iteri: action: (int -> 'T -> unit) -> source: taskSeq<'T> -> Task + val iteriAsync: action: (int -> 'T -> #Task) -> source: taskSeq<'T> -> Task + val last: source: taskSeq<'T> -> Task<'T> + val length: source: taskSeq<'T> -> Task + val lengthBy: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task + val lengthByAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task + val lengthOrMax: max: int -> source: taskSeq<'T> -> Task + val map: mapper: ('T -> 'U) -> source: taskSeq<'T> -> taskSeq<'U> + val mapAsync: mapper: ('T -> #Task<'U>) -> source: taskSeq<'T> -> taskSeq<'U> + val mapi: mapper: (int -> 'T -> 'U) -> source: taskSeq<'T> -> taskSeq<'U> + val mapiAsync: mapper: (int -> 'T -> #Task<'U>) -> source: taskSeq<'T> -> taskSeq<'U> + val ofArray: source: 'T[] -> taskSeq<'T> + val ofAsyncArray: source: Async<'T> array -> taskSeq<'T> + val ofAsyncList: source: Async<'T> list -> taskSeq<'T> + val ofAsyncSeq: source: seq> -> taskSeq<'T> + val ofList: source: 'T list -> taskSeq<'T> + val ofResizeArray: source: ResizeArray<'T> -> taskSeq<'T> + val ofSeq: source: seq<'T> -> taskSeq<'T> + val ofTaskArray: source: #Task<'T> array -> taskSeq<'T> + val ofTaskList: source: #Task<'T> list -> taskSeq<'T> + val ofTaskSeq: source: seq<#Task<'T>> -> taskSeq<'T> + val pick: chooser: ('T -> 'U option) -> source: taskSeq<'T> -> Task<'U> + val pickAsync: chooser: ('T -> #Task<'U option>) -> source: taskSeq<'T> -> Task<'U> + val prependSeq: source1: #seq<'T> -> source2: #taskSeq<'T> -> taskSeq<'T> + val singleton: source: 'T -> taskSeq<'T> + val tail: source: taskSeq<'T> -> Task> + val toArray: source: taskSeq<'T> -> 'T[] + val toArrayAsync: source: taskSeq<'T> -> Task<'T[]> + val toIListAsync: source: taskSeq<'T> -> Task> + val toList: source: taskSeq<'T> -> 'T list + val toListAsync: source: taskSeq<'T> -> Task<'T list> + val toResizeArrayAsync: source: taskSeq<'T> -> Task> + val toSeq: source: taskSeq<'T> -> seq<'T> + val tryExactlyOne: source: taskSeq<'T> -> Task<'T option> + val tryFind: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task<'T option> + val tryFindAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task<'T option> + val tryFindIndex: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task + val tryFindIndexAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task + val tryHead: source: taskSeq<'T> -> Task<'T option> + val tryItem: index: int -> source: taskSeq<'T> -> Task<'T option> + val tryLast: source: taskSeq<'T> -> Task<'T option> + val tryPick: chooser: ('T -> 'U option) -> source: taskSeq<'T> -> Task<'U option> + val tryPickAsync: chooser: ('T -> #Task<'U option>) -> source: taskSeq<'T> -> Task<'U option> + val tryTail: source: taskSeq<'T> -> Task option> + val unbox<'U when 'U: struct> : source: taskSeq -> taskSeq<'U> + val zip: source1: taskSeq<'T> -> source2: taskSeq<'U> -> taskSeq<'T * 'U> ``` [buildstatus]: https://github.com/abelbraaksma/TaskSeq/actions/workflows/main.yaml @@ -600,6 +530,7 @@ module TaskSeq = [18]: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions [19]: https://fsharpforfunandprofit.com/series/computation-expressions/ [20]: https://github.com/dotnet/fsharp/blob/d5312aae8aad650f0043f055bb14c3aa8117e12e/tests/benchmarks/CompiledCodeBenchmarks/TaskPerf/TaskPerf/taskSeq.fs +[21]: https://www.nuget.org/packages/FSharp.Control.TaskSeq#versions-body-tab [#2]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/2 [#11]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/11 @@ -613,4 +544,7 @@ module TaskSeq = [#81]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/81 [#82]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/82 [#83]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/83 +[#90]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/90 +[issues]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues +[nuget]: https://www.nuget.org/packages/FSharp.Control.TaskSeq/ diff --git a/Version.props b/Version.props index 7f8337f7..9c576a91 100644 --- a/Version.props +++ b/Version.props @@ -1,5 +1,5 @@ - 0.2.2 + 0.3.0 \ No newline at end of file diff --git a/assets/TaskSeq.ico b/assets/TaskSeq.ico deleted file mode 100644 index 65d3ee0a..00000000 Binary files a/assets/TaskSeq.ico and /dev/null differ diff --git a/assets/nuget-package-readme.md b/assets/nuget-package-readme.md index dd3a5aa6..35e6320c 100644 --- a/assets/nuget-package-readme.md +++ b/assets/nuget-package-readme.md @@ -14,45 +14,58 @@ An implementation of [`IAsyncEnumerable<'T>`][3] as a computation expression: `t --> - [Overview](#overview) + - [Module functions](#module-functions) - [`taskSeq` computation expressions](#taskseq-computation-expressions) - [Examples](#examples) - [`TaskSeq` module functions](#taskseq-module-functions) - [More information](#more-information) - - [Futher reading `IAsyncEnumerable`](#futher-reading-iasyncenumerable) - - [Futher reading on resumable state machines](#futher-reading-on-resumable-state-machines) + - [Further reading `IAsyncEnumerable`](#further-reading-iasyncenumerable) + - [Further reading on resumable state machines](#further-reading-on-resumable-state-machines) - [Further reading on computation expressions](#further-reading-on-computation-expressions) ----------------------------------------- ## Overview -The `IAsyncEnumerable` interface was added to .NET in `.NET Core 3.0` and is part of `.NET Standard 2.1`. The main use-case was for iterative asynchronous enumeration over some resource. For instance, an event stream or a REST API interface with pagination, asynchronous reading over a list of files and accumulating the results, where each action can be modeled as a [`MoveNextAsync`][4] call on the [`IAsyncEnumerator<'T>`][5] given by a call to [`GetAsyncEnumerator()`][6]. +The `IAsyncEnumerable` interface was added to .NET in `.NET Core 3.0` and is part of `.NET Standard 2.1`. The main use-case was for iterative asynchronous enumeration over some resource. For instance, an event stream or a REST API interface with pagination, asynchronous reading over a list of files and accumulating the results, where each action can be modeled as a [`MoveNextAsync`][4] call on the [`IAsyncEnumerator<'T>`][5] given by a call to [`GetAsyncEnumerator()`][6]. Since the introduction of `task` in F# the call for a native implementation of _task sequences_ has grown, in particular because proper iterating over an `IAsyncEnumerable` has proven challenging, especially if one wants to avoid mutable variables. This library is an answer to that call and implements the same _resumable state machine_ approach with `taskSeq`. -As with `seq` and `Seq`, this library comes with a bunch of well-known collection functions, like `TaskSeq.empty/isEmpty` or `TaskSeq.map/iter/collect` and `TaskSeq.find/pick/choose/filter`. Where applicable, these come with async variants, like `TaskSeq.mapAsync/iterAsync/collectAsync` and `TaskSeq.findAsync/pickAsync/chooseAsync/filterAsync`, which allows the apply function to be asynchronous. +### Module functions -See below for a full list of currently implemented functions. +As with `seq` and `Seq`, this library comes with a bunch of well-known collection functions, like `TaskSeq.empty`, `isEmpty` or `TaskSeq.map`, `iter`, `collect`, `fold` and `TaskSeq.find`, `pick`, `choose`, `filter`. Where applicable, these come with async variants, like `TaskSeq.mapAsync` `iterAsync`, `collectAsync`, `foldAsync` and `TaskSeq.findAsync`, `pickAsync`, `chooseAsync`, `filterAsync`, which allows the applied function to be asynchronous. + +[See below](#taskseq-module-functions) for a full list of currently implemented functions and their variants. ### `taskSeq` computation expressions -The `taskSeq` computation expression can be used just like using `seq`. On top of that, it adds support for working with tasks through `let!` and +The `taskSeq` computation expression can be used just like using `seq`. On top of that, it adds support for working with tasks through `let!` and looping over a normal or asynchronous sequence (one that implements `IAsyncEnumerable<'T>'`). You can use `yield!` and `yield` and there's support -for `use` and `use!`, `try-with` and `try-finally` and `while` loops within the task sequence expression: +for `use` and `use!`, `try-with` and `try-finally` and `while` loops within the task sequence expression. ### Examples ```f# open System.IO - open FSharp.Control // singleton is fine -let hello = taskSeq { yield "Hello, World!"" } +let helloTs = taskSeq { yield "Hello, World!" } + +// cold-started, that is, delay-executed +let f() = task { + // using toList forces execution of whole sequence + let! hello = TaskSeq.toList helloTs // toList returns a Task<'T list> + return List.head hello +} // can be mixed with normal sequences let oneToTen = taskSeq { yield! [1..10] } +// can be used with F#'s task and async in a for-loop +let f() = task { for x in oneToTen do printfn "Number %i" x } +let g() = async { for x in oneToTen do printfn "Number %i" x } + // returns a delayed sequence of IAsyncEnumerable let allFilesAsLines() = taskSeq { let files = Directory.EnumerateFiles(@"c:\temp") @@ -180,7 +193,7 @@ The following is the progress report: | ❓ | `rev` | | | [note #1](#note-1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | | | `scan` | `scan` | `scanAsync` | | | 🚫 | `scanBack` | | | [note #2](#note-2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | -| | `singleton` | `singleton` | | | +| ✅ [#90][] | `singleton` | `singleton` | | | | | `skip` | `skip` | | | | | `skipWhile` | `skipWhile` | `skipWhileAsync` | | | ❓ | `sort` | | | [note #1](#note-1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | @@ -234,14 +247,14 @@ _The motivation for `readOnly` in `Seq` is that a cast from a mutable array or l ## More information -### Futher reading `IAsyncEnumerable` +### Further reading `IAsyncEnumerable` - A good C#-based introduction [can be found in this blog][8]. - [An MSDN article][9] written shortly after it was introduced. - Converting a `seq` to an `IAsyncEnumerable` [demo gist][10] as an example, though `TaskSeq` contains many more utility functions and uses a slightly different approach. - If you're looking for using `IAsyncEnumerable` with `async` and not `task`, the excellent [`AsyncSeq`][11] library should be used. While `TaskSeq` is intended to consume `async` just like `task` does, it won't create an `AsyncSeq` type (at least not yet). If you want classic Async and parallelism, you should get this library instead. -### Futher reading on resumable state machines +### Further reading on resumable state machines - A state machine from a monadic perspective in F# [can be found here][12], which works with the pre-F# 6.0 non-resumable internals. - The [original RFC for F# 6.0 on resumable state machines][13] @@ -283,4 +296,5 @@ _The motivation for `readOnly` in `Seq` is that a cast from a mutable array or l [#76]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/76 [#81]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/81 [#82]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/82 -[#83]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/83 \ No newline at end of file +[#83]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/83 +[#90]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/90 \ No newline at end of file diff --git a/release-notes.txt b/release-notes.txt new file mode 100644 index 00000000..d80f41d1 --- /dev/null +++ b/release-notes.txt @@ -0,0 +1,42 @@ + +Release notes: + +0.3.0 + - internal renames, improved doc comments, signature files for complex types, hide internal-only types, fixes #112. + - adds support for static TaskLike, allowing the same let! and do! overloads that F# task supports, fixes #110. + - implements 'do!' for non-generic Task like with Task.Delay, fixes #43. + - adds support for 'for .. in ..' with task sequences in F# tasks and async, #75, #93 and #99 (with help from @theangrybyrd). + - adds TaskSeq.singleton, #90 (by @gusty). + - fixes overload resolution bug with 'use' and 'use!', #97 (thanks @peterfaria). + - improves TaskSeq.empty by not relying on resumable state, #89 (by @gusty). + - does not throw exceptions anymore for unequal lengths in TaskSeq.zip, fixes #32. + +0.2.2 + - removes TaskSeq.toSeqCachedAsync, which was incorrectly named. Use toSeq or toListAsync instead. + - renames TaskSeq.toSeqCached to TaskSeq.toSeq, which was its actual operational behavior. + +0.2.1 + - fixes an issue with ValueTask on completed iterations. + - adds `TaskSeq.except` and `TaskSeq.exceptOfSeq` async set operations. + +0.2 + - moved from NET 6.0, to NetStandard 2.1 for greater compatibility, no functional changes. + - move to minimally necessary FSharp.Core version: 6.0.2. + - updated readme with progress overview, corrected meta info, added release notes. + +0.1.1 + - updated meta info in nuget package and added readme. + +0.1 + - initial release + - implements taskSeq CE using resumable state machines + - with support for: yield, yield!, let, let!, while, for, try-with, try-finally, use, use! + - and: tasks and valuetasks + - adds toXXX / ofXXX functions + - adds map/mapi/fold/iter/iteri/collect etc with async variants + - adds find/pick/choose/filter etc with async variants and 'try' variants + - adds cast/concat/append/prepend/delay/exactlyOne + - adds empty/isEmpty + - adds findIndex/indexed/init/initInfinite + - adds head/last/tryHead/tryLast/tail/tryTail + - adds zip/length \ No newline at end of file diff --git a/resources/TaskSeq.ico b/resources/TaskSeq.ico deleted file mode 100644 index 65d3ee0a..00000000 Binary files a/resources/TaskSeq.ico and /dev/null differ diff --git a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj index a0760a87..611663b8 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -1,15 +1,13 @@ - + net6.0 false false - ..\..\assets\TaskSeq.ico - @@ -39,6 +37,7 @@ + @@ -47,6 +46,11 @@ + + + + + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.AsyncExtensions.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.AsyncExtensions.Tests.fs new file mode 100644 index 00000000..1f0b9efd --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.AsyncExtensions.Tests.fs @@ -0,0 +1,144 @@ +module TaskSeq.Tests.AsyncExtensions + +open System +open Xunit +open FsUnit.Xunit + +open FSharp.Control + +// +// Async extensions +// + +module EmptySeq = + [)>] + let ``Async-for CE with empty taskSeq`` variant = async { + let values = Gen.getEmptyVariant variant + + let mutable sum = 42 + + for x in values do + sum <- sum + x + + sum |> should equal 42 + } + + [] + let ``Async-for CE must execute side effect in empty taskseq`` () = async { + let mutable data = 0 + let values = taskSeq { do data <- 42 } + + for x in values do + () + + data |> should equal 42 + } + + +module Immutable = + [)>] + let ``Async-for CE with taskSeq`` variant = async { + let values = Gen.getSeqImmutable variant + + let mutable sum = 0 + + for x in values do + sum <- sum + x + + sum |> should equal 55 + } + + [)>] + let ``Async-for CE with taskSeq multiple iterations`` variant = async { + let values = Gen.getSeqImmutable variant + + let mutable sum = 0 + + for x in values do + sum <- sum + x + + // each following iteration should start at the beginning + for x in values do + sum <- sum + x + + for x in values do + sum <- sum + x + + sum |> should equal 165 + } + + [] + let ``Async-for mixing both types of for loops`` () = async { + // this test ensures overload resolution is correct + let ts = TaskSeq.singleton 20 + let sq = Seq.singleton 20 + let mutable sum = 2 + + for x in ts do + sum <- sum + x + + for x in sq do + sum <- sum + x + + sum |> should equal 42 + } + +module SideEffects = + [)>] + let ``Async-for CE with taskSeq`` variant = async { + let values = Gen.getSeqWithSideEffect variant + + let mutable sum = 0 + + for x in values do + sum <- sum + x + + sum |> should equal 55 + } + + [)>] + let ``Async-for CE with taskSeq multiple iterations`` variant = async { + let values = Gen.getSeqWithSideEffect variant + + let mutable sum = 0 + + for x in values do + sum <- sum + x + + // each following iteration should start at the beginning + // with the "side effect" tests, the mutable state updates + for x in values do + sum <- sum + x // starts at 11 + + for x in values do + sum <- sum + x // starts at 21 + + sum |> should equal 465 // eq to: List.sum [1..30] + } + +module Other = + [] + let ``Async-for CE must call dispose in empty taskSeq`` () = async { + let disposed = ref 0 + let values = Gen.getEmptyDisposableTaskSeq disposed + + for x in values do + () + + // the DisposeAsync should be called by now + disposed.Value |> should equal 1 + } + + [] + let ``Async-for CE must call dispose on singleton`` () = async { + let disposed = ref 0 + let mutable sum = 0 + let values = Gen.getSingletonDisposableTaskSeq disposed + + for x in values do + sum <- x + + // the DisposeAsync should be called by now + disposed.Value |> should equal 1 + sum |> should equal 42 + } diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs new file mode 100644 index 00000000..d979f6fa --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Do.Tests.fs @@ -0,0 +1,58 @@ +module TaskSeq.Tests.Do + +open System +open System.Threading.Tasks +open FsUnit +open Xunit + +open FSharp.Control + +[] +let ``CE taskSeq: use 'do'`` () = + let mutable value = 0 + + taskSeq { do value <- value + 1 } |> verifyEmpty + +[] +let ``CE taskSeq: use 'do!' with a task`` () = + let mutable value = 0 + + taskSeq { do! task { do value <- value + 1 } } + |> verifyEmpty + |> Task.map (fun _ -> value |> should equal 1) + +[] +let ``CE taskSeq: use 'do!' with a valuetask`` () = + let mutable value = 0 + + taskSeq { do! ValueTask.ofTask (task { do value <- value + 1 }) } + |> verifyEmpty + |> Task.map (fun _ -> value |> should equal 1) + +[] +let ``CE taskSeq: use 'do!' with a non-generic valuetask`` () = + let mutable value = 0 + + taskSeq { do! ValueTask(task { do value <- value + 1 }) } + |> verifyEmpty + |> Task.map (fun _ -> value |> should equal 1) + +[] +let ``CE taskSeq: use 'do!' with a non-generic task`` () = + let mutable value = 0 + + taskSeq { do! (task { do value <- value + 1 }) |> Task.ignore } + |> verifyEmpty + |> Task.map (fun _ -> value |> should equal 1) + +[] +let ``CE taskSeq: use 'do!' with a task-delay`` () = + let mutable value = 0 + + taskSeq { + do value <- value + 1 + do! Task.Delay 50 + do value <- value + 1 + } + |> verifyEmpty + |> Task.map (fun _ -> value |> should equal 2) diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Let.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Let.Tests.fs new file mode 100644 index 00000000..a4b9b66d --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Let.Tests.fs @@ -0,0 +1,82 @@ +module TaskSeq.Tests.Let + +open System +open System.Threading.Tasks +open FsUnit +open Xunit + +open FSharp.Control + +[] +let ``CE taskSeq: use 'let'`` () = + let mutable value = 0 + + taskSeq { + let value1 = value + 1 + let value2 = value1 + 1 + yield value2 + } + |> TaskSeq.exactlyOne + |> Task.map (should equal 2) + +[] +let ``CE taskSeq: use 'let!' with a task`` () = + let mutable value = 0 + + taskSeq { + let! unit' = task { do value <- value + 1 } + do unit' + } + |> verifyEmpty + |> Task.map (fun _ -> value |> should equal 1) + +[] +let ``CE taskSeq: use 'let!' with a task`` () = + taskSeq { + let! test = task { return "test" } + yield test + } + |> TaskSeq.exactlyOne + |> Task.map (should equal "test") + +[] +let ``CE taskSeq: use 'let!' with a valuetask`` () = + let mutable value = 0 + + taskSeq { + let! unit' = ValueTask.ofTask (task { do value <- value + 1 }) + do unit' + } + |> verifyEmpty + |> Task.map (fun _ -> value |> should equal 1) + +[] +let ``CE taskSeq: use 'let!' with a valuetask`` () = + taskSeq { + let! test = ValueTask.ofTask (task { return "test" }) + yield test + } + |> TaskSeq.exactlyOne + |> Task.map (should equal "test") + +[] +let ``CE taskSeq: use 'let!' with a non-generic valuetask`` () = + let mutable value = 0 + + taskSeq { + let! unit' = ValueTask(task { do value <- value + 1 }) + do unit' + } + |> verifyEmpty + |> Task.map (fun _ -> value |> should equal 1) + +[] +let ``CE taskSeq: use 'let!' with a non-generic task`` () = + let mutable value = 0 + + taskSeq { + let! unit' = (task { do value <- value + 1 }) |> Task.ignore + do unit' + } + |> verifyEmpty + |> Task.map (fun _ -> value |> should equal 1) diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Singleton.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Singleton.Tests.fs new file mode 100644 index 00000000..a916b800 --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Singleton.Tests.fs @@ -0,0 +1,96 @@ +module TaskSeq.Tests.Singleton + +open System.Threading.Tasks +open Xunit +open FsUnit.Xunit +open FsToolkit.ErrorHandling + +open FSharp.Control + +module EmptySeq = + + [)>] + let ``TaskSeq-singleton with empty has length one`` variant = + taskSeq { + yield! TaskSeq.singleton 10 + yield! Gen.getEmptyVariant variant + } + |> TaskSeq.exactlyOne + |> Task.map (should equal 10) + +module Other = + [] + let ``TaskSeq-singleton creates a sequence of one`` () = + TaskSeq.singleton 42 + |> TaskSeq.exactlyOne + |> Task.map (should equal 42) + + [] + let ``TaskSeq-singleton can be yielded multiple times`` () = + let singleton = TaskSeq.singleton 42 + + taskSeq { + yield! singleton + yield! singleton + yield! singleton + yield! singleton + } + |> TaskSeq.toList + |> should equal [ 42; 42; 42; 42 ] + + [] + let ``TaskSeq-singleton with isEmpty`` () = + TaskSeq.singleton 42 + |> TaskSeq.isEmpty + |> Task.map (should be False) + + [] + let ``TaskSeq-singleton with append`` () = + TaskSeq.singleton 42 + |> TaskSeq.append (TaskSeq.singleton 42) + |> TaskSeq.toList + |> should equal [ 42; 42 ] + + [)>] + let ``TaskSeq-singleton with collect`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.collect TaskSeq.singleton + |> verify1To10 + + [] + let ``TaskSeq-singleton does not throw when getting Current before MoveNext`` () = task { + let enumerator = (TaskSeq.singleton 42).GetAsyncEnumerator() + let defaultValue = enumerator.Current // should return the default value for int + defaultValue |> should equal 0 + } + + [] + let ``TaskSeq-singleton does not throw when getting Current after last MoveNext`` () = task { + let enumerator = (TaskSeq.singleton 42).GetAsyncEnumerator() + let! isNext = enumerator.MoveNextAsync() + isNext |> should be True + let value = enumerator.Current // the first and only value + value |> should equal 42 + + // move past the end + let! isNext = enumerator.MoveNextAsync() + isNext |> should be False + let defaultValue = enumerator.Current // should return the default value for int + defaultValue |> should equal 0 + } + + [] + let ``TaskSeq-singleton multiple MoveNext is fine`` () = task { + let enumerator = (TaskSeq.singleton 42).GetAsyncEnumerator() + let! isNext = enumerator.MoveNextAsync() + isNext |> should be True + let! _ = enumerator.MoveNextAsync() + let! _ = enumerator.MoveNextAsync() + let! _ = enumerator.MoveNextAsync() + let! isNext = enumerator.MoveNextAsync() + isNext |> should be False + + // should return the default value for int after moving past the end + let defaultValue = enumerator.Current + defaultValue |> should equal 0 + } diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TaskExtensions.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TaskExtensions.Tests.fs new file mode 100644 index 00000000..5d5316a4 --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TaskExtensions.Tests.fs @@ -0,0 +1,143 @@ +module TaskSeq.Tests.TaskExtensions + +open System +open Xunit +open FsUnit.Xunit + +open FSharp.Control + +// +// Task extensions +// + +module EmptySeq = + [)>] + let ``Task-for CE with empty taskSeq`` variant = task { + let values = Gen.getEmptyVariant variant + + let mutable sum = 42 + + for x in values do + sum <- sum + x + + sum |> should equal 42 + } + + [] + let ``Task-for CE must execute side effect in empty taskseq`` () = task { + let mutable data = 0 + let values = taskSeq { do data <- 42 } + + for x in values do + () + + data |> should equal 42 + } + +module Immutable = + [)>] + let ``Task-for CE with taskSeq`` variant = task { + let values = Gen.getSeqImmutable variant + + let mutable sum = 0 + + for x in values do + sum <- sum + x + + sum |> should equal 55 + } + + [)>] + let ``Task-for CE with taskSeq multiple iterations`` variant = task { + let values = Gen.getSeqImmutable variant + + let mutable sum = 0 + + for x in values do + sum <- sum + x + + // each following iteration should start at the beginning + for x in values do + sum <- sum + x + + for x in values do + sum <- sum + x + + sum |> should equal 165 + } + + [] + let ``Task-for mixing both types of for loops`` () = async { + // this test ensures overload resolution is correct + let ts = TaskSeq.singleton 20 + let sq = Seq.singleton 20 + let mutable sum = 2 + + for x in ts do + sum <- sum + x + + for x in sq do + sum <- sum + x + + sum |> should equal 42 + } + +module SideEffects = + [)>] + let ``Task-for CE with taskSeq`` variant = task { + let values = Gen.getSeqWithSideEffect variant + + let mutable sum = 0 + + for x in values do + sum <- sum + x + + sum |> should equal 55 + } + + [)>] + let ``Task-for CE with taskSeq multiple iterations`` variant = task { + let values = Gen.getSeqWithSideEffect variant + + let mutable sum = 0 + + for x in values do + sum <- sum + x + + // each following iteration should start at the beginning + // with the "side effect" tests, the mutable state updates + for x in values do + sum <- sum + x // starts at 11 + + for x in values do + sum <- sum + x // starts at 21 + + sum |> should equal 465 // eq to: List.sum [1..30] + } + +module Other = + [] + let ``Task-for CE must call dispose in empty taskSeq`` () = async { + let disposed = ref 0 + let values = Gen.getEmptyDisposableTaskSeq disposed + + for x in values do + () + + // the DisposeAsync should be called by now + disposed.Value |> should equal 1 + } + + [] + let ``Task-for CE must call dispose on singleton`` () = async { + let disposed = ref 0 + let mutable sum = 0 + let values = Gen.getSingletonDisposableTaskSeq disposed + + for x in values do + sum <- x + + // the DisposeAsync should be called by now + disposed.Value |> should equal 1 + sum |> should equal 42 + } diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Using.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Using.Tests.fs new file mode 100644 index 00000000..8ef92f5d --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Using.Tests.fs @@ -0,0 +1,106 @@ +module TaskSeq.Test.Using + +open System +open System.Threading.Tasks +open FSharp.Control +open FsUnit +open Xunit + + +type private OneGetter() = + member _.Get1() = 1 + +type private Disposable(disposed: bool ref) = + inherit OneGetter() + + interface IDisposable with + member _.Dispose() = disposed.Value <- true + +type private AsyncDisposable(disposed: bool ref) = + inherit OneGetter() + + interface IAsyncDisposable with + member _.DisposeAsync() = ValueTask(task { do disposed.Value <- true }) + +type private MultiDispose(disposed: int ref) = + inherit OneGetter() + + interface IDisposable with + member _.Dispose() = disposed.Value <- 1 + + interface IAsyncDisposable with + member _.DisposeAsync() = ValueTask(task { do disposed.Value <- -1 }) + +let private check = TaskSeq.length >> Task.map (should equal 1) + +[] +let ``CE taskSeq: Using when type implements IDisposable`` () = + let disposed = ref false + + let ts = taskSeq { + use x = new Disposable(disposed) + yield x.Get1() + } + + check ts + |> Task.map (fun _ -> disposed.Value |> should be True) + +[] +let ``CE taskSeq: Using when type implements IAsyncDisposable`` () = + let disposed = ref false + + let ts = taskSeq { + use x = AsyncDisposable(disposed) + yield x.Get1() + } + + check ts + |> Task.map (fun _ -> disposed.Value |> should be True) + +[] +let ``CE taskSeq: Using when type implements IDisposable and IAsyncDisposable`` () = + let disposed = ref 0 + + let ts = taskSeq { + use x = new MultiDispose(disposed) // Used to fail to compile (see #97) + yield x.Get1() + } + + check ts + |> Task.map (fun _ -> disposed.Value |> should equal -1) // should prefer IAsyncDisposable, which returns -1 + +[] +let ``CE taskSeq: Using! when type implements IDisposable`` () = + let disposed = ref false + + let ts = taskSeq { + use! x = task { return new Disposable(disposed) } + yield x.Get1() + } + + check ts + |> Task.map (fun _ -> disposed.Value |> should be True) + +[] +let ``CE taskSeq: Using! when type implements IAsyncDisposable`` () = + let disposed = ref false + + let ts = taskSeq { + use! x = task { return AsyncDisposable(disposed) } + yield x.Get1() + } + + check ts + |> Task.map (fun _ -> disposed.Value |> should be True) + +[] +let ``CE taskSeq: Using! when type implements IDisposable and IAsyncDisposable`` () = + let disposed = ref 0 + + let ts = taskSeq { + use! x = task { return new MultiDispose(disposed) } // Used to fail to compile (see #97) + yield x.Get1() + } + + check ts + |> Task.map (fun _ -> disposed.Value |> should equal -1) // should prefer IAsyncDisposable, which returns -1 diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs index 2d0b2f12..1206d74c 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs @@ -132,32 +132,3 @@ module Other = combined |> should equal [| ("one", 42L); ("two", 43L) |] } - - [] - let ``TaskSeq-zip throws on unequal lengths, variant`` leftThrows = task { - let long = Gen.sideEffectTaskSeq 11 - let short = Gen.sideEffectTaskSeq 10 - - let combined = - if leftThrows then - TaskSeq.zip short long - else - TaskSeq.zip long short - - fun () -> TaskSeq.toArrayAsync combined |> Task.ignore - |> should throwAsyncExact typeof - } - - [] - let ``TaskSeq-zip throws on unequal lengths with empty seq`` leftThrows = task { - let one = Gen.sideEffectTaskSeq 1 - - let combined = - if leftThrows then - TaskSeq.zip TaskSeq.empty one - else - TaskSeq.zip one TaskSeq.empty - - fun () -> TaskSeq.toArrayAsync combined |> Task.ignore - |> should throwAsyncExact typeof - } diff --git a/src/FSharp.Control.TaskSeq.Test/TestUtils.fs b/src/FSharp.Control.TaskSeq.Test/TestUtils.fs index 91baf383..36244708 100644 --- a/src/FSharp.Control.TaskSeq.Test/TestUtils.fs +++ b/src/FSharp.Control.TaskSeq.Test/TestUtils.fs @@ -136,11 +136,18 @@ type DummyTaskFactory(µsecMin: int64<µs>, µsecMax: int64<µs>) = [] module TestUtils = + /// Verifies that a task sequence is empty by converting to an array and checking emptiness. let verifyEmpty ts = ts |> TaskSeq.toArrayAsync |> Task.map (Array.isEmpty >> should be True) + /// Verifies that a task sequence contains integers 1-10, by converting to an array and comparing. + let verify1To10 ts = + ts + |> TaskSeq.toArrayAsync + |> Task.map (should equal [| 1..10 |]) + /// Delays (no spin-wait!) between 20 and 70ms, assuming a 15.6ms resolution clock let longDelay () = task { do! Task.Delay(Random().Next(20, 70)) } @@ -516,6 +523,44 @@ module TestUtils = } | x -> failwithf "Invalid test variant: %A" x + /// An empty taskSeq that can be used with tests for checking if the dispose method gets called. + /// Will add 1 to the passed integer upon disposing. + let getEmptyDisposableTaskSeq (disposed: int ref) = + { new IAsyncEnumerable<'T> with + member _.GetAsyncEnumerator(_) = + { new IAsyncEnumerator<'T> with + member _.MoveNextAsync() = ValueTask.False + member _.Current = Unchecked.defaultof<'T> + member _.DisposeAsync() = ValueTask(task { do disposed.Value <- disposed.Value + 1 }) + } + } + + /// A singleton taskSeq that can be used with tests for checking if the dispose method gets called + /// The singleton value is '42'. Will add 1 to the passed integer upon disposing. + let getSingletonDisposableTaskSeq (disposed: int ref) = + { new IAsyncEnumerable with + member _.GetAsyncEnumerator(_) = + let mutable status = BeforeAll + + { new IAsyncEnumerator with + member _.MoveNextAsync() = + match status with + | BeforeAll -> + status <- WithCurrent + ValueTask.True + | WithCurrent -> + status <- AfterAll + ValueTask.False + | AfterAll -> ValueTask.False + + member _.Current: int = + match status with + | WithCurrent -> 42 + | _ -> Unchecked.defaultof + + member _.DisposeAsync() = ValueTask(task { do disposed.Value <- disposed.Value + 1 }) + } + } // // following types can be used with Theory & TestData // diff --git a/src/FSharp.Control.TaskSeq.sln b/src/FSharp.Control.TaskSeq.sln index ab1a7527..cce9e711 100644 --- a/src/FSharp.Control.TaskSeq.sln +++ b/src/FSharp.Control.TaskSeq.sln @@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\build.cmd = ..\build.cmd ..\Directory.Build.props = ..\Directory.Build.props ..\README.md = ..\README.md + ..\release-notes.txt = ..\release-notes.txt ..\Version.props = ..\Version.props EndProjectSection EndProject @@ -20,6 +21,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ ..\.github\workflows\build.yaml = ..\.github\workflows\build.yaml ..\.github\dependabot.yml = ..\.github\dependabot.yml ..\.github\workflows\main.yaml = ..\.github\workflows\main.yaml + ..\.github\workflows\publish.yaml = ..\.github\workflows\publish.yaml + ..\.github\workflows\test-report.yaml = ..\.github\workflows\test-report.yaml ..\.github\workflows\test.yaml = ..\.github\workflows\test.yaml EndProjectSection EndProject @@ -29,7 +32,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{B198D5 ProjectSection(SolutionItems) = preProject ..\assets\nuget-package-readme.md = ..\assets\nuget-package-readme.md ..\assets\taskseq-icon.png = ..\assets\taskseq-icon.png - ..\assets\TaskSeq.ico = ..\assets\TaskSeq.ico EndProjectSection EndProject Global diff --git a/src/FSharp.Control.TaskSeq/AssemblyInfo.fs b/src/FSharp.Control.TaskSeq/AssemblyInfo.fs new file mode 100644 index 00000000..d8bda820 --- /dev/null +++ b/src/FSharp.Control.TaskSeq/AssemblyInfo.fs @@ -0,0 +1,8 @@ +namespace TaskSeq.Tests + +open System.Runtime.CompilerServices + +// ensure the test project has access to the internal types +[] + +do () diff --git a/src/FSharp.Control.TaskSeq/AsyncExtensions.fs b/src/FSharp.Control.TaskSeq/AsyncExtensions.fs new file mode 100644 index 00000000..25817526 --- /dev/null +++ b/src/FSharp.Control.TaskSeq/AsyncExtensions.fs @@ -0,0 +1,12 @@ +namespace FSharp.Control + +[] +module AsyncExtensions = + + // Add asynchronous for loop to the 'async' computation builder + type Microsoft.FSharp.Control.AsyncBuilder with + + member _.For(source: taskSeq<'T>, action: 'T -> Async) = + source + |> TaskSeq.iterAsync (action >> Async.StartAsTask) + |> Async.AwaitTask diff --git a/src/FSharp.Control.TaskSeq/AsyncExtensions.fsi b/src/FSharp.Control.TaskSeq/AsyncExtensions.fsi new file mode 100644 index 00000000..cf96281e --- /dev/null +++ b/src/FSharp.Control.TaskSeq/AsyncExtensions.fsi @@ -0,0 +1,11 @@ +namespace FSharp.Control + +[] +module AsyncExtensions = + + type AsyncBuilder with + + /// + /// Inside , iterate over all values of a . + /// + member For: source: taskSeq<'T> * action: ('T -> Async) -> Async diff --git a/src/FSharp.Control.TaskSeq/DebugUtils.fs b/src/FSharp.Control.TaskSeq/DebugUtils.fs new file mode 100644 index 00000000..aab74e3a --- /dev/null +++ b/src/FSharp.Control.TaskSeq/DebugUtils.fs @@ -0,0 +1,55 @@ +namespace FSharp.Control + +open System.Threading.Tasks +open System +open System.Diagnostics +open System.Threading + +type Debug = + + [] + static val mutable private verbose: bool option + + /// Setting from environment variable TASKSEQ_LOG_VERBOSE, which, + /// when set, enables (very) verbose printing of flow and state + static member private getVerboseSetting() = + match Debug.verbose with + | None -> + let verboseEnv = + try + match Environment.GetEnvironmentVariable "TASKSEQ_LOG_VERBOSE" with + | null -> false + | x -> + match x.ToLowerInvariant().Trim() with + | "1" + | "true" + | "on" + | "yes" -> true + | _ -> false + + with _ -> + false + + Debug.verbose <- Some verboseEnv + verboseEnv + + | Some setting -> setting + + /// Private helper to log to stdout in DEBUG builds only + [] + static member private print value = + match Debug.getVerboseSetting () with + | false -> () + | true -> + // don't use ksprintf here, because the compiler does not remove all allocations due to + // the way PrintfFormat types are compiled, even if we set the Conditional attribute. + let ct = Thread.CurrentThread + printfn "%i (%b): %s" ct.ManagedThreadId ct.IsThreadPoolThread value + + /// Log to stdout in DEBUG builds only + [] + static member logInfo(str) = Debug.print str + + /// Log to stdout in DEBUG builds only + [] + static member logInfo(str, data) = Debug.print $"%s{str}{data}" diff --git a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj index b0b89c51..7ec1202e 100644 --- a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj +++ b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj @@ -1,18 +1,17 @@ - + netstandard2.1 true - ..\..\assets\TaskSeq.ico True Computation expression 'taskSeq' for processing IAsyncEnumerable sequences and module functions $(Version) Abel Braaksma; Don Syme - This library brings C#'s concept of 'await foreach' to F#. + This library brings C#'s concept of 'await foreach' to F#, with a seamless implementation of IAsyncEnumerable<'T>. -The 'taskSeq' computation expression adds support for awaitable asyncronous sequences with a similar ease of use and performance as F#'s 'task' CE. TaskSeq brings 'seq' and 'task' together in a safe way. +The 'taskSeq' computation expression adds support for awaitable asyncronous sequences with a similar ease of use and performance as F#'s 'task' CE, with minimal overhead through ValueTask under the hood. TaskSeq brings 'seq' and 'task' together in a safe way. -Generates optimized IL code and comes with a comprehensive set of module functions. See README for more info. +Generates optimized IL code through the new resumable state machines, and comes with a comprehensive set of helpful functions in module 'TaskSeq'. See README for documentation and more info. Copyright 2022 https://github.com/fsprojects/FSharp.Control.TaskSeq https://github.com/fsprojects/FSharp.Control.TaskSeq @@ -21,56 +20,43 @@ Generates optimized IL code and comes with a comprehensive set of module functio MIT False nuget-package-readme.md - - Release notes: - 0.2.2 - - removes TaskSeq.toSeqCachedAsync, which was incorrectly named. Use toSeq or toListAsync instead. - - renames TaskSeq.toSeqCached to TaskSeq.toSeq, which was its actual operational behavior. - 0.2.1 - - fixes an issue with ValueTask on completed iterations. - - adds `TaskSeq.except` and `TaskSeq.exceptOfSeq` async set operations. - 0.2 - - moved from NET 6.0, to NetStandard 2.1 for greater compatibility, no functional changes. - - move to minimally necessary FSharp.Core version: 6.0.2. - - updated readme with progress overview, corrected meta info, added release notes. - 0.1.1 - - updated meta info in nuget package and added readme. - 0.1 - - initial release - - implements taskSeq CE using resumable state machines - - with support for: yield, yield!, let, let!, while, for, try-with, try-finally, use, use! - - and: tasks and valuetasks - - adds toXXX / ofXXX functions - - adds map/mapi/fold/iter/iteri/collect etc with async variants - - adds find/pick/choose/filter etc with async variants and 'try' variants - - adds cast/concat/append/prepend/delay/exactlyOne - - adds empty/isEmpty - - adds findIndex/indexed/init/initInfinite - - adds head/last/tryHead/tryLast/tail/tryTail - - adds zip/length - - + $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../../release-notes.txt")) + taskseq'fsharp;f#;computation expression;IAsyncEnumerable;task;async;asyncseq; + True + snupkg + + + + + + + + + True - + True \ + + + + + + + + - - - - - diff --git a/src/FSharp.Control.TaskSeq/TaskExtensions.fs b/src/FSharp.Control.TaskSeq/TaskExtensions.fs new file mode 100644 index 00000000..b168f358 --- /dev/null +++ b/src/FSharp.Control.TaskSeq/TaskExtensions.fs @@ -0,0 +1,80 @@ +namespace FSharp.Control + +open System.Collections.Generic +open System.Threading +open System.Threading.Tasks + +open Microsoft.FSharp.Core.CompilerServices +open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicOperators + +#nowarn "57" +#nowarn "1204" +#nowarn "3513" + +[] +module TaskExtensions = + + // Add asynchronous for loop to the 'task' computation builder + type Microsoft.FSharp.Control.TaskBuilder with + + /// Used by `For`. F# currently doesn't support `while!`, so this cannot be called directly from the task CE + /// This code is mostly a copy of TaskSeq.WhileAsync. + member inline _.WhileAsync + ( + [] condition: unit -> ValueTask, + body: TaskCode<_, unit> + ) : TaskCode<_, _> = + let mutable condition_res = true + + // note that this While itself has both a dynamic and static implementation + // so we don't need to add that here (TODO: how to verify?). + ResumableCode.While( + (fun () -> condition_res), + TaskCode<_, _>(fun sm -> + let mutable __stack_condition_fin = true + let __stack_vtask = condition () + + let mutable awaiter = __stack_vtask.GetAwaiter() + + if awaiter.IsCompleted then + Debug.logInfo "at Task.WhileAsync: returning completed task" + + __stack_condition_fin <- true + condition_res <- awaiter.GetResult() + else + Debug.logInfo "at Task.WhileAsync: awaiting non-completed task" + + // This will yield with __stack_fin = false + // This will resume with __stack_fin = true + let __stack_yield_fin = ResumableCode.Yield().Invoke(&sm) + __stack_condition_fin <- __stack_yield_fin + + if __stack_condition_fin then + condition_res <- awaiter.GetResult() + + + if __stack_condition_fin then + if condition_res then body.Invoke(&sm) else true + else + sm.Data.MethodBuilder.AwaitUnsafeOnCompleted(&awaiter, &sm) + false) + ) + + member inline this.For(source: taskSeq<'T>, body: 'T -> TaskCode<_, unit>) : TaskCode<_, unit> = + TaskCode<'TOverall, unit>(fun sm -> + this + .Using( + source.GetAsyncEnumerator(CancellationToken()), + (fun e -> + this.WhileAsync( + // __debugPoint is only available from FSharp.Core 6.0.4 + //(fun () -> + // Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers.__debugPoint + // "ForLoop.InOrToKeyword" + + // e.MoveNextAsync()), + e.MoveNextAsync, + (fun sm -> (body e.Current).Invoke(&sm)) + )) + ) + .Invoke(&sm)) diff --git a/src/FSharp.Control.TaskSeq/TaskExtensions.fsi b/src/FSharp.Control.TaskSeq/TaskExtensions.fsi new file mode 100644 index 00000000..76cd8f9d --- /dev/null +++ b/src/FSharp.Control.TaskSeq/TaskExtensions.fsi @@ -0,0 +1,13 @@ +namespace FSharp.Control + +#nowarn "1204" + +[] +module TaskExtensions = + + type TaskBuilder with + + /// + /// Inside , iterate over all values of a . + /// + member inline For: source: taskSeq<'T> * body: ('T -> TaskCode<'TOverall, unit>) -> TaskCode<'TOverall, unit> diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index b94fbb26..05f5313c 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -4,17 +4,24 @@ open System.Collections.Generic open System.Threading open System.Threading.Tasks +#nowarn "57" + module TaskSeq = - // F# BUG: the following module is 'AutoOpen' and this isn't needed in the Tests project. Why do we need to open it? - open FSharp.Control.TaskSeqBuilders // Just for convenience module Internal = TaskSeqInternal - let empty<'T> = taskSeq { - for c: 'T in [] do - yield c - } + let empty<'T> = + { new IAsyncEnumerable<'T> with + member _.GetAsyncEnumerator(_) = + { new IAsyncEnumerator<'T> with + member _.MoveNextAsync() = ValueTask.False + member _.Current = Unchecked.defaultof<'T> + member _.DisposeAsync() = ValueTask.CompletedTask + } + } + + let singleton (source: 'T) = Internal.singleton source let isEmpty source = Internal.isEmpty source @@ -32,10 +39,6 @@ module TaskSeq = e.DisposeAsync().AsTask().Wait() ] - let format x = string x - let f () = format 42 - - let toArray (source: taskSeq<'T>) = [| let e = source.GetAsyncEnumerator(CancellationToken()) diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index aeae538d..1f0f1497 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -1,13 +1,19 @@ namespace FSharp.Control +#nowarn "1204" + module TaskSeq = open System.Collections.Generic open System.Threading.Tasks - open FSharp.Control.TaskSeqBuilders /// Initialize an empty taskSeq. val empty<'T> : taskSeq<'T> + /// + /// Creates a sequence from that generates a single element and then ends. + /// + val singleton: source: 'T -> taskSeq<'T> + /// /// Returns if the task sequence contains no elements, otherwise. /// diff --git a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs index 8ec5cf34..bdf5c725 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs @@ -1,4 +1,4 @@ -namespace FSharp.Control.TaskSeqBuilders +namespace FSharp.Control open System.Diagnostics @@ -13,13 +13,12 @@ open System.Threading.Tasks.Sources open FSharp.Core.CompilerServices open FSharp.Core.CompilerServices.StateMachineHelpers +open FSharp.Control [] module Internal = // cannot be marked with 'internal' scope - /// Setting from environment variable TASKSEQ_LOG_VERBOSE, which, - /// when set, enables (very) verbose printing of flow and state let initVerbose () = try match Environment.GetEnvironmentVariable "TASKSEQ_LOG_VERBOSE" with @@ -35,70 +34,15 @@ module Internal = // cannot be marked with 'internal' scope with _ -> false - type Debug = - [] - static val mutable private verbose: bool option - - /// Setting from environment variable TASKSEQ_LOG_VERBOSE, which, - /// when set, enables (very) verbose printing of flow and state - static member private getVerboseSetting() = - match Debug.verbose with - | None -> - let verboseEnv = - try - match Environment.GetEnvironmentVariable "TASKSEQ_LOG_VERBOSE" with - | null -> false - | x -> - match x.ToLowerInvariant().Trim() with - | "1" - | "true" - | "on" - | "yes" -> true - | _ -> false - - with _ -> - false - - Debug.verbose <- Some verboseEnv - verboseEnv - - | Some setting -> setting - - [] - static member private print value = - match Debug.getVerboseSetting () with - | false -> () - | true -> - // don't use ksprintf here, because the compiler does not remove all allocations due to - // the way PrintfFormat types are compiled, even if we set the Conditional attribute. - printfn "%i (%b): %s" Thread.CurrentThread.ManagedThreadId Thread.CurrentThread.IsThreadPoolThread value - - [] - static member logInfo(str) = Debug.print str - - [] - static member logInfo(str, data) = Debug.print $"%s{str}{data}" - - /// Call MoveNext on an IAsyncStateMachine by reference let inline moveNextRef (x: byref<'T> when 'T :> IAsyncStateMachine) = x.MoveNext() - // F# requires that we implement interfaces even on an abstract class let inline raiseNotImpl () = NotImplementedException "Abstract Class: method or property not implemented" |> raise -open type Debug - type taskSeq<'T> = IAsyncEnumerable<'T> -type IPriority1 = - interface - end - -type IPriority2 = - interface - end [] type TaskSeqStateMachineData<'T>() = @@ -131,7 +75,7 @@ type TaskSeqStateMachineData<'T>() = /// A reference to 'self', because otherwise we can't use byref in the resumable code. [] - val mutable boxedSelf: TaskSeq<'T> + val mutable boxedSelf: TaskSeqBase<'T> member data.PushDispose(disposer: unit -> Task) = if isNull data.disposalStack then @@ -143,7 +87,7 @@ type TaskSeqStateMachineData<'T>() = if not (isNull data.disposalStack) then data.disposalStack.RemoveAt(data.disposalStack.Count - 1) -and [] TaskSeq<'T>() = +and [] TaskSeqBase<'T>() = abstract MoveNextAsyncResult: unit -> ValueTask @@ -171,7 +115,7 @@ and [] TaskSeq<'T>() = and [] TaskSeq<'Machine, 'T when 'Machine :> IAsyncStateMachine and 'Machine :> IResumableStateMachine>>() = - inherit TaskSeq<'T>() + inherit TaskSeqBase<'T>() let initialThreadId = Environment.CurrentManagedThreadId /// Shadows the initial machine, just after it is initialized by the F# compiler-generated state. @@ -243,7 +187,7 @@ and [] TaskSeq<'Machine, 'T this // just return 'self' here | _ -> - logInfo "GetAsyncEnumerator, start cloning..." + Debug.logInfo "GetAsyncEnumerator, start cloning..." // We need to reset state, but only to the "initial machine", resetting the _machine to // Unchecked.defaultof<_> is wrong, as the compiler uses this to track state. However, @@ -259,7 +203,7 @@ and [] TaskSeq<'Machine, 'T clone._machine <- this._initialMachine clone._initialMachine <- this._initialMachine // TODO: proof with a test that this is necessary: probably not clone.InitMachineData(ct, &clone._machine) - logInfo "GetAsyncEnumerator, finished cloning..." + Debug.logInfo "GetAsyncEnumerator, finished cloning..." clone interface System.Collections.Generic.IAsyncEnumerator<'T> with @@ -274,39 +218,39 @@ and [] TaskSeq<'Machine, 'T Unchecked.defaultof<'T> member this.MoveNextAsync() = - logInfo "MoveNextAsync..." + Debug.logInfo "MoveNextAsync..." if this._machine.ResumptionPoint = -1 then // can't use as IAsyncEnumerator before IAsyncEnumerable - logInfo "at MoveNextAsync: Resumption point = -1" + Debug.logInfo "at MoveNextAsync: Resumption point = -1" - ValueTask() + ValueTask.False elif this._machine.Data.completed then - logInfo "at MoveNextAsync: completed = true" + Debug.logInfo "at MoveNextAsync: completed = true" // return False when beyond the last item this._machine.Data.promiseOfValueOrEnd.Reset() - ValueTask() + ValueTask.False else - logInfo "at MoveNextAsync: normal resumption scenario" + Debug.logInfo "at MoveNextAsync: normal resumption scenario" let data = this._machine.Data data.promiseOfValueOrEnd.Reset() let mutable ts = this - logInfo "at MoveNextAsync: start calling builder.MoveNext()" + Debug.logInfo "at MoveNextAsync: start calling builder.MoveNext()" data.builder.MoveNext(&ts) - logInfo "at MoveNextAsync: finished calling builder.MoveNext()" + Debug.logInfo "at MoveNextAsync: finished calling builder.MoveNext()" this.MoveNextAsyncResult() /// Disposes of the IAsyncEnumerator (*not* the IAsyncEnumerable!!!) member this.DisposeAsync() = task { - logInfo "DisposeAsync..." + Debug.logInfo "DisposeAsync..." match this._machine.Data.disposalStack with | null -> () @@ -334,7 +278,7 @@ and [] TaskSeq<'Machine, 'T match status with | ValueTaskSourceStatus.Succeeded -> - logInfo "at MoveNextAsyncResult: case succeeded..." + Debug.logInfo "at MoveNextAsyncResult: case succeeded..." let result = data.promiseOfValueOrEnd.GetResult(version) @@ -343,29 +287,29 @@ and [] TaskSeq<'Machine, 'T // the Current value data.current <- ValueNone - ValueTask(result) + ValueTask.FromResult result | ValueTaskSourceStatus.Faulted | ValueTaskSourceStatus.Canceled | ValueTaskSourceStatus.Pending as state -> - logInfo ("at MoveNextAsyncResult: case ", state) + Debug.logInfo ("at MoveNextAsyncResult: case ", state) - ValueTask(this, version) // uses IValueTaskSource<'T> + ValueTask.ofIValueTaskSource this version | _ -> - logInfo "at MoveNextAsyncResult: Unexpected state" + Debug.logInfo "at MoveNextAsyncResult: Unexpected state" // assume it's a possibly new, not yet supported case, treat as default - ValueTask(this, version) // uses IValueTaskSource<'T> + ValueTask.ofIValueTaskSource this version -and TaskSeqCode<'T> = ResumableCode, unit> +and ResumableTSC<'T> = ResumableCode, unit> and TaskSeqStateMachine<'T> = ResumableStateMachine> and TaskSeqResumptionFunc<'T> = ResumptionFunc> and TaskSeqResumptionDynamicInfo<'T> = ResumptionDynamicInfo> type TaskSeqBuilder() = - member inline _.Delay(f: unit -> TaskSeqCode<'T>) : TaskSeqCode<'T> = TaskSeqCode<'T>(fun sm -> f().Invoke(&sm)) + member inline _.Delay(f: unit -> ResumableTSC<'T>) : ResumableTSC<'T> = ResumableTSC<'T>(fun sm -> f().Invoke(&sm)) - member inline _.Run(code: TaskSeqCode<'T>) : IAsyncEnumerable<'T> = + member inline _.Run(code: ResumableTSC<'T>) : IAsyncEnumerable<'T> = if __useResumableCode then // This is the static implementation. A new struct type is created. __stateMachine, IAsyncEnumerable<'T>> @@ -375,12 +319,12 @@ type TaskSeqBuilder() = __resumeAt sm.ResumptionPoint try - logInfo "at Run.MoveNext start" + Debug.logInfo "at Run.MoveNext start" let __stack_code_fin = code.Invoke(&sm) if __stack_code_fin then - logInfo $"at Run.MoveNext, done" + Debug.logInfo $"at Run.MoveNext, done" // Signal we're at the end // NOTE: if we don't do it here, as well as in IValueTaskSource.GetResult @@ -391,14 +335,14 @@ type TaskSeqBuilder() = sm.Data.completed <- true elif sm.Data.current.IsSome then - logInfo $"at Run.MoveNext, still more items in enumerator" + Debug.logInfo $"at Run.MoveNext, still more items in enumerator" // Signal there's more data: sm.Data.promiseOfValueOrEnd.SetResult(true) else // Goto request - logInfo $"at Run.MoveNext, await, MoveNextAsync has not completed yet" + Debug.logInfo $"at Run.MoveNext, await, MoveNextAsync has not completed yet" // don't capture the full object in the next closure (won't work because: byref) // but only a reference to itself. @@ -411,7 +355,7 @@ type TaskSeqBuilder() = ) with exn -> - logInfo ("Setting exception of PromiseOfValueOrEnd to: ", exn.Message) + Debug.logInfo ("Setting exception of PromiseOfValueOrEnd to: ", exn.Message) sm.Data.promiseOfValueOrEnd.SetException(exn) sm.Data.builder.Complete() @@ -419,7 +363,7 @@ type TaskSeqBuilder() = )) (SetStateMachineMethodImpl<_>(fun sm state -> ())) // not used in reference impl (AfterCode<_, _>(fun sm -> - logInfo "at AfterCode<_, _>, after F# inits the sm, and we can attach extra info" + Debug.logInfo "at AfterCode<_, _>, after F# inits the sm, and we can attach extra info" let ts = TaskSeq, 'T>() ts._initialMachine <- sm @@ -438,20 +382,21 @@ type TaskSeqBuilder() = |> raise - member inline _.Zero() : TaskSeqCode<'T> = - logInfo "at Zero()" + member inline _.Zero() : ResumableTSC<'T> = + Debug.logInfo "at Zero()" ResumableCode.Zero() - member inline _.Combine(task1: TaskSeqCode<'T>, task2: TaskSeqCode<'T>) : TaskSeqCode<'T> = - logInfo "at Combine(.., ..)" + member inline _.Combine(task1: ResumableTSC<'T>, task2: ResumableTSC<'T>) : ResumableTSC<'T> = + Debug.logInfo "at Combine(.., ..)" ResumableCode.Combine(task1, task2) + /// Used by `For`. F# currently doesn't support `while!`, so this cannot be called directly from the CE member inline _.WhileAsync ( [] condition: unit -> ValueTask, - body: TaskSeqCode<'T> - ) : TaskSeqCode<'T> = + body: ResumableTSC<'T> + ) : ResumableTSC<'T> = let mutable condition_res = true ResumableCode.While( @@ -461,12 +406,12 @@ type TaskSeqBuilder() = let __stack_vtask = condition () if __stack_vtask.IsCompleted then - logInfo "at WhileAsync: returning completed task" + Debug.logInfo "at WhileAsync: returning completed task" __stack_condition_fin <- true condition_res <- __stack_vtask.Result else - logInfo "at WhileAsync: awaiting non-completed task" + Debug.logInfo "at WhileAsync: awaiting non-completed task" let task = __stack_vtask.AsTask() let mutable awaiter = task.GetAwaiter() @@ -487,20 +432,17 @@ type TaskSeqBuilder() = false) ) - member inline b.While([] condition: unit -> bool, body: TaskSeqCode<'T>) : TaskSeqCode<'T> = - logInfo "at While(...)" - - // was this: - // b.WhileAsync((fun () -> ValueTask(condition ())), body) + member inline b.While([] condition: unit -> bool, body: ResumableTSC<'T>) : ResumableTSC<'T> = + Debug.logInfo "at While(...)" ResumableCode.While(condition, body) - member inline _.TryWith(body: TaskSeqCode<'T>, catch: exn -> TaskSeqCode<'T>) : TaskSeqCode<'T> = + member inline _.TryWith(body: ResumableTSC<'T>, catch: exn -> ResumableTSC<'T>) : ResumableTSC<'T> = ResumableCode.TryWith(body, catch) - member inline _.TryFinallyAsync(body: TaskSeqCode<'T>, compensation: unit -> Task) : TaskSeqCode<'T> = + member inline _.TryFinallyAsync(body: ResumableTSC<'T>, compensation: unit -> Task) : ResumableTSC<'T> = ResumableCode.TryFinallyAsync( - TaskSeqCode<'T>(fun sm -> + ResumableTSC<'T>(fun sm -> sm.Data.PushDispose(fun () -> compensation ()) body.Invoke(&sm)), @@ -521,9 +463,9 @@ type TaskSeqBuilder() = __stack_condition_fin) ) - member inline _.TryFinally(body: TaskSeqCode<'T>, compensation: unit -> unit) : TaskSeqCode<'T> = + member inline _.TryFinally(body: ResumableTSC<'T>, compensation: unit -> unit) : ResumableTSC<'T> = ResumableCode.TryFinally( - TaskSeqCode<'T>(fun sm -> + ResumableTSC<'T>(fun sm -> sm.Data.PushDispose(fun () -> compensation () Task.CompletedTask) @@ -536,34 +478,7 @@ type TaskSeqBuilder() = true) ) - member inline this.Using - ( - disp: #IDisposable, - body: #IDisposable -> TaskSeqCode<'T>, - ?priority: IPriority2 - ) : TaskSeqCode<'T> = - - // FIXME: what about priority? - ignore priority - - // A using statement is just a try/finally with the finally block disposing if non-null. - this.TryFinally( - (fun sm -> (body disp).Invoke(&sm)), - (fun () -> - // yes, this can be null from time to time - if not (isNull (box disp)) then - disp.Dispose()) - ) - - member inline this.Using - ( - disp: #IAsyncDisposable, - body: #IAsyncDisposable -> TaskSeqCode<'T>, - ?priority: IPriority1 - ) : TaskSeqCode<'T> = - - // FIXME: what about priorities? - ignore priority + member inline this.Using(disp: #IAsyncDisposable, body: #IAsyncDisposable -> ResumableTSC<'T>) : ResumableTSC<'T> = // A using statement is just a try/finally with the finally block disposing if non-null. this.TryFinallyAsync( @@ -575,85 +490,174 @@ type TaskSeqBuilder() = Task.CompletedTask) ) - member inline this.For(sequence: seq<'TElement>, body: 'TElement -> TaskSeqCode<'T>) : TaskSeqCode<'T> = - // A for loop is just a using statement on the sequence's enumerator... - this.Using( - sequence.GetEnumerator(), - // ... and its body is a while loop that advances the enumerator and runs the body on each element. - (fun e -> this.While((fun () -> e.MoveNext()), (fun sm -> (body e.Current).Invoke(&sm)))) - ) - - member inline this.For(source: #IAsyncEnumerable<'TElement>, body: 'TElement -> TaskSeqCode<'T>) : TaskSeqCode<'T> = - TaskSeqCode<'T>(fun sm -> - this - .Using( - source.GetAsyncEnumerator(sm.Data.cancellationToken), - (fun e -> this.WhileAsync((fun () -> e.MoveNextAsync()), (fun sm -> (body e.Current).Invoke(&sm)))) - ) - .Invoke(&sm)) - - member inline _.Yield(v: 'T) : TaskSeqCode<'T> = - TaskSeqCode<'T>(fun sm -> + member inline _.Yield(v: 'T) : ResumableTSC<'T> = + ResumableTSC<'T>(fun sm -> // This will yield with __stack_fin = false // This will resume with __stack_fin = true - logInfo "at Yield" + Debug.logInfo "at Yield" let __stack_fin = ResumableCode.Yield().Invoke(&sm) sm.Data.current <- ValueSome v sm.Data.awaiter <- null __stack_fin) - member inline this.YieldFrom(source: IAsyncEnumerable<'T>) : TaskSeqCode<'T> = - this.For(source, (fun v -> this.Yield(v))) - - member inline this.YieldFrom(source: seq<'T>) : TaskSeqCode<'T> = this.For(source, (fun v -> this.Yield(v))) - - member inline _.Bind(task: Task<'TResult1>, continuation: ('TResult1 -> TaskSeqCode<'T>)) : TaskSeqCode<'T> = - TaskSeqCode<'T>(fun sm -> - let mutable awaiter = task.GetAwaiter() - let mutable __stack_fin = true +// +// These "modules of priority" allow for an indecisive F# to resolve +// the proper overload if a single type implements more than one +// interface. For instance, a type implementing 'IDisposable' and +// 'IAsyncDisposable'. +// +// See for more info tasks.fs in F# Core. +// +// This section also includes the dependencies of such overloads +// (like For depending on Using etc). +// - logInfo "at Bind" - - if not awaiter.IsCompleted then - // This will yield with __stack_fin2 = false - // This will resume with __stack_fin2 = true - let __stack_fin2 = ResumableCode.Yield().Invoke(&sm) - __stack_fin <- __stack_fin2 - - logInfo ("at Bind: with __stack_fin = ", __stack_fin) - logInfo ("at Bind: this.completed = ", sm.Data.completed) +[] +module LowPriority = + type TaskSeqBuilder with + + // + // Note: we cannot place _.Bind directly on the type, as the NoEagerXXX attribute + // has no effect, and each use of `do!` will give an overload error (because the + // `TaskLike` type and the `Task<_>` type are partially interchangeable, see notes there). + // + // However, we cannot unify these two methods, because Task<_> inherits from Task (non-generic) + // and we need a way to distinguish these two methods. + // + // Types handled: + // - ValueTask (non-generic, because it implements GetResult() -> unit) + // - ValueTask<'T> (because it implements GetResult() -> 'TResult) + // - Task (non-generic, because it implements GetResult() -> unit) + // - any other type that implements GetAwaiter() + // + // Not handled: + // - Task<'T> (because it only implements GetResult() -> unit, not GetResult() -> 'TResult) + + [] + member inline _.Bind< ^TaskLike, 'TResult1, 'TResult2, ^Awaiter, 'TOverall + when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter) + and ^Awaiter :> ICriticalNotifyCompletion + and ^Awaiter: (member get_IsCompleted: unit -> bool) + and ^Awaiter: (member GetResult: unit -> 'TResult1)> + ( + task: ^TaskLike, + continuation: ('TResult1 -> ResumableTSC<'TResult2>) + ) : ResumableTSC<'TResult2> = + + ResumableTSC<'TResult2>(fun sm -> + let mutable awaiter = (^TaskLike: (member GetAwaiter: unit -> ^Awaiter) (task)) + let mutable __stack_fin = true + + Debug.logInfo "at TaskLike bind" + + if not (^Awaiter: (member get_IsCompleted: unit -> bool) (awaiter)) then + // This will yield with __stack_fin2 = false + // This will resume with __stack_fin2 = true + let __stack_fin2 = ResumableCode.Yield().Invoke(&sm) + __stack_fin <- __stack_fin2 + + Debug.logInfo ("at TaskLike bind: with __stack_fin = ", __stack_fin) + Debug.logInfo ("at TaskLike bind: this.completed = ", sm.Data.completed) + + if __stack_fin then + Debug.logInfo "at TaskLike bind!: finished awaiting, calling continuation" + let result = (^Awaiter: (member GetResult: unit -> 'TResult1) (awaiter)) + (continuation result).Invoke(&sm) - if __stack_fin then - let result = awaiter.GetResult() - (continuation result).Invoke(&sm) + else + Debug.logInfo "at TaskLike bind: await further" - else - logInfo "at Bind: calling AwaitUnsafeOnCompleted" + sm.Data.awaiter <- awaiter + sm.Data.current <- ValueNone + false) - sm.Data.awaiter <- awaiter - sm.Data.current <- ValueNone - false) - member inline _.Bind(task: ValueTask<'TResult1>, continuation: ('TResult1 -> TaskSeqCode<'T>)) : TaskSeqCode<'T> = - TaskSeqCode<'T>(fun sm -> - let mutable awaiter = task.GetAwaiter() - let mutable __stack_fin = true +[] +module MediumPriority = + type TaskSeqBuilder with + + member inline this.Using(disp: #IDisposable, body: #IDisposable -> ResumableTSC<'T>) : ResumableTSC<'T> = + + // A using statement is just a try/finally with the finally block disposing if non-null. + this.TryFinally( + (fun sm -> (body disp).Invoke(&sm)), + (fun () -> + // yes, this can be null from time to time + if not (isNull (box disp)) then + disp.Dispose()) + ) + + member inline this.For(sequence: seq<'TElement>, body: 'TElement -> ResumableTSC<'T>) : ResumableTSC<'T> = + // A for loop is just a using statement on the sequence's enumerator... + this.Using( + sequence.GetEnumerator(), + // ... and its body is a while loop that advances the enumerator and runs the body on each element. + (fun e -> this.While((fun () -> e.MoveNext()), (fun sm -> (body e.Current).Invoke(&sm)))) + ) + + member inline this.YieldFrom(source: seq<'T>) : ResumableTSC<'T> = this.For(source, (fun v -> this.Yield(v))) + + member inline this.For + ( + source: #IAsyncEnumerable<'TElement>, + body: 'TElement -> ResumableTSC<'T> + ) : ResumableTSC<'T> = + ResumableTSC<'T>(fun sm -> + this + .Using( + source.GetAsyncEnumerator(sm.Data.cancellationToken), + (fun e -> + this.WhileAsync((fun () -> e.MoveNextAsync()), (fun sm -> (body e.Current).Invoke(&sm)))) + ) + .Invoke(&sm)) + + member inline this.YieldFrom(source: IAsyncEnumerable<'T>) : ResumableTSC<'T> = + this.For(source, (fun v -> this.Yield(v))) - logInfo "at BindV" +[] +module HighPriority = + type TaskSeqBuilder with + + // + // Notes Task: + // - Task<_> implements GetAwaiter(), but TaskAwaiter does not implement GetResult() -> TResult + // - Instead, it has GetResult() -> unit, which is not '^TaskLike' + // - Conclusion: we need an extra high-prio overload to allow support for Task<_> + // + // Notes ValueTask: + // - In contrast, ValueTask<_> *does have* GetResult() -> 'TResult + // - Conclusion: we do not need an extra overload anymore for ValueTask + // + member inline _.Bind(task: Task<'TResult1>, continuation: ('TResult1 -> ResumableTSC<'T>)) : ResumableTSC<'T> = + ResumableTSC<'T>(fun sm -> + let mutable awaiter = task.GetAwaiter() + let mutable __stack_fin = true + + Debug.logInfo "at Bind" + + if not awaiter.IsCompleted then + // This will yield with __stack_fin2 = false + // This will resume with __stack_fin2 = true + let __stack_fin2 = ResumableCode.Yield().Invoke(&sm) + __stack_fin <- __stack_fin2 + + Debug.logInfo ("at Bind: with __stack_fin = ", __stack_fin) + Debug.logInfo ("at Bind: this.completed = ", sm.Data.completed) + + if __stack_fin then + Debug.logInfo "at Bind: finished awaiting, calling continuation" + let result = awaiter.GetResult() + (continuation result).Invoke(&sm) - if not awaiter.IsCompleted then - // This will yield with __stack_fin2 = false - // This will resume with __stack_fin2 = true - let __stack_fin2 = ResumableCode.Yield().Invoke(&sm) - __stack_fin <- __stack_fin2 + else + Debug.logInfo "at Bind: await further" - if __stack_fin then - let result = awaiter.GetResult() - (continuation result).Invoke(&sm) - else - logInfo "at BindV: calling AwaitUnsafeOnCompleted" + sm.Data.awaiter <- awaiter + sm.Data.current <- ValueNone + false) - sm.Data.awaiter <- awaiter - sm.Data.current <- ValueNone - false) +[] +module TaskSeqBuilder = + /// Builds an asynchronous task sequence based on IAsyncEnumerable<'T> using computation expression syntax. + let taskSeq = TaskSeqBuilder() diff --git a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi new file mode 100644 index 00000000..4f115910 --- /dev/null +++ b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi @@ -0,0 +1,193 @@ +namespace FSharp.Control + +open System +open System.Threading +open System.Threading.Tasks +open System.Threading.Tasks.Sources +open System.Runtime.CompilerServices +open System.Collections.Generic + +open FSharp.Core.CompilerServices + +[] +module Internal = + + /// + /// Setting from environment variable , which, + /// when set, enables (very) verbose printing of flow and state + /// + val initVerbose: unit -> bool + + /// Call MoveNext on an IAsyncStateMachine by reference + val inline moveNextRef: x: byref<#IAsyncStateMachine> -> unit + + /// F# requires that we implement interfaces even on an abstract class. + val inline raiseNotImpl: unit -> 'a + +/// +/// Result of any computation expression, alias for . +/// +type taskSeq<'T> = IAsyncEnumerable<'T> + +/// TaskSeqCode type alias of ResumableCode delegate type, specially recognized by the F# compiler +and ResumableTSC<'T> = ResumableCode, unit> + +/// +/// Contains the state data for the computation expression builder. +/// For use in this library only. Required by the method. +/// +and TaskSeqStateMachine<'T> = ResumableStateMachine> + +/// +/// Contains the state data for the computation expression builder. +/// For use in this library only. Required by the method. +/// +and [] TaskSeqStateMachineData<'T> = + + new: unit -> TaskSeqStateMachineData<'T> + + [] + val mutable cancellationToken: CancellationToken + + /// Keeps track of the objects that need to be disposed off on IAsyncDispose. + [] + val mutable disposalStack: ResizeArray<(unit -> Task)> + + [] + val mutable awaiter: ICriticalNotifyCompletion + + [] + val mutable promiseOfValueOrEnd: ManualResetValueTaskSourceCore + + /// Helper struct providing methods for awaiting 'next' in async iteration scenarios. + [] + val mutable builder: AsyncIteratorMethodBuilder + + /// Whether or not a full iteration through the IAsyncEnumerator has completed + [] + val mutable completed: bool + + /// Used by the AsyncEnumerator interface to return the Current value when + /// IAsyncEnumerator.Current is called + [] + val mutable current: ValueOption<'T> + + /// A reference to 'self', because otherwise we can't use byref in the resumable code. + [] + val mutable boxedSelf: TaskSeqBase<'T> + + member PopDispose: unit -> unit + + member PushDispose: disposer: (unit -> Task) -> unit + +/// +/// Abstract base class for . +/// For use by this library only, should not be used directly in user code. Its operation depends highly on resumable state. +/// +and [] TaskSeqBase<'T> = + interface IValueTaskSource + interface IValueTaskSource + interface IAsyncStateMachine + interface IAsyncEnumerable<'T> + interface IAsyncEnumerator<'T> + + new: unit -> TaskSeqBase<'T> + + abstract MoveNextAsyncResult: unit -> ValueTask + +/// +/// Main implementation of generic and related interfaces, +/// which forms the meat of the logic behind computation expresssions. +/// For use by this library only, should not be used directly in user code. Its operation depends highly on resumable state. +/// +and [] TaskSeq<'Machine, 'T + when 'Machine :> IAsyncStateMachine and 'Machine :> IResumableStateMachine>> = + inherit TaskSeqBase<'T> + interface IAsyncEnumerator<'T> + interface IAsyncEnumerable<'T> + interface IAsyncStateMachine + interface IValueTaskSource + interface IValueTaskSource + + new: unit -> TaskSeq<'Machine, 'T> + + [] + val mutable _initialMachine: 'Machine + + /// Keeps the active state machine. + [] + val mutable _machine: 'Machine + + //new: unit -> TaskSeq<'Machine, 'T> + member InitMachineData: ct: CancellationToken * machine: byref<'Machine> -> unit + override MoveNextAsyncResult: unit -> ValueTask + +/// +/// Main builder class for the computation expression. +/// +[] +type TaskSeqBuilder = + + member inline Combine: task1: ResumableTSC<'T> * task2: ResumableTSC<'T> -> ResumableTSC<'T> + member inline Delay: f: (unit -> ResumableTSC<'T>) -> ResumableTSC<'T> + member inline Run: code: ResumableTSC<'T> -> taskSeq<'T> + member inline TryFinally: body: ResumableTSC<'T> * compensation: (unit -> unit) -> ResumableTSC<'T> + member inline TryFinallyAsync: body: ResumableTSC<'T> * compensation: (unit -> Task) -> ResumableTSC<'T> + member inline TryWith: body: ResumableTSC<'T> * catch: (exn -> ResumableTSC<'T>) -> ResumableTSC<'T> + member inline Using: disp: 'a * body: ('a -> ResumableTSC<'T>) -> ResumableTSC<'T> when 'a :> IAsyncDisposable + member inline While: condition: (unit -> bool) * body: ResumableTSC<'T> -> ResumableTSC<'T> + /// Used by `For`. F# currently doesn't support `while!`, so this cannot be called directly from the CE + member inline WhileAsync: condition: (unit -> ValueTask) * body: ResumableTSC<'T> -> ResumableTSC<'T> + member inline Yield: v: 'T -> ResumableTSC<'T> + member inline Zero: unit -> ResumableTSC<'T> + +[] +module TaskSeqBuilder = + + /// + /// Builds an asynchronous task sequence based on using computation expression syntax. + /// + val taskSeq: TaskSeqBuilder + +/// +/// Contains low priority extension methods for the main builder class for the computation expression. +/// The , and modules are not meant to be +/// accessed directly from user code. They solely serve to disambiguate overload resolution inside the computation expression. +/// +[] +module LowPriority = + type TaskSeqBuilder with + + [] + member inline Bind< ^TaskLike, 'TResult1, 'TResult2, ^Awaiter, 'TOverall> : + task: ^TaskLike * continuation: ('TResult1 -> ResumableTSC<'TResult2>) -> ResumableTSC<'TResult2> + when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter) + and ^Awaiter :> ICriticalNotifyCompletion + and ^Awaiter: (member get_IsCompleted: unit -> bool) + and ^Awaiter: (member GetResult: unit -> 'TResult1) + +/// +/// Contains low priority extension methods for the main builder class for the computation expression. +/// The , and modules are not meant to be +/// accessed directly from user code. They solely serve to disambiguate overload resolution inside the computation expression. +/// +[] +module MediumPriority = + type TaskSeqBuilder with + + member inline Using: disp: 'a * body: ('a -> ResumableTSC<'T>) -> ResumableTSC<'T> when 'a :> IDisposable + member inline For: sequence: seq<'TElement> * body: ('TElement -> ResumableTSC<'T>) -> ResumableTSC<'T> + member inline YieldFrom: source: seq<'T> -> ResumableTSC<'T> + member inline For: source: #taskSeq<'TElement> * body: ('TElement -> ResumableTSC<'T>) -> ResumableTSC<'T> + member inline YieldFrom: source: taskSeq<'T> -> ResumableTSC<'T> + +/// +/// Contains low priority extension methods for the main builder class for the computation expression. +/// The , and modules are not meant to be +/// accessed directly from user code. They solely serve to disambiguate overload resolution inside the computation expression. +/// +[] +module HighPriority = + type TaskSeqBuilder with + + member inline Bind: task: Task<'TResult1> * continuation: ('TResult1 -> ResumableTSC<'T>) -> ResumableTSC<'T> diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index 7779429a..8f7446ef 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -4,37 +4,37 @@ open System open System.Collections.Generic open System.Threading open System.Threading.Tasks -open FSharp.Control.TaskSeqBuilders -[] -module ExtraTaskSeqOperators = - /// A TaskSeq workflow for IAsyncEnumerable<'T> types. - let taskSeq = TaskSeqBuilder() +[] +type internal AsyncEnumStatus = + | BeforeAll + | WithCurrent + | AfterAll [] -type Action<'T, 'U, 'TaskU when 'TaskU :> Task<'U>> = +type internal Action<'T, 'U, 'TaskU when 'TaskU :> Task<'U>> = | CountableAction of countable_action: (int -> 'T -> 'U) | SimpleAction of simple_action: ('T -> 'U) | AsyncCountableAction of async_countable_action: (int -> 'T -> 'TaskU) | AsyncSimpleAction of async_simple_action: ('T -> 'TaskU) [] -type FolderAction<'T, 'State, 'TaskState when 'TaskState :> Task<'State>> = +type internal FolderAction<'T, 'State, 'TaskState when 'TaskState :> Task<'State>> = | FolderAction of state_action: ('State -> 'T -> 'State) | AsyncFolderAction of async_state_action: ('State -> 'T -> 'TaskState) [] -type ChooserAction<'T, 'U, 'TaskOption when 'TaskOption :> Task<'U option>> = +type internal ChooserAction<'T, 'U, 'TaskOption when 'TaskOption :> Task<'U option>> = | TryPick of try_pick: ('T -> 'U option) | TryPickAsync of async_try_pick: ('T -> 'TaskOption) [] -type PredicateAction<'T, 'TaskBool when 'TaskBool :> Task> = +type internal PredicateAction<'T, 'TaskBool when 'TaskBool :> Task> = | Predicate of try_filter: ('T -> bool) | PredicateAsync of async_try_filter: ('T -> 'TaskBool) [] -type InitAction<'T, 'TaskT when 'TaskT :> Task<'T>> = +type internal InitAction<'T, 'TaskT when 'TaskT :> Task<'T>> = | InitAction of init_item: (int -> 'T) | InitActionAsync of async_init_item: (int -> 'TaskT) @@ -61,6 +61,31 @@ module internal TaskSeqInternal = return not step } + let singleton (source: 'T) = + { new IAsyncEnumerable<'T> with + member _.GetAsyncEnumerator(_) = + let mutable status = BeforeAll + + { new IAsyncEnumerator<'T> with + member _.MoveNextAsync() = + match status with + | BeforeAll -> + status <- WithCurrent + ValueTask.True + | WithCurrent -> + status <- AfterAll + ValueTask.False + | AfterAll -> ValueTask.False + + member _.Current: 'T = + match status with + | WithCurrent -> source + | _ -> Unchecked.defaultof<'T> + + member _.DisposeAsync() = ValueTask.CompletedTask + } + } + /// Returns length unconditionally, or based on a predicate let lengthBy predicate (source: taskSeq<_>) = task { use e = source.GetAsyncEnumerator(CancellationToken()) @@ -272,28 +297,17 @@ module internal TaskSeqInternal = } let zip (source1: taskSeq<_>) (source2: taskSeq<_>) = taskSeq { - let inline validate step1 step2 = - if step1 <> step2 then - if step1 then - invalidArg "taskSequence1" "The task sequences have different lengths." - - if step2 then - invalidArg "taskSequence2" "The task sequences have different lengths." - - use e1 = source1.GetAsyncEnumerator(CancellationToken()) use e2 = source2.GetAsyncEnumerator(CancellationToken()) let mutable go = true let! step1 = e1.MoveNextAsync() let! step2 = e2.MoveNextAsync() go <- step1 && step2 - validate step1 step2 while go do yield e1.Current, e2.Current let! step1 = e1.MoveNextAsync() let! step2 = e2.MoveNextAsync() - validate step1 step2 go <- step1 && step2 } diff --git a/src/FSharp.Control.TaskSeq/Utils.fs b/src/FSharp.Control.TaskSeq/Utils.fs index 74bd115a..63274ed4 100644 --- a/src/FSharp.Control.TaskSeq/Utils.fs +++ b/src/FSharp.Control.TaskSeq/Utils.fs @@ -1,16 +1,47 @@ namespace FSharp.Control open System.Threading.Tasks +open System +open System.Diagnostics +open System.Threading [] module ValueTaskExtensions = /// Extensions for ValueTask that are not available in NetStandard 2.1, but are - /// available in .NET 5+. + /// available in .NET 5+. We put them in Extension space to mimic the behavior of NetStandard 2.1 type ValueTask with /// (Extension member) Gets a task that has already completed successfully. static member inline CompletedTask = Unchecked.defaultof + +module ValueTask = + /// A successfully completed ValueTask of boolean that has the value false. + let False = ValueTask() + + /// A successfully completed ValueTask of boolean that has the value true. + let True = ValueTask true + + /// Creates a ValueTask with the supplied result of the successful operation. + let inline FromResult (x: 'T) = ValueTask<'T> x + + /// Creates a ValueTask with an IValueTaskSource representing the operation + let inline ofIValueTaskSource taskSource version = ValueTask(taskSource, version) + + /// Creates a ValueTask form a Task<'T> + let inline ofTask (task: Task<'T>) = ValueTask<'T>(task) + + /// Ignore a ValueTask<'T>, returns a non-generic ValueTask. + let inline ignore (vtask: ValueTask<'T>) = + // this implementation follows Stephen Toub's advice, see: + // https://github.com/dotnet/runtime/issues/31503#issuecomment-554415966 + if vtask.IsCompletedSuccessfully then + // ensure any side effect executes + vtask.Result |> ignore + ValueTask() + else + ValueTask(vtask.AsTask()) + module Task = /// Convert an Async<'T> into a Task<'T> let inline ofAsync (async: Async<'T>) = task { return! async } @@ -24,24 +55,19 @@ module Task = /// Convert a Task<'T> into an Async<'T> let inline toAsync (task: Task<'T>) = Async.AwaitTask task - /// Convert a Task into a Task - let inline toTask (task: Task) = task :> Task - /// Convert a Task<'T> into a ValueTask<'T> let inline toValueTask (task: Task<'T>) = ValueTask<'T> task - /// Convert a Task into a non-generic ValueTask - let inline toIgnoreValueTask (task: Task) = ValueTask(task :> Task) - /// /// Convert a ValueTask<'T> to a Task<'T>. To use a non-generic ValueTask, /// consider using: . /// let inline ofValueTask (valueTask: ValueTask<'T>) = task { return! valueTask } - /// Convert a Task<'T> into a Task, ignoring the result + /// Convert a Task<'T> into a non-generic Task, ignoring the result let inline ignore (task: Task<'T>) = TaskBuilder.task { + // ensure the task is awaited let! _ = task return () } diff --git a/src/FSharp.Control.TaskSeq/Utils.fsi b/src/FSharp.Control.TaskSeq/Utils.fsi new file mode 100644 index 00000000..219d8e0a --- /dev/null +++ b/src/FSharp.Control.TaskSeq/Utils.fsi @@ -0,0 +1,87 @@ +namespace FSharp.Control + +open System.Diagnostics +open System.Threading.Tasks +open System.Threading.Tasks.Sources + +[] +module ValueTaskExtensions = + type System.Threading.Tasks.ValueTask with + + /// (Extension member) Gets a task that has already completed successfully. + static member inline CompletedTask: System.Threading.Tasks.ValueTask + +module ValueTask = + + /// A successfully completed ValueTask of boolean that has the value false. + val False: ValueTask + + /// A successfully completed ValueTask of boolean that has the value true. + val True: ValueTask + + /// Creates a ValueTask with the supplied result of the successful operation. + val inline FromResult: x: 'T -> ValueTask<'T> + + /// Creates a ValueTask with an IValueTaskSource representing the operation + val inline ofIValueTaskSource: taskSource: IValueTaskSource -> version: int16 -> ValueTask + + /// Creates a ValueTask form a Task<'T> + val inline ofTask: task: Task<'T> -> ValueTask<'T> + + /// Ignore a ValueTask<'T>, returns a non-generic ValueTask. + val inline ignore: vtask: ValueTask<'T> -> ValueTask + +module Task = + + /// Convert an Async<'T> into a Task<'T> + val inline ofAsync: async: Async<'T> -> Task<'T> + + /// Convert a unit-task into a Task + val inline ofTask: task': Task -> Task + + /// Convert a non-task function into a task-returning function + val inline apply: func: ('a -> 'b) -> ('a -> Task<'b>) + + /// Convert a Task<'T> into an Async<'T> + val inline toAsync: task: Task<'T> -> Async<'T> + + /// Convert a Task<'T> into a ValueTask<'T> + val inline toValueTask: task: Task<'T> -> ValueTask<'T> + + /// + /// Convert a ValueTask<'T> to a Task<'T>. To use a non-generic ValueTask, + /// consider using: . + /// + val inline ofValueTask: valueTask: ValueTask<'T> -> Task<'T> + + /// Convert a Task<'T> into a non-generic Task, ignoring the result + val inline ignore: task: Task<'T> -> Task + + /// Map a Task<'T> + val inline map: mapper: ('T -> 'U) -> task: Task<'T> -> Task<'U> + + /// Bind a Task<'T> + val inline bind: binder: ('T -> #Task<'U>) -> task: Task<'T> -> Task<'U> + + /// Create a task from a value + val inline fromResult: value: 'U -> Task<'U> + +module Async = + + /// Convert an Task<'T> into an Async<'T> + val inline ofTask: task: Task<'T> -> Async<'T> + + /// Convert a unit-task into an Async + val inline ofUnitTask: task: Task -> Async + + /// Convert a Task<'T> into an Async<'T> + val inline toTask: async: Async<'T> -> Task<'T> + + /// Convert an Async<'T> into an Async, ignoring the result + val inline ignore: async': Async<'T> -> Async + + /// Map an Async<'T> + val inline map: mapper: ('T -> 'U) -> async: Async<'T> -> Async<'U> + + /// Bind an Async<'T> + val inline bind: binder: (Async<'T> -> Async<'U>) -> task: Async<'T> -> Async<'U>