From a2e1c8f6ebf20261e99c9ab89f1d76eda9852c6c Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Wed, 9 Nov 2022 03:01:20 +0100 Subject: [PATCH 01/15] Create initial package settings for TaskSeq --- Directory.Build.props | 3 +++ Version.props | 5 +++++ .../FSharp.Control.TaskSeq.fsproj | 19 +++++++++++++++--- taskseq-icon.png | Bin 0 -> 1392 bytes 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 Directory.Build.props create mode 100644 Version.props create mode 100644 taskseq-icon.png 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/Version.props b/Version.props new file mode 100644 index 00000000..24649dfe --- /dev/null +++ b/Version.props @@ -0,0 +1,5 @@ + + + 0.1 + + \ No newline at end of file diff --git a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj index ddba9897..a62f4d78 100644 --- a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj +++ b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj @@ -1,9 +1,18 @@ - + net6.0 true TaskSeq.ico + True + Computation expression `taskSeq` for processing IAsyncEnumerable sequences + $(Version) + Abel Braaksma; Don Syme + The `taskSeq` computation expression combines the power of `seq` and `task` of F# 6.0 to work with IAsyncEnumerable sequences seamlessly. +Build on top of the success of `task` using statically compiled resumable state machines under the hood. + Copyright 2022 Abel Braaksma + https://github.com/fsprojects/FSharp.Control.TaskSeq + taskseq-icon.png @@ -22,8 +31,12 @@ + + - + + True + \ + - diff --git a/taskseq-icon.png b/taskseq-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b14a77c1c87e440956611f86d968cc1cd61e328b GIT binary patch literal 1392 zcmeAS@N?(olHy`uVBq!ia0vp^(?OVn4MT zkC+BGU+8@YwqFeA;uf5{;X7-SNCNx(pkvcgJIlj=ta#7Nbj0%r$8q_0eku$7LPN8n z<}9i8RQ7cCWcF-dWLBd2((6*pr9C>ITqie|lpPgS&ph8CPsl?6>%{7lQRXBBxi zE--h!X;RFItDhp~?{s@qr?@j)#gqN3ybRDsF_)S?@@`tV{=+2ctyubkHQMH#dk1FaZdDfo({!u7m zt&XvF(B)qfm&|)Qdq$Dmt?4`cEjC*mQMhz=^ZVZKQK=K-i?wd;X5U3JEw}b~tllj;E7rkLV zNvr)pT@%xeumk@le7U2zLH_2G=GA;}!rKp6)pW6lA2`5jkaeG7@tn;0g)^fqloJ|Hv#$wGXylG~ zz$EroQxD4G`6eU2;Q*@+7dt`f)=uVxw_oP1K5+i~BArj?qil-}lQ-%xbssv&s#EZ@ zsgwsuzDft$6Rw}~Usdq{)AhSDHQc-#4m@R*01*+YFWG^zrW?F26@Pxu8FQ|Ikvk?1 ziTKOB=3B{!hDR6mg?$xp0Z$ntL=Ab-qW3XiyE_K)L%ASzPL+m(k`*}@h-r$ zDBAd|0Wurgulyfe59{*!SiKs|2Pqnoawn zJ6~z#wfB?QW+&v{DPDWC`RkeGjmF2y)~Bz|xy>%T?O3Suwd-fUZty8P|2x1U_SLh@ z=`Z#4Z%JO)%}~B;!(}~X_HmOJA9Joee|9~m)H?n8nu5E=tG`;BKYaEna!);D`-9at U<+bfPfkh~Tr>mdKI;Vst0P_rdCjbBd literal 0 HcmV?d00001 From dad26d1a511a1e4e013490531d43612f651c0b53 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Wed, 9 Nov 2022 03:44:40 +0100 Subject: [PATCH 02/15] Update package definition license --- src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj index a62f4d78..a640a885 100644 --- a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj +++ b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj @@ -13,6 +13,9 @@ Build on top of the success of `task` using statically compiled resumable state Copyright 2022 Abel Braaksma https://github.com/fsprojects/FSharp.Control.TaskSeq taskseq-icon.png + ..\..\packages + MIT + False From 1e9f58b12eb02f5a2d7d844f9163ce22d08fb808 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Wed, 9 Nov 2022 04:11:59 +0100 Subject: [PATCH 03/15] Baseline for 0.2 --- Version.props | 2 +- .../FSharp.Control.TaskSeq.fsproj | 23 +++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Version.props b/Version.props index 24649dfe..94eb20c3 100644 --- a/Version.props +++ b/Version.props @@ -1,5 +1,5 @@ - 0.1 + 0.2 \ No newline at end of file diff --git a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj index a640a885..0f004dce 100644 --- a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj +++ b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj @@ -1,21 +1,33 @@  - net6.0 + netstandard2.1 true TaskSeq.ico True - Computation expression `taskSeq` for processing IAsyncEnumerable sequences + Computation expression 'taskSeq' for processing IAsyncEnumerable sequences and module functions $(Version) Abel Braaksma; Don Syme - The `taskSeq` computation expression combines the power of `seq` and `task` of F# 6.0 to work with IAsyncEnumerable sequences seamlessly. -Build on top of the success of `task` using statically compiled resumable state machines under the hood. - Copyright 2022 Abel Braaksma + Provides the 'taskSeq' computation expression to support performance and statially optimized async sequences using the new F# 6.0 resumable state machines, similar to 'task'. + +This library brings C#'s concept of 'await foreach' to F#. + Copyright 2022 https://github.com/fsprojects/FSharp.Control.TaskSeq + https://github.com/fsprojects/FSharp.Control.TaskSeq taskseq-icon.png ..\..\packages MIT False + README.md + + v.0.2 + - moved from NET 6.0, to NetStandard 2.1 for greater compatibility, no functional changes + - updated readme with progress overview, corrected meta info, added release notes + v.0.1.1 + - updated meta info in nuget package and added readme + v.0.1 + - initial release + @@ -41,5 +53,6 @@ Build on top of the success of `task` using statically compiled resumable state True \ + From 7e24a0b76f09d62ed7e27f9c3da1af1b6b55e0fc Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Wed, 9 Nov 2022 13:49:11 +0100 Subject: [PATCH 04/15] Add and update package-specific readme --- assets/nuget-package-readme.md | 226 ++++++++++++++++++ .../FSharp.Control.TaskSeq.fsproj | 11 +- 2 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 assets/nuget-package-readme.md diff --git a/assets/nuget-package-readme.md b/assets/nuget-package-readme.md new file mode 100644 index 00000000..e36edac4 --- /dev/null +++ b/assets/nuget-package-readme.md @@ -0,0 +1,226 @@ +# TaskSeq + +An implementation of [`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. + +----------------------------------------- + +## Table of contents + + + +- [Implementation progress](#implementation-progress) + - [`taskSeq` CE](#taskseq-ce) + - [`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) + +----------------------------------------- + +## Implementation progress + +As of 6 November 2022: + +### `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! + +### `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](#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` | | | +| ✅ [#70][] | `contains` | `contains` | | | +| ✅ [#82][] | `delay` | `delay` | | | +| | `distinct` | `distinct` | | | +| | `distinctBy` | `dictinctBy` | `distinctByAsync` | | +| ✅ [#2][] | `empty` | `empty` | | | +| ✅ [#23][] | `exactlyOne` | `exactlyOne` | | | +| | `except` | `except` | | | +| ✅ [#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.") | +| | `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` | | +| | `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](#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` | | +| | `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](#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.") | +| | `removeAt` | `removeAt` | | | +| | `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.") | +| | `singleton` | `singleton` | | | +| | `skip` | `skip` | | | +| | `skipWhile` | `skipWhile` | `skipWhileAsync` | | +| ❓ | `sort` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | +| ❓ | `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` | | | +| | `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](#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.") | +| | `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` | | +| | `updateAt` | `updateAt` | | | +| | `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 + +### 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 + diff --git a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj index 0f004dce..0dc18ec8 100644 --- a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj +++ b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj @@ -18,10 +18,11 @@ This library brings C#'s concept of 'await foreach' to F#. ..\..\packages MIT False - README.md + nuget-package-readme.md v.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 v.0.1.1 - updated meta info in nuget package and added readme @@ -53,6 +54,12 @@ This library brings C#'s concept of 'await foreach' to F#. True \ - + + + + + + + From bc2ea591911bf4e3b54a13649f552fbe4cbf4683 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Wed, 9 Nov 2022 23:10:57 +0100 Subject: [PATCH 05/15] Fix FSharp.Core deps and FsToolkit deps conflicts. Update application icon. --- .../TaskSeq.ico | Bin .../FSharp.Control.TaskSeq.Test.fsproj | 14 ++++----- .../FSharp.Control.TaskSeq.fsproj | 28 +++++++----------- src/FSharp.Control.TaskSeq/TaskSeq.ico | Bin 179661 -> 0 bytes 4 files changed, 17 insertions(+), 25 deletions(-) rename {src/FSharp.Control.TaskSeq.Test => assets}/TaskSeq.ico (100%) delete mode 100644 src/FSharp.Control.TaskSeq/TaskSeq.ico 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/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/FSharp.Control.TaskSeq.fsproj b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj index 0dc18ec8..71d448a4 100644 --- a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj +++ b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj @@ -3,7 +3,7 @@ netstandard2.1 true - TaskSeq.ico + ..\..\assets\TaskSeq.ico True Computation expression 'taskSeq' for processing IAsyncEnumerable sequences and module functions $(Version) @@ -31,15 +31,13 @@ This library brings C#'s concept of 'await foreach' to F#. - - - - - - - - + + + True + \ + + @@ -47,19 +45,13 @@ This library brings C#'s concept of 'await foreach' to F#. + - - - True - \ - - - - - + + diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.ico b/src/FSharp.Control.TaskSeq/TaskSeq.ico deleted file mode 100644 index 65d3ee0aff6faf1af94180d52627c31065b1b8f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 179661 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 Tehbe?;TiYBGoO>!GM4@SruWSf From cf5f55f505d8b22d1532451ee8232a2a76aff369 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Wed, 9 Nov 2022 23:11:10 +0100 Subject: [PATCH 06/15] Implement ValueTask.CompletedTask, it is not available in NetStandard 2.1 --- src/FSharp.Control.TaskSeq/Utils.fs | 9 +++++++++ 1 file changed, 9 insertions(+) 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 } From cfbfed5a3b2603ff84f58b37e3ebb6a9a459169c Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Thu, 10 Nov 2022 04:16:20 +0100 Subject: [PATCH 07/15] Fix limited markdown support on nuget site for Nuget's readme --- assets/nuget-package-readme.md | 128 ++++++++++++++++++++++++--------- 1 file changed, 94 insertions(+), 34 deletions(-) diff --git a/assets/nuget-package-readme.md b/assets/nuget-package-readme.md index e36edac4..dd3a5aa6 100644 --- a/assets/nuget-package-readme.md +++ b/assets/nuget-package-readme.md @@ -1,8 +1,6 @@ # TaskSeq -An implementation of [`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. ----------------------------------------- @@ -15,8 +13,9 @@ 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 --> -- [Implementation progress](#implementation-progress) - - [`taskSeq` CE](#taskseq-ce) +- [Overview](#overview) + - [`taskSeq` computation expressions](#taskseq-computation-expressions) + - [Examples](#examples) - [`TaskSeq` module functions](#taskseq-module-functions) - [More information](#more-information) - [Futher reading `IAsyncEnumerable`](#futher-reading-iasyncenumerable) @@ -25,13 +24,66 @@ The `IAsyncEnumerable` interface was added to .NET in `.NET Core 3.0` and is par ----------------------------------------- -## Implementation progress +## 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`. + +As with `seq` and `Seq`, this library comes with a bunch of well-known collection functions, like `TaskSeq.empty/isEmpty` or `TaskSeq.map/iter/collect` and `TaskSeq.find/pick/choose/filter`. Where applicable, these come with async variants, like `TaskSeq.mapAsync/iterAsync/collectAsync` and `TaskSeq.findAsync/pickAsync/chooseAsync/filterAsync`, which allows the apply function to be asynchronous. + +See below for a full list of currently implemented functions. + +### `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 -As of 6 November 2022: +// singleton is fine +let hello = taskSeq { yield "Hello, World!"" } -### `taskSeq` CE +// can be mixed with normal sequences +let oneToTen = taskSeq { yield! [1..10] } -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! +// 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 @@ -41,13 +93,13 @@ The following is the progress report: | 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.") | +| ❓ | `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](#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.") | +| ❓ | `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` | | @@ -63,21 +115,22 @@ The following is the progress report: | | `distinctBy` | `dictinctBy` | `distinctByAsync` | | | ✅ [#2][] | `empty` | `empty` | | | | ✅ [#23][] | `exactlyOne` | `exactlyOne` | | | -| | `except` | `except` | | | +| ✅ [#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.") | +| 🚫 | `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](#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.") | +| 🚫 | `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](#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.") | +| 🚫 | `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](#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.") | +| ❓ | `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` | | @@ -97,7 +150,7 @@ The following is the progress report: | | `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.") | +| 🚫 | `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` | | | @@ -118,23 +171,23 @@ The following is the progress report: | | `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.") | +| 🚫 | `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](#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.") | +| 🚫 | `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](#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.") | +| ❓ | `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](#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.") | +| 🚫 | `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](#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.") | +| ❓ | `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` | | @@ -147,13 +200,13 @@ The following is the progress report: | ✅ [#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.") | +| ❓ | `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](#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.") | +| 🚫 | `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](#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.") | +| 🚫 | `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` | | | @@ -167,10 +220,17 @@ The following is the progress report: | | `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._ -¹⁾ _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._ +#### 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 @@ -223,4 +283,4 @@ The following is the progress report: [#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 From 6ec0d0781e1253be5672476c20fc69a32e788e0a Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Thu, 10 Nov 2022 04:17:00 +0100 Subject: [PATCH 08/15] Move taskseq-icon and update solution folder items in sln file --- taskseq-icon.png => assets/taskseq-icon.png | Bin src/FSharp.Control.TaskSeq.sln | 11 +++++++++++ 2 files changed, 11 insertions(+) rename taskseq-icon.png => assets/taskseq-icon.png (100%) diff --git a/taskseq-icon.png b/assets/taskseq-icon.png similarity index 100% rename from taskseq-icon.png rename to assets/taskseq-icon.png diff --git a/src/FSharp.Control.TaskSeq.sln b/src/FSharp.Control.TaskSeq.sln index 2df77210..ab1a7527 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}" @@ -22,6 +25,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ 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 +52,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} From 25cae331a64bca8d1784673af0a7fd78a208c77e Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Thu, 10 Nov 2022 04:18:04 +0100 Subject: [PATCH 09/15] Rename `TaskSeq.toSeqCached` -> `toSeq` and remove `TaskSeq.toSeqCachedAsync`, which simply didn't make much sense --- .../TaskSeq.Append.Tests.fs | 4 +- .../TaskSeq.Cast.Tests.fs | 4 +- .../TaskSeq.Collect.Tests.fs | 4 +- .../TaskSeq.Concat.Tests.fs | 4 +- .../TaskSeq.Delay.Tests.fs | 4 +- .../TaskSeq.Empty.Tests.fs | 2 +- .../TaskSeq.Map.Tests.fs | 8 ++-- .../TaskSeq.ToXXX.Tests.fs | 31 ++------------ src/FSharp.Control.TaskSeq.Test/TestUtils.fs | 4 +- .../FSharp.Control.TaskSeq.fsproj | 41 ++++++++++++++----- src/FSharp.Control.TaskSeq/TaskSeq.fs | 4 +- src/FSharp.Control.TaskSeq/TaskSeq.fsi | 16 +++++--- 12 files changed, 63 insertions(+), 63 deletions(-) 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/FSharp.Control.TaskSeq.fsproj b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj index 71d448a4..b0b89c51 100644 --- a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj +++ b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj @@ -8,9 +8,11 @@ Computation expression 'taskSeq' for processing IAsyncEnumerable sequences and module functions $(Version) Abel Braaksma; Don Syme - Provides the 'taskSeq' computation expression to support performance and statially optimized async sequences using the new F# 6.0 resumable state machines, similar to 'task'. + This library brings C#'s concept of 'await foreach' to F#. -This library brings C#'s concept of 'await foreach' to F#. +The 'taskSeq' computation expression adds support for awaitable asyncronous sequences with a similar ease of use and performance as F#'s 'task' CE. TaskSeq brings 'seq' and 'task' together in a safe way. + +Generates optimized IL code and comes with a comprehensive set of module functions. See README for more info. Copyright 2022 https://github.com/fsprojects/FSharp.Control.TaskSeq https://github.com/fsprojects/FSharp.Control.TaskSeq @@ -20,20 +22,39 @@ This library brings C#'s concept of 'await foreach' to F#. False nuget-package-readme.md - v.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 - v.0.1.1 - - updated meta info in nuget package and added readme - v.0.1 + 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 + - + True \ 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> From 42f7f618137ec5a4f150656a29042887d5c79af7 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Thu, 10 Nov 2022 04:18:25 +0100 Subject: [PATCH 10/15] Release version 0.2.2 --- Version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Version.props b/Version.props index 94eb20c3..7f8337f7 100644 --- a/Version.props +++ b/Version.props @@ -1,5 +1,5 @@ - 0.2 + 0.2.2 \ No newline at end of file From 4ec511883f72e8a2cc8cb3ad2eb4af79e457f732 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 13 Nov 2022 01:31:57 +0100 Subject: [PATCH 11/15] Update nuget description --- src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj index b0b89c51..4287c5e9 100644 --- a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj +++ b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj @@ -1,4 +1,4 @@ - + netstandard2.1 @@ -8,11 +8,11 @@ Computation expression 'taskSeq' for processing IAsyncEnumerable sequences and module functions $(Version) Abel Braaksma; Don Syme - This library brings C#'s concept of 'await foreach' to F#. + This library brings C#'s concept of 'await foreach' to F#, with a seamless implementation of IAsyncEnumerable<'T>. -The 'taskSeq' computation expression adds support for awaitable asyncronous sequences with a similar ease of use and performance as F#'s 'task' CE. TaskSeq brings 'seq' and 'task' together in a safe way. +The 'taskSeq' computation expression adds support for awaitable asyncronous sequences with a similar ease of use and performance as F#'s 'task' CE, with minimal overhead through ValueTask under the hood. TaskSeq brings 'seq' and 'task' together in a safe way. -Generates optimized IL code and comes with a comprehensive set of module functions. See README for more info. +Generates optimized IL code through the new resumable state machines, and comes with a comprehensive set of helpful functions in module 'TaskSeq'. See README for documentation and more info. Copyright 2022 https://github.com/fsprojects/FSharp.Control.TaskSeq https://github.com/fsprojects/FSharp.Control.TaskSeq From 5446badd1851d5e03e9f2fe2801b270e9e0abaed Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 13 Nov 2022 01:59:55 +0100 Subject: [PATCH 12/15] Update readmes again --- README.md | 145 +++++++++++++++++++++++++++++---- assets/nuget-package-readme.md | 15 ++-- 2 files changed, 138 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 7da1e2fe..35054f13 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 -Not necessarily in order of importance: +Dotnet Nuget + +```cmd +dotnet add package FSharp.Control.TaskSeq +``` + +For a specific project: + +```cmd +dotnet add myproject.fsproj package FSharp.Control.TaskSeq +``` + +F# Interactive (FSI): + +```f# +// latest version +> #r "nuget: FSharp.Control.TaskSeq" + +// or with specific version +> #r "nuget: FSharp.Control.TaskSeq, 0.2.2" +``` + +Paket: + +```cmd +dotnet paket add FSharp.Control.TaskSeq --project +``` + +Package Manager: + +```cmd +PM> NuGet\Install-Package FSharp.Control.TaskSeq +``` + +As package reference in `fsproj` or `csproj` file: + +```xml + + +``` + +### Examples + +```f# +open System.IO + +open FSharp.Control + +// singleton is fine +let 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. @@ -600,6 +711,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 +726,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/assets/nuget-package-readme.md b/assets/nuget-package-readme.md index dd3a5aa6..6fa010bf 100644 --- a/assets/nuget-package-readme.md +++ b/assets/nuget-package-readme.md @@ -14,6 +14,7 @@ An implementation of [`IAsyncEnumerable<'T>`][3] as a computation expression: `t --> - [Overview](#overview) + - [Module functions](#module-functions) - [`taskSeq` computation expressions](#taskseq-computation-expressions) - [Examples](#examples) - [`TaskSeq` module functions](#taskseq-module-functions) @@ -26,19 +27,21 @@ An implementation of [`IAsyncEnumerable<'T>`][3] as a computation expression: `t ## Overview -The `IAsyncEnumerable` interface was added to .NET in `.NET Core 3.0` and is part of `.NET Standard 2.1`. The main use-case was for iterative asynchronous enumeration over some resource. For instance, an event stream or a REST API interface with pagination, asynchronous reading over a list of files and accumulating the results, where each action can be modeled as a [`MoveNextAsync`][4] call on the [`IAsyncEnumerator<'T>`][5] given by a call to [`GetAsyncEnumerator()`][6]. +The `IAsyncEnumerable` interface was added to .NET in `.NET Core 3.0` and is part of `.NET Standard 2.1`. The main use-case was for iterative asynchronous enumeration over some resource. For instance, an event stream or a REST API interface with pagination, asynchronous reading over a list of files and accumulating the results, where each action can be modeled as a [`MoveNextAsync`][4] call on the [`IAsyncEnumerator<'T>`][5] given by a call to [`GetAsyncEnumerator()`][6]. Since the introduction of `task` in F# the call for a native implementation of _task sequences_ has grown, in particular because proper iterating over an `IAsyncEnumerable` has proven challenging, especially if one wants to avoid mutable variables. This library is an answer to that call and implements the same _resumable state machine_ approach with `taskSeq`. -As with `seq` and `Seq`, this library comes with a bunch of well-known collection functions, like `TaskSeq.empty/isEmpty` or `TaskSeq.map/iter/collect` and `TaskSeq.find/pick/choose/filter`. Where applicable, these come with async variants, like `TaskSeq.mapAsync/iterAsync/collectAsync` and `TaskSeq.findAsync/pickAsync/chooseAsync/filterAsync`, which allows the apply function to be asynchronous. +### Module functions -See below for a full list of currently implemented functions. +As with `seq` and `Seq`, this library comes with a bunch of well-known collection functions, like `TaskSeq.empty`, `isEmpty` or `TaskSeq.map`, `iter`, `collect`, `fold` and `TaskSeq.find`, `pick`, `choose`, `filter`. Where applicable, these come with async variants, like `TaskSeq.mapAsync` `iterAsync`, `collectAsync`, `foldAsync` and `TaskSeq.findAsync`, `pickAsync`, `chooseAsync`, `filterAsync`, which allows the applied function to be asynchronous. + +[See below](#taskseq-module-functions) for a full list of currently implemented functions and their variants. ### `taskSeq` computation expressions -The `taskSeq` computation expression can be used just like using `seq`. On top of that, it adds support for working with tasks through `let!` and +The `taskSeq` computation expression can be used just like using `seq`. On top of that, it adds support for working with tasks through `let!` and looping over a normal or asynchronous sequence (one that implements `IAsyncEnumerable<'T>'`). You can use `yield!` and `yield` and there's support -for `use` and `use!`, `try-with` and `try-finally` and `while` loops within the task sequence expression: +for `use` and `use!`, `try-with` and `try-finally` and `while` loops within the task sequence expression. ### Examples @@ -48,7 +51,7 @@ open System.IO open FSharp.Control // singleton is fine -let hello = taskSeq { yield "Hello, World!"" } +let hello = taskSeq { yield "Hello, World!" } // can be mixed with normal sequences let oneToTen = taskSeq { yield! [1..10] } From 91292a79b863f77aeaf9a8b58dc0aea6e6afd289 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 13 Nov 2022 02:46:15 +0100 Subject: [PATCH 13/15] Update current set of implemented functions of the TaskSeq surface area --- README.md | 350 ++++++++++++------------------------------------------ 1 file changed, 78 insertions(+), 272 deletions(-) diff --git a/README.md b/README.md index 35054f13..126493b2 100644 --- a/README.md +++ b/README.md @@ -394,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 @@ -405,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 From b50cbaab770db2cec40c42c0419314c4a4b2b0cc Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 13 Nov 2022 02:59:20 +0100 Subject: [PATCH 14/15] Update package tags, and include symbols --- src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj index 4287c5e9..d0298601 100644 --- a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj +++ b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj @@ -8,7 +8,7 @@ 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>. + 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. @@ -50,6 +50,9 @@ Generates optimized IL code through the new resumable state machines, and comes - adds zip/length + taskseq'fsharp;f#;computation expression;IAsyncEnumerable;task;async;asyncseq; + True + snupkg From 08e6f55ef7561f59e429da360d5fbda5521ac50c Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 13 Nov 2022 13:37:05 +0100 Subject: [PATCH 15/15] Add publish-to-nuget action on 'main', only when version changes --- .github/workflows/build.yaml | 20 -------------------- .github/workflows/main.yaml | 20 -------------------- .github/workflows/publish.yaml | 30 ++++++++++++++++++++++++++++++ .gitignore | 1 + src/FSharp.Control.TaskSeq.sln | 1 + 5 files changed, 32 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/publish.yaml 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/src/FSharp.Control.TaskSeq.sln b/src/FSharp.Control.TaskSeq.sln index ab1a7527..2e1e32f6 100644 --- a/src/FSharp.Control.TaskSeq.sln +++ b/src/FSharp.Control.TaskSeq.sln @@ -20,6 +20,7 @@ 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