diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4ac8d90b..19b688b1 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -18,23 +18,3 @@ jobs: # build it, test it, pack it - name: Run dotnet build (release) 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..02eb3c0d 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -45,23 +45,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..c97c3343 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,30 @@ +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) + 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/.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/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..dd5c7f4c --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/README.md b/README.md index 7da1e2fe..126493b2 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ [![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. - -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. +An implementation of [`IAsyncEnumerable<'T>`][3] as a computation expression: `taskSeq { ... }` with an accompanying `TaskSeq` module. ----------------------------------------- @@ -18,10 +17,15 @@ 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) @@ -38,27 +42,134 @@ The `IAsyncEnumerable` interface was added to .NET in `.NET Core 3.0` and is par ----------------------------------------- -## Feature planning +## 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]. + +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: -Not necessarily in order of importance: +```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 hello = taskSeq { yield "Hello, World!" } + +// can be mixed with normal sequences +let oneToTen = taskSeq { yield! [1..10] } + +// 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. @@ -283,8 +394,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 +404,92 @@ 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 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 +517,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 @@ -614,3 +532,4 @@ module TaskSeq = [#82]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/82 [#83]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/83 +[issues]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues \ No newline at end of file diff --git a/Version.props b/Version.props new file mode 100644 index 00000000..7f8337f7 --- /dev/null +++ b/Version.props @@ -0,0 +1,5 @@ + + + 0.2.2 + + \ No newline at end of file diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.ico b/assets/TaskSeq.ico similarity index 100% rename from src/FSharp.Control.TaskSeq.Test/TaskSeq.ico rename to assets/TaskSeq.ico diff --git a/assets/nuget-package-readme.md b/assets/nuget-package-readme.md new file mode 100644 index 00000000..6fa010bf --- /dev/null +++ b/assets/nuget-package-readme.md @@ -0,0 +1,289 @@ +# TaskSeq + +An implementation of [`IAsyncEnumerable<'T>`][3] as a computation expression: `taskSeq { ... }` with an accompanying `TaskSeq` module. + +----------------------------------------- + +## Table of contents + + + +- [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 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]. + +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](#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 +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. + +### Examples + +```f# +open System.IO + +open FSharp.Control + +// singleton is fine +let hello = taskSeq { yield "Hello, World!" } + +// can be mixed with normal sequences +let oneToTen = taskSeq { yield! [1..10] } + +// 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 +} +``` + +### `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. + +The following is the progress report: + +| Done | `Seq` | `TaskSeq` | Variants | Remarks | +|------------------|--------------------|-----------------|----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ❓ | `allPairs` | `allPairs` | | [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.") | +| ✅ [#81][] | `append` | `append` | | | +| ✅ [#81][] | | | `appendSeq` | | +| ✅ [#81][] | | | `prependSeq` | | +| | `average` | `average` | | | +| | `averageBy` | `averageBy` | `averageByAsync` | | +| ❓ | `cache` | `cache` | | [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.") | +| ✅ [#67][] | `cast` | `cast` | | | +| ✅ [#67][] | | | `box` | | +| ✅ [#67][] | | | `unbox` | | +| ✅ [#23][] | `choose` | `choose` | `chooseAsync` | | +| | `chunkBySize` | `chunkBySize` | | | +| ✅ [#11][] | `collect` | `collect` | `collectAsync` | | +| ✅ [#11][] | | `collectSeq` | `collectSeqAsync` | | +| | `compareWith` | `compareWith` | `compareWithAsync` | | +| ✅ [#69][] | `concat` | `concat` | | | +| ✅ [#70][] | `contains` | `contains` | | | +| ✅ [#82][] | `delay` | `delay` | | | +| | `distinct` | `distinct` | | | +| | `distinctBy` | `dictinctBy` | `distinctByAsync` | | +| ✅ [#2][] | `empty` | `empty` | | | +| ✅ [#23][] | `exactlyOne` | `exactlyOne` | | | +| ✅ [#83][] | `except` | `except` | | | +| ✅ [#83][] | | `exceptOfSeq` | | | +| ✅ [#70][] | `exists` | `exists` | `existsAsync` | | +| | `exists2` | `exists2` | | | +| ✅ [#23][] | `filter` | `filter` | `filterAsync` | | +| ✅ [#23][] | `find` | `find` | `findAsync` | | +| 🚫 | `findBack` | | | [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.") | +| ✅ [#68][] | `findIndex` | `findIndex` | `findIndexAsync` | | +| 🚫 | `findIndexBack` | n/a | n/a | [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.") | +| ✅ [#2][] | `fold` | `fold` | `foldAsync` | | +| | `fold2` | `fold2` | `fold2Async` | | +| 🚫 | `foldBack` | | | [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.") | +| 🚫 | `foldBack2` | | | [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.") | +| | `forall` | `forall` | `forallAsync` | | +| | `forall2` | `forall2` | `forall2Async` | | +| ❓ | `groupBy` | `groupBy` | `groupByAsync` | [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.") | +| ✅ [#23][] | `head` | `head` | | | +| ✅ [#68][] | `indexed` | `indexed` | | | +| ✅ [#69][] | `init` | `init` | `initAsync` | | +| ✅ [#69][] | `initInfinite` | `initInfinite` | `initInfiniteAsync` | | +| | `insertAt` | `insertAt` | | | +| | `insertManyAt` | `insertManyAt` | | | +| ✅ [#23][] | `isEmpty` | `isEmpty` | | | +| ✅ [#23][] | `item` | `item` | | | +| ✅ [#2][] | `iter` | `iter` | `iterAsync` | | +| | `iter2` | `iter2` | `iter2Async` | | +| ✅ [#2][] | `iteri` | `iteri` | `iteriAsync` | | +| | `iteri2` | `iteri2` | `iteri2Async` | | +| ✅ [#23][] | `last` | `last` | | | +| ✅ [#53][] | `length` | `length` | | | +| ✅ [#53][] | | `lengthBy` | `lengthByAsync` | | +| ✅ [#2][] | `map` | `map` | `mapAsync` | | +| | `map2` | `map2` | `map2Async` | | +| | `map3` | `map3` | `map3Async` | | +| | `mapFold` | `mapFold` | `mapFoldAsync` | | +| 🚫 | `mapFoldBack` | | | [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.") | +| ✅ [#2][] | `mapi` | `mapi` | `mapiAsync` | | +| | `mapi2` | `mapi2` | `mapi2Async` | | +| | `max` | `max` | | | +| | `maxBy` | `maxBy` | `maxByAsync` | | +| | `min` | `min` | | | +| | `minBy` | `minBy` | `minByAsync` | | +| ✅ [#2][] | `ofArray` | `ofArray` | | | +| ✅ [#2][] | | `ofAsyncArray` | | | +| ✅ [#2][] | | `ofAsyncList` | | | +| ✅ [#2][] | | `ofAsyncSeq` | | | +| ✅ [#2][] | `ofList` | `ofList` | | | +| ✅ [#2][] | | `ofTaskList` | | | +| ✅ [#2][] | | `ofResizeArray` | | | +| ✅ [#2][] | | `ofSeq` | | | +| ✅ [#2][] | | `ofTaskArray` | | | +| ✅ [#2][] | | `ofTaskList` | | | +| ✅ [#2][] | | `ofTaskSeq` | | | +| | `pairwise` | `pairwise` | | | +| | `permute` | `permute` | `permuteAsync` | | +| ✅ [#23][] | `pick` | `pick` | `pickAsync` | | +| 🚫 | `readOnly` | | | [note #3](#note-3 "The motivation for 'readOnly' in 'Seq' is that a cast from a mutable array or list to a 'seq<_>' is valid and can be cast back, leading to a mutable sequence. Since 'TaskSeq' doesn't implement 'IEnumerable<_>', such casts are not possible.") | +| | `reduce` | `reduce` | `reduceAsync` | | +| 🚫 | `reduceBack` | | | [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.") | +| | `removeAt` | `removeAt` | | | +| | `removeManyAt` | `removeManyAt` | | | +| | `replicate` | `replicate` | | | +| ❓ | `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` | | | +| | `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.") | +| ❓ | `sortBy` | | | [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.") | +| ❓ | `sortByAscending` | | | [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.") | +| ❓ | `sortByDescending` | | | [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.") | +| ❓ | `sortWith` | | | [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.") | +| | `splitInto` | `splitInto` | | | +| | `sum` | `sum` | | | +| | `sumBy` | `sumBy` | `sumByAsync` | | +| ✅ [#76][] | `tail` | `tail` | | | +| | `take` | `take` | | | +| | `takeWhile` | `takeWhile` | `takeWhileAsync` | | +| ✅ [#2][] | `toArray` | `toArray` | `toArrayAsync` | | +| ✅ [#2][] | | `toIList` | `toIListAsync` | | +| ✅ [#2][] | `toList` | `toList` | `toListAsync` | | +| ✅ [#2][] | | `toResizeArray` | `toResizeArrayAsync` | | +| ✅ [#2][] | | `toSeq` | `toSeqAsync` | | +| | | […] | | | +| ❓ | `transpose` | | | [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.") | +| | `truncate` | `truncate` | | | +| ✅ [#23][] | `tryExactlyOne` | `tryExactlyOne` | `tryExactlyOneAsync` | | +| ✅ [#23][] | `tryFind` | `tryFind` | `tryFindAsync` | | +| 🚫 | `tryFindBack` | | | [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.") | +| ✅ [#68][] | `tryFindIndex` | `tryFindIndex` | `tryFindIndexAsync` | | +| 🚫 | `tryFindIndexBack` | | | [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.") | +| ✅ [#23][] | `tryHead` | `tryHead` | | | +| ✅ [#23][] | `tryItem` | `tryItem` | | | +| ✅ [#23][] | `tryLast` | `tryLast` | | | +| ✅ [#23][] | `tryPick` | `tryPick` | `tryPickAsync` | | +| ✅ [#76][] | | `tryTail` | | | +| | `unfold` | `unfold` | `unfoldAsync` | | +| | `updateAt` | `updateAt` | | | +| | `where` | `where` | `whereAsync` | | +| | `windowed` | `windowed` | | | +| ✅ [#2][] | `zip` | `zip` | | | +| | `zip3` | `zip3` | | | +| | | `zip4` | | | + +#### 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._ + +#### 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 `xxxBack` iterators._ + +#### Note 3 + +_The motivation for `readOnly` in `Seq` is that a cast from a mutable array or list to a `seq<_>` is valid and can be cast back, leading to a mutable sequence. Since `TaskSeq` doesn't implement `IEnumerable<_>`, such casts are not possible._ + +## More information + +### Futher 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 + +- 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] +- The [original RFC for introducing `task`][14] to F# 6.0. +- A [pre F# 6.0 `TaskBuilder`][15] that motivated the `task` CE later added to F# Core. +- [MSDN Documentation on `task`][16] and [`async`][17]. + +### Further reading on computation expressions + +- [Docs on MSDN][18] form a good summary and starting point. +- Arguably the best [step-by-step tutorial to using and building computation expressions][19] by Scott Wlaschin. + +[3]: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1?view=net-7.0 +[4]: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerator-1.movenextasync?view=net-7.0 +[5]: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerator-1?view=net-7.0 +[6]: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1.getasyncenumerator?view=net-7.0 +[7]: https://learn.microsoft.com/en-us/dotnet/api/system.iasyncdisposable?view=net-7.0 +[8]: https://stu.dev/iasyncenumerable-introduction/ +[9]: https://learn.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8 +[10]: https://gist.github.com/akhansari/d88812b742aa6be1c35b4f46bd9f8532 +[11]: https://fsprojects.github.io/FSharp.Control.AsyncSeq/AsyncSeq.html +[12]: http://blumu.github.io/ResumableMonad/TheResumableMonad.html +[13]: https://github.com/fsharp/fslang-design/blob/main/FSharp-6.0/FS-1087-resumable-code.md +[14]: https://github.com/fsharp/fslang-design/blob/main/FSharp-6.0/FS-1097-task-builder.md +[15]: https://github.com/rspeele/TaskBuilder.fs/ +[16]: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/task-expressions +[17]: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/async-expressions +[18]: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions +[19]: https://fsharpforfunandprofit.com/series/computation-expressions/ + +[#2]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/2 +[#11]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/11 +[#23]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/23 +[#53]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/53 +[#67]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/67 +[#68]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/68 +[#69]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/69 +[#70]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/70 +[#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 diff --git a/assets/taskseq-icon.png b/assets/taskseq-icon.png new file mode 100644 index 00000000..b14a77c1 Binary files /dev/null and b/assets/taskseq-icon.png 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 9914b72c..a0760a87 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -1,14 +1,15 @@ - + net6.0 false false - TaskSeq.ico + ..\..\assets\TaskSeq.ico + @@ -50,11 +51,10 @@ - - - - - + + + + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Append.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Append.Tests.fs index 0448acc7..629dd777 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Append.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Append.Tests.fs @@ -17,8 +17,8 @@ open System.Collections.Generic let validateSequence ts = ts - |> TaskSeq.toSeqCachedAsync - |> Task.map (Seq.map string) + |> TaskSeq.toListAsync + |> Task.map (List.map string) |> Task.map (String.concat "") |> Task.map (should equal "1234567891012345678910") diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Cast.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Cast.Tests.fs index 2dfaa57b..378253f4 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Cast.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Cast.Tests.fs @@ -17,8 +17,8 @@ open FSharp.Control /// Asserts that a sequence contains the char values 'A'..'J'. let validateSequence ts = ts - |> TaskSeq.toSeqCachedAsync - |> Task.map (Seq.map string) + |> TaskSeq.toListAsync + |> Task.map (List.map string) |> Task.map (String.concat "") |> Task.map (should equal "12345678910") diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Collect.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Collect.Tests.fs index fd3d7676..ba975595 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Collect.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Collect.Tests.fs @@ -102,8 +102,8 @@ module Immutable = let validateSequence ts = ts - |> TaskSeq.toSeqCachedAsync - |> Task.map (Seq.map string) + |> TaskSeq.toListAsync + |> Task.map (List.map string) |> Task.map (String.concat "") |> Task.map (should equal "ABBCCDDEEFFGGHHIIJJK") diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Concat.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Concat.Tests.fs index df4c07e8..cfbd9464 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Concat.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Concat.Tests.fs @@ -15,8 +15,8 @@ open System.Collections.Generic let validateSequence ts = ts - |> TaskSeq.toSeqCachedAsync - |> Task.map (Seq.map string) + |> TaskSeq.toListAsync + |> Task.map (List.map string) |> Task.map (String.concat "") |> Task.map (should equal "123456789101234567891012345678910") diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Delay.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Delay.Tests.fs index 20026528..a13c9111 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Delay.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Delay.Tests.fs @@ -15,8 +15,8 @@ open System.Collections.Generic let validateSequence ts = ts - |> TaskSeq.toSeqCachedAsync - |> Task.map (Seq.map string) + |> TaskSeq.toListAsync + |> Task.map (List.map string) |> Task.map (String.concat "") |> Task.map (should equal "12345678910") diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Empty.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Empty.Tests.fs index a9477eb1..ccdba831 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Empty.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Empty.Tests.fs @@ -10,7 +10,7 @@ open FSharp.Control [] let ``TaskSeq-empty returns an empty sequence`` () = task { - let! sq = TaskSeq.empty |> TaskSeq.toSeqCachedAsync + let! sq = TaskSeq.empty |> TaskSeq.toListAsync Seq.isEmpty sq |> should be True Seq.length sq |> should equal 0 } diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Map.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Map.Tests.fs index c9a1ab1b..4793684e 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Map.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Map.Tests.fs @@ -16,8 +16,8 @@ open FSharp.Control /// Asserts that a sequence contains the char values 'A'..'J'. let validateSequence ts = ts - |> TaskSeq.toSeqCachedAsync - |> Task.map (Seq.map string) + |> TaskSeq.toListAsync + |> Task.map (List.map string) |> Task.map (String.concat "") |> Task.map (should equal "ABCDEFGHIJ") @@ -29,8 +29,8 @@ let validateSequenceWithOffset offset ts = |> String.concat "" ts - |> TaskSeq.toSeqCachedAsync - |> Task.map (Seq.map string) + |> TaskSeq.toListAsync + |> Task.map (List.map string) |> Task.map (String.concat "") |> Task.map (should equal expected) diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.ToXXX.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.ToXXX.Tests.fs index 42656317..fbb5635b 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.ToXXX.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.ToXXX.Tests.fs @@ -35,13 +35,6 @@ module EmptySeq = results |> should be Empty } - [)>] - let ``TaskSeq-toSeqCachedAsync with empty`` variant = task { - let tq = Gen.getEmptyVariant variant - let! (results: seq<_>) = tq |> TaskSeq.toSeqCachedAsync - results |> Seq.toArray |> should be Empty - } - [)>] let ``TaskSeq-toIListAsync with empty`` variant = task { let tq = Gen.getEmptyVariant variant @@ -71,7 +64,7 @@ module EmptySeq = [)>] let ``TaskSeq-toSeqCached with empty`` variant = let tq = Gen.getEmptyVariant variant - let (results: seq<_>) = tq |> TaskSeq.toSeqCached + let (results: seq<_>) = tq |> TaskSeq.toSeq results |> Seq.toArray |> should be Empty module Immutable = @@ -90,13 +83,6 @@ module Immutable = results |> should equal [ 1..10 ] } - [)>] - let ``TaskSeq-toSeqCachedAsync should succeed`` variant = task { - let tq = Gen.getSeqImmutable variant - let! (results: seq<_>) = tq |> TaskSeq.toSeqCachedAsync - results |> Seq.toArray |> should equal [| 1..10 |] - } - [)>] let ``TaskSeq-toIListAsync should succeed`` variant = task { let tq = Gen.getSeqImmutable variant @@ -126,7 +112,7 @@ module Immutable = [)>] let ``TaskSeq-toSeqCached should succeed and be blocking`` variant = let tq = Gen.getSeqImmutable variant - let (results: seq<_>) = tq |> TaskSeq.toSeqCached + let (results: seq<_>) = tq |> TaskSeq.toSeq results |> Seq.toArray |> should equal [| 1..10 |] @@ -159,15 +145,6 @@ module SideEffects = results2 |> should equal [ 11..20 ] } - [)>] - let ``TaskSeq-toSeqCachedAsync should execute side effects multiple times`` variant = task { - let tq = Gen.getSeqWithSideEffect variant - let! (results1: seq<_>) = tq |> TaskSeq.toSeqCachedAsync - let! (results2: seq<_>) = tq |> TaskSeq.toSeqCachedAsync - results1 |> Seq.toArray |> should equal [| 1..10 |] - results2 |> Seq.toArray |> should equal [| 11..20 |] - } - [)>] let ``TaskSeq-toIListAsync should execute side effects multiple times`` variant = task { let tq = Gen.getSeqWithSideEffect variant @@ -205,7 +182,7 @@ module SideEffects = [)>] let ``TaskSeq-toSeqCached should execute side effects multiple times`` variant = let tq = Gen.getSeqWithSideEffect variant - let (results1: seq<_>) = tq |> TaskSeq.toSeqCached - let (results2: seq<_>) = tq |> TaskSeq.toSeqCached + let (results1: seq<_>) = tq |> TaskSeq.toSeq + let (results2: seq<_>) = tq |> TaskSeq.toSeq results1 |> Seq.toArray |> should equal [| 1..10 |] results2 |> Seq.toArray |> should equal [| 11..20 |] diff --git a/src/FSharp.Control.TaskSeq.Test/TestUtils.fs b/src/FSharp.Control.TaskSeq.Test/TestUtils.fs index ef50dba8..91baf383 100644 --- a/src/FSharp.Control.TaskSeq.Test/TestUtils.fs +++ b/src/FSharp.Control.TaskSeq.Test/TestUtils.fs @@ -138,8 +138,8 @@ type DummyTaskFactory(µsecMin: int64<µs>, µsecMax: int64<µs>) = module TestUtils = let verifyEmpty ts = ts - |> TaskSeq.toSeqCachedAsync - |> Task.map (Seq.isEmpty >> should be True) + |> TaskSeq.toArrayAsync + |> Task.map (Array.isEmpty >> should be True) /// Delays (no spin-wait!) between 20 and 70ms, assuming a 15.6ms resolution clock let longDelay () = task { do! Task.Delay(Random().Next(20, 70)) } diff --git a/src/FSharp.Control.TaskSeq.sln b/src/FSharp.Control.TaskSeq.sln index 2df77210..2e1e32f6 100644 --- a/src/FSharp.Control.TaskSeq.sln +++ b/src/FSharp.Control.TaskSeq.sln @@ -8,8 +8,11 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B252135E-C676-4542-8B72-412DF1B9487C}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + ..\.gitignore = ..\.gitignore ..\build.cmd = ..\build.cmd + ..\Directory.Build.props = ..\Directory.Build.props ..\README.md = ..\README.md + ..\Version.props = ..\Version.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{E55512EE-8DE2-4B44-9A4A-CF779734160B}" @@ -17,11 +20,19 @@ 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.yaml = ..\.github\workflows\test.yaml EndProjectSection EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharp.Control.TaskSeq.Test", "FSharp.Control.TaskSeq.Test\FSharp.Control.TaskSeq.Test.fsproj", "{06CA2C7E-04DA-4A85-BB8E-4D94BD67AEB3}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{B198D5FE-A731-4AFA-96A5-E5DD94EE293D}" + 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 GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,6 +53,7 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {E55512EE-8DE2-4B44-9A4A-CF779734160B} = {B252135E-C676-4542-8B72-412DF1B9487C} + {B198D5FE-A731-4AFA-96A5-E5DD94EE293D} = {B252135E-C676-4542-8B72-412DF1B9487C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2AE57787-A847-4460-A627-1EB1D224FBC3} diff --git a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj index ddba9897..d0298601 100644 --- a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj +++ b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj @@ -1,20 +1,67 @@ - net6.0 + netstandard2.1 true - TaskSeq.ico - + ..\..\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#, 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, with minimal overhead through ValueTask under the hood. TaskSeq brings 'seq' and 'task' together in a safe way. - - +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 + taskseq-icon.png + ..\..\packages + 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 + + + taskseq'fsharp;f#;computation expression;IAsyncEnumerable;task;async;asyncseq; + True + snupkg + + + True + \ + + @@ -22,8 +69,13 @@ + + + + + - + + - diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index 9fb7e314..b94fbb26 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -46,7 +46,7 @@ module TaskSeq = e.DisposeAsync().AsTask().Wait() |] - let toSeqCached (source: taskSeq<'T>) = seq { + let toSeq (source: taskSeq<'T>) = seq { let e = source.GetAsyncEnumerator(CancellationToken()) try @@ -66,8 +66,6 @@ module TaskSeq = let toIListAsync source = Internal.toResizeArrayAndMapAsync (fun x -> x :> IList<_>) source - let toSeqCachedAsync source = Internal.toResizeArrayAndMapAsync (fun x -> x :> seq<_>) source - // // Convert 'OfXXX' functions // diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index dcbab450..aeae538d 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -154,8 +154,16 @@ module TaskSeq = /// Returns taskSeq as an array. This function is blocking until the sequence is exhausted and will properly dispose of the resources. val toArray: source: 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: source: taskSeq<'T> -> seq<'T> + /// + /// Returns the task sequence as an F# , that is, an + /// . This function is blocking at each , but otherwise + /// acts as a normal delay-executed sequence. + /// It will then dispose of the resources. + /// + /// + /// The input task sequence. + /// The resulting task sequence. + val toSeq: source: taskSeq<'T> -> seq<'T> /// Unwraps the taskSeq as a Task>. This function is non-blocking. val toArrayAsync: source: taskSeq<'T> -> Task<'T[]> @@ -169,10 +177,6 @@ module TaskSeq = /// Unwraps the taskSeq as a Task>. This function is non-blocking. val toIListAsync: source: 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: source: taskSeq<'T> -> Task> - /// Create a taskSeq of an array. val ofArray: source: 'T[] -> taskSeq<'T> diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.ico b/src/FSharp.Control.TaskSeq/TaskSeq.ico deleted file mode 100644 index 65d3ee0a..00000000 Binary files a/src/FSharp.Control.TaskSeq/TaskSeq.ico and /dev/null differ diff --git a/src/FSharp.Control.TaskSeq/Utils.fs b/src/FSharp.Control.TaskSeq/Utils.fs index ecc2f685..74bd115a 100644 --- a/src/FSharp.Control.TaskSeq/Utils.fs +++ b/src/FSharp.Control.TaskSeq/Utils.fs @@ -2,6 +2,15 @@ namespace FSharp.Control open System.Threading.Tasks +[] +module ValueTaskExtensions = + /// Extensions for ValueTask that are not available in NetStandard 2.1, but are + /// available in .NET 5+. + type ValueTask with + + /// (Extension member) Gets a task that has already completed successfully. + static member inline CompletedTask = Unchecked.defaultof + module Task = /// Convert an Async<'T> into a Task<'T> let inline ofAsync (async: Async<'T>) = task { return! async }