Skip to content

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

License

Notifications You must be signed in to change notification settings

fsprojects/FSharp.Control.TaskSeq

Repository files navigation

build test Nuget

TaskSeq

An implementation of IAsyncEnumerable<'T> 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.

Release notes

See Releases for the an extensive version history of TaskSeq. See Status overview below for a progress report.


Table of contents


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 call on the IAsyncEnumerator<'T> given by a call to GetAsyncEnumerator().

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 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 Tasks 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

dotnet add package FSharp.Control.TaskSeq

For a specific project:

dotnet add myproject.fsproj package FSharp.Control.TaskSeq

F# Interactive (FSI):

// latest version
> #r "nuget: FSharp.Control.TaskSeq"

// or with specific version
> #r "nuget: FSharp.Control.TaskSeq, 0.4.0"

Paket:

dotnet paket add FSharp.Control.TaskSeq --project <project>

Package Manager:

PM> NuGet\Install-Package FSharp.Control.TaskSeq

As package reference in fsproj or csproj file:

<!-- replace version with most recent version -->
<PackageReference Include="FSharp.Control.TaskSeq" Version="0.4.0" />

Examples

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<string>
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 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 ValueTasks 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> note #1 Its own type, also called IAsyncEnumerable<'T>note #1
Implementation State machine (statically compiled) No state machine, continuation style
Semantics seq-like: on-demand seq-like: on-demand
Disposability Asynchronous, through IAsyncDisposable Synchronous, through IDisposable
Support let! All task-like: Async<'T>, Task<'T>, ValueTask<'T> or any GetAwaiter() Async<'T> only
Support do! Async<unit>, Task<unit> and Task, ValueTask<unit> and ValueTask Async<unit> only
Support yield! IAsyncEnumerable<'T> (= TaskSeq), AsyncSeq, any sequence AsyncSeq
Support for IAsyncEnumerable<'T> (= 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
Conversion from other Implicit (yield!) or TaskSeq.ofAsyncSeq AsyncSeq.ofAsyncEnum
Recursion in yield! No (requires F# support, upcoming) Yes
Iteration semantics Two operations, 'Next' is a value task, 'Current' must be called separately One operation, 'Next' is Async, returns option with 'Current'
MoveNextAsync Returns ValueTask<bool> Returns Async<'T option>
Current Returns 'T 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:

  • Take existing taskSeq resumable code from F# and fix it. DONE
  • 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
    • min / max / minBy / maxBy & async variant (see #221)
    • insertAt / updateAt and related (see #236)
    • average / averageBy, sum and related
    • forall / forallAsync (see #240)
    • 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
  • Publish package on Nuget, DONE, PUBLISHED SINCE: 7 November 2022. See https://www.nuget.org/packages/FSharp.Control.TaskSeq
  • Make TaskSeq interoperable with Task by expanding the latter with a for .. in .. do that acceps task sequences
  • 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
  • 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 https://fsprojects.github.io

Implementation progress

Nuget

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!.

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
#81 append append
#81 appendSeq
#81 prependSeq
average average
averageBy averageBy averageByAsync
cache cache note #1
#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
#68 findIndex findIndex findIndexAsync
🚫 findIndexBack n/a n/a note #2
#2 fold fold foldAsync
fold2 fold2 fold2Async
🚫 foldBack note #2
🚫 foldBack2 note #2
#240 forall forall forallAsync
forall2 forall2 forall2Async
groupBy groupBy groupByAsync note #1
#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
#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
reduce reduce reduceAsync
🚫 reduceBack note #2
#236 removeAt removeAt
#236 removeManyAt removeManyAt
replicate replicate
rev note #1
scan scan scanAsync
🚫 scanBack note #2
#90 singleton singleton
#209 skip skip
#219 skipWhile skipWhile skipWhileAsync
#219 skipWhileInclusive skipWhileInclusiveAsync
sort note #1
sortBy note #1
sortByAscending note #1
sortByDescending note #1
sortWith note #1
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
#209 truncate truncate
#23 tryExactlyOne tryExactlyOne tryExactlyOneAsync
#23 tryFind tryFind tryFindAsync
🚫 tryFindBack note #2
#68 tryFindIndex tryFindIndex tryFindIndexAsync
🚫 tryFindIndexBack note #2
#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.
  • An MSDN article written shortly after it was introduced.
  • Converting a seq to an IAsyncEnumerable demo gist as an example, though TaskSeq contains many more utility functions and uses a slightly different approach.

Further reading on resumable state machines

Further reading on computation expressions

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

build [build] [release|debug]

With no arguments, defaults to release.

Run the tests

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

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 caused by an xUnit runner bug.

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:

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

build help

For more info, see this PR: #29.

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

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<obj>
    val cast: source: TaskSeq<obj> -> 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<ResizeArray<'T>> -> TaskSeq<'T>
    val contains<'T when 'T: equality> : value: 'T -> source: TaskSeq<'T> -> Task<bool>
    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<bool>
    val existsAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> Task<bool>
    val filter: predicate: ('T -> bool) -> source: TaskSeq<'T> -> TaskSeq<'T>
    val filterAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> TaskSeq<'T>
    val find: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task<'T>
    val findAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> Task<'T>
    val findIndex: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task<int>
    val findIndexAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> Task<int>
    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<bool>
    val forallAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> Task<bool>
    val head: source: TaskSeq<'T> -> Task<'T>
    val indexed: source: TaskSeq<'T> -> TaskSeq<int * 'T>
    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<bool>
    val item: index: int -> source: TaskSeq<'T> -> Task<'T>
    val iter: action: ('T -> unit) -> source: TaskSeq<'T> -> Task<unit>
    val iterAsync: action: ('T -> #Task<unit>) -> source: TaskSeq<'T> -> Task<unit>
    val iteri: action: (int -> 'T -> unit) -> source: TaskSeq<'T> -> Task<unit>
    val iteriAsync: action: (int -> 'T -> #Task<unit>) -> source: TaskSeq<'T> -> Task<unit>
    val last: source: TaskSeq<'T> -> Task<'T>
    val length: source: TaskSeq<'T> -> Task<int>
    val lengthBy: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task<int>
    val lengthByAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> Task<int>
    val lengthOrMax: max: int -> source: TaskSeq<'T> -> Task<int>
    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<Async<'T>> -> 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<TaskSeq<'T>>
    val take: count: int -> source: TaskSeq<'T> -> TaskSeq<'T>
    val takeWhile: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task<TaskSeq<'T>>
    val takeWhileAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> Task<TaskSeq<'T>>
    val takeWhileInclusive: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task<TaskSeq<'T>>
    val takeWhileInclusiveAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> Task<TaskSeq<'T>>
    val toArray: source: TaskSeq<'T> -> 'T[]
    val toArrayAsync: source: TaskSeq<'T> -> Task<'T[]>
    val toIListAsync: source: TaskSeq<'T> -> Task<IList<'T>>
    val toList: source: TaskSeq<'T> -> 'T list
    val toListAsync: source: TaskSeq<'T> -> Task<'T list>
    val toResizeArrayAsync: source: TaskSeq<'T> -> Task<ResizeArray<'T>>
    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<bool>) -> source: TaskSeq<'T> -> Task<'T option>
    val tryFindIndex: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task<int option>
    val tryFindIndexAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> Task<int option>
    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<TaskSeq<'T> option>
    val where: predicate: ('T -> bool) -> source: TaskSeq<'T> -> TaskSeq<'T>
    val whereAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> TaskSeq<'T>
    val unbox<'U when 'U: struct> : source: TaskSeq<obj> -> TaskSeq<'U>
    val updateAt: position:int -> value:'T -> source: TaskSeq<'T> -> TaskSeq<'T>
    val zip: source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'T * 'U>