An open API service indexing awesome lists of open source software.

https://github.com/fsprojects/fsharp.control.taskseq

A computation expression and module for seamless working with IAsyncEnumerable<'T> as if it is just another sequence
https://github.com/fsprojects/fsharp.control.taskseq

computation-expressions fsharp iasyncenumerable library module package task taskseq tasksequence

Last synced: 7 months ago
JSON representation

A computation expression and module for seamless working with IAsyncEnumerable<'T> as if it is just another sequence

Awesome Lists containing this project

README

          

[![build][buildstatus_img]][buildstatus]
[![test][teststatus_img]][teststatus]
[![Nuget](https://buildstats.info/nuget/FSharp.Control.TaskSeq?includePreReleases=true)](https://www.nuget.org/packages/FSharp.Control.TaskSeq/)

# TaskSeq

An implementation of [`IAsyncEnumerable<'T>`][3] as a computation expression: `taskSeq { ... }` with an accompanying `TaskSeq` module and functions, that allow seamless use of asynchronous sequences similar to F#'s native `seq` and `task` CE's.

* Latest stable version: [0.4.0 is on NuGet][nuget].

## Release notes

See [Releases](https://github.com/fsprojects/FSharp.Control.TaskSeq/releases) for the an extensive version history of `TaskSeq`. See [Status overview](#status--planning) below for a progress report.

-----------------------------------------

## Table of contents

- [Overview](#overview)
- [Module functions](#module-functions)
- [`taskSeq` computation expressions](#taskseq-computation-expressions)
- [Installation](#installation)
- [Examples](#examples)
- [Choosing between `AsyncSeq` and `TaskSeq`](#choosing-between-asyncseq-and-taskseq)
- [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)
- [Further reading on `IAsyncEnumerable`](#further-reading-on-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)
- [Build the solution](#build-the-solution)
- [Run the tests](#run-the-tests)
- [Run the CI command](#run-the-ci-command)
- [Advanced](#advanced)
- [Get help](#get-help)
- [Work in progress](#work-in-progress)
- [Current set of `TaskSeq` utility functions](#current-set-of-taskseq-utility-functions)

-----------------------------------------

## 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, sequential 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>`][3] 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 iteration over an `IAsyncEnumerable` has proven challenging, especially if one wants to avoid mutable variables. This library is an answer to that call and applies 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`, `takeWhile`. Where applicable, these come with async variants, like `TaskSeq.mapAsync` `iterAsync`, `collectAsync`, `foldAsync` and `TaskSeq.findAsync`, `pickAsync`, `chooseAsync`, `filterAsync`, `takeWhileAsync` 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`.
Additionally, it adds support for working with `Task`s through `let!` and
looping over both normal and asynchronous sequences (ones that implement
`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.4.0"
```

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
}
```

## Choosing between `AsyncSeq` and `TaskSeq`

The [`AsyncSeq`][11] and `TaskSeq` libraries both operate on asynchronous sequences, but there are a few fundamental differences. The most notable being that the former _does not_ implement `IAsyncEnumerable<'T>`, though it does have a type of that name with different semantics (not surprising; it predates the definition of the modern one). Another key difference is that `TaskSeq` uses `ValueTask`s for the asynchronous computations, whereas `AsyncSeq` uses F#'s `Async<'T>`.

There are more differences:

| | `TaskSeq` | `AsyncSeq` |
|----------------------------|---------------------------------------------------------------------------------|----------------------------------------------------------------------|
| **Frameworks** | .NET 5.0+, NetStandard 2.1 | .NET 5.0+, NetStandard 2.0 and 2.1, .NET Framework 4.6.1+ |
| **F# concept of** | `task` | `async` |
| **Underlying type** | [`Generic.IAsyncEnumerable<'T>`][3] [note #1](#tsnote1 "Full name System.Collections.Generic.IAsyncEnumerable<'T>.")| Its own type, also called `IAsyncEnumerable<'T>`[note #1](#tsnote1 "Full name FSharp.Control.IAsyncEnumerable<'T>.") |
| **Implementation** | State machine (statically compiled) | No state machine, continuation style |
| **Semantics** | `seq`-like: on-demand | `seq`-like: on-demand |
| **Disposability** | Asynchronous, through [`IAsyncDisposable`][7] | Synchronous, through `IDisposable` |
| **Support `let!`** | All `task`-like: `Async<'T>`, `Task<'T>`, `ValueTask<'T>` or any `GetAwaiter()` | `Async<'T>` only |
| **Support `do!`** | `Async`, `Task` and `Task`, `ValueTask` and `ValueTask` | `Async` only |
| **Support `yield!`** | [`IAsyncEnumerable<'T>`][3] (= `TaskSeq`), `AsyncSeq`, any sequence | `AsyncSeq` |
| **Support `for`** | [`IAsyncEnumerable<'T>`][3] (= `TaskSeq`), `AsyncSeq`, any sequence | `AsyncSeq`, any sequence |
| **Behavior with `yield`** | Zero allocations; no `Task` or even `ValueTask` created | Allocates an F# `Async` wrapped in a singleton `AsyncSeq` |
| **Conversion to other** | `TaskSeq.toAsyncSeq` | [`AsyncSeq.toAsyncEnum`][22] |
| **Conversion from other** | Implicit (`yield!`) or `TaskSeq.ofAsyncSeq` | [`AsyncSeq.ofAsyncEnum`][23] |
| **Recursion in `yield!`** | **No** (requires F# support, upcoming) | Yes |
| **Iteration semantics** | [Two operations][6], 'Next' is a value task, 'Current' must be called separately| One operation, 'Next' is `Async`, returns `option` with 'Current' |
| **`MoveNextAsync`** | [Returns `ValueTask`][4] | Returns `Async<'T option>` |
| **[`Current`][5]** | [Returns `'T`][5] | n/a |
| **Cancellation** | See [#133][], until 0.3.0: use `GetAsyncEnumerator(cancelToken)` | Implicit token flows to all subtasks per `async` semantics |
| **Performance** | Very high, negligible allocations | Slower, more allocations, due to using `async` and cont style |
| **Parallelism** | Unclear, interface is meant for _sequential/async_ processing | Supported by extension functions |

¹⁾ _Both `AsyncSeq` and `TaskSeq` use a type called `IAsyncEnumerable<'T>`, but only `TaskSeq` uses the type from the BCL Generic Collections. `AsyncSeq` supports .NET Framework 4.6.x and NetStandard 2.0 as well, which do not have this type in the BCL._

## Status & planning

The `TaskSeq` project already has a wide array of functions and functionalities, see overview below. The current status is: *STABLE*. However, certain features we'd really like to add:

- [x] Take existing `taskSeq` resumable code from F# and fix it. **DONE**
- [x] Add almost all functions from `Seq` that could apply to `TaskSeq` (full overview below). **MOSTLY DONE, STILL TODO**
- [ ] Add remaining relevant functions from `Seq`. **PLANNED FOR 0.4.x**
- [x] `min` / `max` / `minBy` / `maxBy` & async variant (see [#221])
- [x] `insertAt` / `updateAt` and related (see [#236])
- [ ] `average` / `averageBy`, `sum` and related
- [x] `forall` / `forallAsync` (see [#240])
- [x] `skip` / `drop` / `truncate` / `take` (see [#209])
- [ ] `chunkBySize` / `windowed`
- [ ] `compareWith`
- [ ] `distinct`
- [ ] `exists2` / `map2` / `fold2` / `iter2` and related '2'-functions
- [ ] `mapFold`
- [ ] `pairwise` / `allpairs` / `permute` / `distinct` / `distinctBy`
- [ ] `replicate`
- [ ] `reduce` / `scan`
- [ ] `unfold`
- [x] Publish package on Nuget, **DONE, PUBLISHED SINCE: 7 November 2022**. See https://www.nuget.org/packages/FSharp.Control.TaskSeq
- [x] Make `TaskSeq` interoperable with `Task` by expanding the latter with a `for .. in .. do` that acceps task sequences
- [x] Add to/from functions to seq, list, array
- [ ] Add applicable functions from `AsyncSeq`. **PLANNED FOR 0.5-alpha**
- [ ] (Better) support for cancellations
- [ ] Make the tasks cancellable with token (see [#133]). **PLANNED FOR 0.5-alpha**
- [ ] Support `ConfiguredCancelableAsyncEnumerable` (see [#167]). **PLANNED FOR 0.5-alpha**
- [ ] Interop with `cancellableTask` and `valueTask` from [`IcedTasks`][24]
- [ ] Interop with `AsyncSeq`.
- [ ] (maybe) Support any awaitable type in the function lib (that is: where a `Task` is required, accept a `ValueTask` and `Async` as well)
- [ ] Add `TaskEx` functionality (separate lib). **DISCUSSION**
- [ ] Move documentation to

### Implementation progress

* As of 9 November 2022: [Nuget package available][21]. In this phase, we will frequently update the package, see [release notes.txt](release-notes.txt). Current version:
* Major update: 17 March 2024, version 0.4.0

[![Nuget](https://img.shields.io/nuget/vpre/FSharp.Control.TaskSeq)](https://www.nuget.org/packages/FSharp.Control.TaskSeq/)

### 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!][issues].

### 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 by `AsyncSeq`. Each implemented function is documented through XML doc comments to provide the necessary context-sensitive help.

This is what has been implemented so far, is planned or skipped:

| Done | `Seq` | `TaskSeq` | Variants | Remarks |
|------------------|--------------------|----------------------|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ❓ | `allPairs` | `allPairs` | | [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.") |
| ✅ [#81][] | `append` | `append` | | |
| ✅ [#81][] | | | `appendSeq` | |
| ✅ [#81][] | | | `prependSeq` | |
| | `average` | `average` | | |
| | `averageBy` | `averageBy` | `averageByAsync` | |
| ❓ | `cache` | `cache` | | [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.") |
| ✅ [#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` | | |
| ✅ [#237][]| `concat` (list) | `concat` (list) | | |
| ✅ [#237][]| `concat` (array) | `concat` (array) | | |
| ✅ [#237][]| `concat` (r-array) | `concat` (r-array) | | |
| ✅ [#237][]| `concat` (seq) | `concat` (seq) | | |
| ✅ [#70][] | `contains` | `contains` | | |
| ✅ [#82][] | `delay` | `delay` | | |
| | `distinct` | `distinct` | | |
| | `distinctBy` | `dictinctBy` | `distinctByAsync` | |
| ✅ [#209][]| | `drop` | | |
| ✅ [#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](#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.") |
| ✅ [#68][] | `findIndex` | `findIndex` | `findIndexAsync` | |
| 🚫 | `findIndexBack` | n/a | n/a | [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.") |
| ✅ [#2][] | `fold` | `fold` | `foldAsync` | |
| | `fold2` | `fold2` | `fold2Async` | |
| 🚫 | `foldBack` | | | [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.") |
| 🚫 | `foldBack2` | | | [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.") |
| ✅ [#240][]| `forall` | `forall` | `forallAsync` | |
| | `forall2` | `forall2` | `forall2Async` | |
| ❓ | `groupBy` | `groupBy` | `groupByAsync` | [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.") |
| ✅ [#23][] | `head` | `head` | | |
| ✅ [#68][] | `indexed` | `indexed` | | |
| ✅ [#69][] | `init` | `init` | `initAsync` | |
| ✅ [#69][] | `initInfinite` | `initInfinite` | `initInfiniteAsync` | |
| ✅ [#236][]| `insertAt` | `insertAt` | | |
| ✅ [#236][]| `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](#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.") |
| ✅ [#2][] | `mapi` | `mapi` | `mapiAsync` | |
| | `mapi2` | `mapi2` | `mapi2Async` | |
| ✅ [#221][]| `max` | `max` | | |
| ✅ [#221][]| `maxBy` | `maxBy` | `maxByAsync` | |
| ✅ [#221][]| `min` | `min` | | |
| ✅ [#221][]| `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](#note3 "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](#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.") |
| ✅ [#236][]| `removeAt` | `removeAt` | | |
| ✅ [#236][]| `removeManyAt` | `removeManyAt` | | |
| | `replicate` | `replicate` | | |
| ❓ | `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.") |
| ✅ [#90][] | `singleton` | `singleton` | | |
| ✅ [#209][]| `skip` | `skip` | | |
| ✅ [#219][]| `skipWhile` | `skipWhile` | `skipWhileAsync` | |
| ✅ [#219][]| | `skipWhileInclusive` | `skipWhileInclusiveAsync` | |
| ❓ | `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.") |
| ❓ | `sortBy` | | | [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.") |
| ❓ | `sortByAscending` | | | [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.") |
| ❓ | `sortByDescending` | | | [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.") |
| ❓ | `sortWith` | | | [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.") |
| | `splitInto` | `splitInto` | | |
| | `sum` | `sum` | | |
| | `sumBy` | `sumBy` | `sumByAsync` | |
| ✅ [#76][] | `tail` | `tail` | | |
| ✅ [#209][]| `take` | `take` | | |
| ✅ [#126][]| `takeWhile` | `takeWhile` | `takeWhileAsync` | |
| ✅ [#126][]| | `takeWhileInclusive` | `takeWhileInclusiveAsync` | |
| ✅ [#2][] | `toArray` | `toArray` | `toArrayAsync` | |
| ✅ [#2][] | | `toIList` | `toIListAsync` | |
| ✅ [#2][] | `toList` | `toList` | `toListAsync` | |
| ✅ [#2][] | | `toResizeArray` | `toResizeArrayAsync` | |
| ✅ [#2][] | | `toSeq` | `toSeqAsync` | |
| | | […] | | |
| ❓ | `transpose` | | | [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.") |
| ✅ [#209][]| `truncate` | `truncate` | | |
| ✅ [#23][] | `tryExactlyOne` | `tryExactlyOne` | `tryExactlyOneAsync` | |
| ✅ [#23][] | `tryFind` | `tryFind` | `tryFindAsync` | |
| 🚫 | `tryFindBack` | | | [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.") |
| ✅ [#68][] | `tryFindIndex` | `tryFindIndex` | `tryFindIndexAsync` | |
| 🚫 | `tryFindIndexBack` | | | [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.") |
| ✅ [#23][] | `tryHead` | `tryHead` | | |
| ✅ [#23][] | `tryItem` | `tryItem` | | |
| ✅ [#23][] | `tryLast` | `tryLast` | | |
| ✅ [#23][] | `tryPick` | `tryPick` | `tryPickAsync` | |
| ✅ [#76][] | | `tryTail` | | |
| | `unfold` | `unfold` | `unfoldAsync` | |
| ✅ [#236][]| `updateAt` | `updateAt` | | |
| ✅ [#217][]| `where` | `where` | `whereAsync` | |
| | `windowed` | `windowed` | | |
| ✅ [#2][] | `zip` | `zip` | | |
| | `zip3` | `zip3` | | |
| | | `zip4` | | |

¹⁾ _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._
²⁾ _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._
³⁾ _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

### Further reading on `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.

### 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]
- 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.

## Building & testing

TLDR: just run `build`. Or load the `sln` file in Visual Studio or VS Code and compile.

### Prerequisites

At the very least, to get the source to compile, you'll need:

- .NET 6 or .NET 7 Preview
- F# 6.0 or 7.0 compiler
- To use `build.cmd`, the `dotnet` command must be accessible from your path.

Just check-out this repo locally. Then, from the root of the repo, you can do:

### Build the solution

```bash
build [build] [release|debug]
```

With no arguments, defaults to `release`.

### Run the tests

```bash
build test [release|debug]
```

With no arguments, defaults to `release`. By default, all tests are output to the console. If you don't want that, you can use `--logger console;verbosity=summary`.
Furthermore, no TRX file is generated and the `--blame-xxx` flags aren't set.

### Run the CI command

```bash
build ci [release|debug]
```

With no arguments, defaults to `release`. This will run `dotnet test` with the `--blame-xxx` settings enabled to [prevent hanging tests][1] caused by
an [xUnit runner bug][2].

There are no special CI environment variables that need to be set for running this locally.

### Advanced

You can pass any additional options that are valid for `dotnet test` and `dotnet build` respectively. However,
these cannot be the very first argument, so you should either use `build build --myadditionalOptions fizz buzz`, or
just specify the build-kind, i.e. this is fine:

```bash
build debug --verbosity detailed
build test --logger console;verbosity=summary
```

At this moment, additional options cannot have quotes in them.

Command modifiers, like `release` and `debug`, can be specified with `-` or `/` if you so prefer: `dotnet build /release`.

### Get help

```bash
build help
```

For more info, see this PR: .

## Work in progress

The `taskSeq` CE using the statically compilable _resumable state machine_ approach is based on, and draw heavily from [Don Symes `taskSeq.fs`][20] as used to test the resumable state machine in the F# core compiler.

On top of that, this library adds a set of `TaskSeq` module functions, with their `Async` variants, on par with `Seq` and `AsyncSeq`.

## Current set of `TaskSeq` utility functions

The following are the current surface area of the `TaskSeq` utility functions, ordered alphabetically.

```f#
module TaskSeq =
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>
val chooseAsync: chooser: ('T -> #Task<'U option>) -> source: TaskSeq<'T> -> TaskSeq<'U>
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 concat: sources: TaskSeq<'T seq> -> TaskSeq<'T>
val concat: sources: TaskSeq<'T list> -> TaskSeq<'T>
val concat: sources: TaskSeq<'T array> -> TaskSeq<'T>
val concat: sources: TaskSeq> -> TaskSeq<'T>
val contains<'T when 'T: equality> : value: 'T -> source: TaskSeq<'T> -> Task
val delay: generator: (unit -> TaskSeq<'T>) -> TaskSeq<'T>
val drop: count: int -> source: 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>
val filterAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> TaskSeq<'T>
val find: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task<'T>
val findAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task<'T>
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 forall: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task
val forallAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task
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 insertAt: position:int -> value:'T -> source: TaskSeq<'T> -> TaskSeq<'T>
val insertManyAt: position:int -> values:TaskSeq<'T> -> source: TaskSeq<'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 max: source: TaskSeq<'T> -> Task<'T> when 'T: comparison
val max: source: TaskSeq<'T> -> Task<'T> when 'T: comparison
val maxBy: projection: ('T -> 'U) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison
val minBy: projection: ('T -> 'U) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison
val maxByAsync: projection: ('T -> #Task<'U>) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison
val minByAsync: projection: ('T -> #Task<'U>) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison 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 removeAt: position:int -> source: TaskSeq<'T> -> TaskSeq<'T>
val removeManyAt: position:int -> count:int -> source: TaskSeq<'T> -> TaskSeq<'T>
val singleton: source: 'T -> TaskSeq<'T>
val skip: count: int -> source: TaskSeq<'T> -> TaskSeq<'T>
val tail: source: TaskSeq<'T> -> Task>
val take: count: int -> source: TaskSeq<'T> -> TaskSeq<'T>
val takeWhile: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task>
val takeWhileAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task>
val takeWhileInclusive: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task>
val takeWhileInclusiveAsync: predicate: ('T -> #Task) -> 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 truncate: count: int -> source: TaskSeq<'T> -> TaskSeq<'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 where: predicate: ('T -> bool) -> source: TaskSeq<'T> -> TaskSeq<'T>
val whereAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> TaskSeq<'T>
val unbox<'U when 'U: struct> : source: TaskSeq -> TaskSeq<'U>
val updateAt: position:int -> value:'T -> source: TaskSeq<'T> -> TaskSeq<'T>
val zip: source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'T * 'U>
```

[buildstatus]: https://github.com/fsprojects/FSharp.Control.TaskSeq/actions/workflows/main.yaml
[buildstatus_img]: https://github.com/fsprojects/FSharp.Control.TaskSeq/actions/workflows/main.yaml/badge.svg
[teststatus]: https://github.com/fsprojects/FSharp.Control.TaskSeq/actions/workflows/test.yaml
[teststatus_img]: https://github.com/fsprojects/FSharp.Control.TaskSeq/actions/workflows/test.yaml/badge.svg

[1]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/25
[2]: https://github.com/xunit/xunit/issues/2587
[3]: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1?view=net-6.0
[4]: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerator-1.movenextasync?view=net-6.0
[5]: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerator-1.current?view=net-6.0
[6]: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1.getasyncenumerator?view=net-6.0
[7]: https://learn.microsoft.com/en-us/dotnet/api/system.iasyncdisposable?view=net-6.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/
[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
[22]: https://fsprojects.github.io/FSharp.Control.AsyncSeq/reference/fsharp-control-asyncseq.html#toAsyncEnum
[23]: https://fsprojects.github.io/FSharp.Control.AsyncSeq/reference/fsharp-control-asyncseq.html#fromAsyncEnum
[24]: https://github.com/TheAngryByrd/IcedTasks

[#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
[#90]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/90
[#126]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/126
[#133]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/133
[#167]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/167
[#209]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/209
[#217]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/217
[#219]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/219
[#221]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/221
[#237]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/237
[#236]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/236
[#240]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/240

[issues]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues
[nuget]: https://www.nuget.org/packages/FSharp.Control.TaskSeq/