diff --git a/README.md b/README.md
index c932efa2..a102b443 100644
--- a/README.md
+++ b/README.md
@@ -64,130 +64,134 @@ We are working hard on getting a full set of module functions on `TaskSeq` that
The following is the progress report:
-| Done | `Seq` | `TaskSeq` | `async` variant | 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.") |
-| | `append` | `append` | | |
-| | `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.") |
-| ✅ | `cast` | `cast` | | |
-| ✅ | | `box` | | |
-| ✅ | | `unbox` | | |
-| ✅ | `choose` | `choose` | `chooseAsync` | |
-| | `chunkBySize` | `chunkBySize` | | |
-| ✅ | `collect` | `collect` | `collectAsync` | |
-| | `compareWith` | `compareWith` | `compareWithAsync` | |
-| ✅ | `concat` | `concat` | | |
-| ✅ | `contains` | `contains` | | |
-| | `delay` | `delay` | | |
-| | `distinct` | `distinct` | | |
-| | `distinctBy` | `dictinctBy` | `distinctByAsync` | |
-| ✅ | `empty` | `empty` | | |
-| ✅ | `exactlyOne` | `exactlyOne` | | |
-| | `except` | `except` | | |
-| ✅ | `exists` | `exists` | | |
-| | `exists2` | `exists2` | | |
-| ✅ | `filter` | `filter` | `filterAsync` | |
-| ✅ | `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.") |
-| ✅ | `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.") |
-| ✅ | `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.") |
-| ✅ | `head` | `head` | | |
-| ✅ | `indexed` | `indexed` | | |
-| ✅ | `init` | `init` | `initAsync` | |
-| ✅ | `initInfinite` | `initInfinite` | `initInfiniteAsync` | |
-| | `insertAt` | `insertAt` | | |
-| | `insertManyAt` | `insertManyAt` | | |
-| ✅ | `isEmpty` | `isEmpty` | | |
-| ✅ | `item` | `item` | | |
-| ✅ | `iter` | `iter` | `iterAsync` | |
-| | `iter2` | `iter2` | `iter2Async` | |
-| ✅ | `iteri` | `iteri` | `iteriAsync` | |
-| | `iteri2` | `iteri2` | `iteri2Async` | |
-| ✅ | `last` | `last` | | |
-| ✅ | `length` | `length` | | |
-| ✅ | | `lengthBy` | `lengthByAsyn` | |
-| ✅ | `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.") |
-| ✅ | `mapi` | `mapi` | `mapiAsync` | |
-| | `mapi2` | `mapi2` | `mapi2Async` | |
-| | `max` | `max` | | |
-| | `maxBy` | `maxBy` | `maxByAsync` | |
-| | `min` | `min` | | |
-| | `minBy` | `minBy` | `minByAsync` | |
-| ✅ | `ofArray` | `ofArray` | | |
-| ✅ | | `ofAsyncArray` | | |
-| ✅ | | `ofAsyncList` | | |
-| ✅ | | `ofAsyncSeq` | | |
-| ✅ | `ofList` | `ofList` | | |
-| ✅ | | `ofTaskList` | | |
-| ✅ | | `ofResizeArray` | | |
-| ✅ | | `ofSeq` | | |
-| ✅ | | `ofTaskArray` | | |
-| ✅ | | `ofTaskList` | | |
-| ✅ | | `ofTaskSeq` | | |
-| | `pairwise` | `pairwise` | | |
-| | `permute` | `permute` | `permuteAsync` | |
-| ✅ | `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` | |
-| ✅ | `tail` | `tail` | | |
-| | `take` | `take` | | |
-| | `takeWhile` | `takeWhile` | `takeWhileAsync` | |
-| ✅ | `toArray` | `toArray` | `toArrayAsync` | |
-| ✅ | | `toIList` | `toIListAsync` | |
-| ✅ | `toList` | `toList` | `toListAsync` | |
-| ✅ | | `toResizeArray` | `toResizeArrayAsync` | |
-| ✅ | | `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` | | |
-| ✅ | `tryExactlyOne` | `tryExactlyOne` | `tryExactlyOneAsync` | |
-| ✅ | `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.") |
-| ✅ | `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.") |
-| ✅ | `tryHead` | `tryHead` | | |
-| ✅ | `tryItem` | `tryItem` | | |
-| ✅ | `tryLast` | `tryLast` | | |
-| ✅ | `tryPick` | `tryPick` | `tryPickAsync` | |
-| ✅ | | `tryTail` | | |
-| | `unfold` | `unfold` | `unfoldAsync` | |
-| | `updateAt` | `updateAt` | | |
-| | `where` | `where` | `whereAsync` | |
-| | `windowed` | `windowed` | | |
-| ✅ | `zip` | `zip` | | |
-| | `zip3` | `zip3` | | |
-| | | `zip4` |
+| 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` | | |
+| | `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._
@@ -595,3 +599,15 @@ 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
+
+[#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
+
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 e18b3a0e..4ab3edd3 100644
--- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
+++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
@@ -12,6 +12,7 @@
+
diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Append.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Append.Tests.fs
new file mode 100644
index 00000000..0448acc7
--- /dev/null
+++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Append.Tests.fs
@@ -0,0 +1,115 @@
+module TaskSeq.Tests.Append
+
+open System
+
+open Xunit
+open FsUnit.Xunit
+open FsToolkit.ErrorHandling
+
+open FSharp.Control
+open System.Collections.Generic
+
+//
+// TaskSeq.append
+// TaskSeq.appendSeq
+// TaskSeq.prependSeq
+//
+
+let validateSequence ts =
+ ts
+ |> TaskSeq.toSeqCachedAsync
+ |> Task.map (Seq.map string)
+ |> Task.map (String.concat "")
+ |> Task.map (should equal "1234567891012345678910")
+
+module EmptySeq =
+ [)>]
+ let ``TaskSeq-append both args empty`` variant =
+ Gen.getEmptyVariant variant
+ |> TaskSeq.append (Gen.getEmptyVariant variant)
+ |> verifyEmpty
+
+ [)>]
+ let ``TaskSeq-appendSeq both args empty`` variant =
+ Seq.empty
+ |> TaskSeq.appendSeq (Gen.getEmptyVariant variant)
+ |> verifyEmpty
+
+ [)>]
+ let ``TaskSeq-prependSeq both args empty`` variant =
+ Gen.getEmptyVariant variant
+ |> TaskSeq.prependSeq Seq.empty
+ |> verifyEmpty
+
+module Immutable =
+ [)>]
+ let ``TaskSeq-append`` variant =
+ Gen.getSeqImmutable variant
+ |> TaskSeq.append (Gen.getSeqImmutable variant)
+ |> validateSequence
+
+ [)>]
+ let ``TaskSeq-appendSeq with a list`` variant =
+ [ 1..10 ]
+ |> TaskSeq.appendSeq (Gen.getSeqImmutable variant)
+ |> validateSequence
+
+ [)>]
+ let ``TaskSeq-appendSeq with an array`` variant =
+ [| 1..10 |]
+ |> TaskSeq.appendSeq (Gen.getSeqImmutable variant)
+ |> validateSequence
+
+ [)>]
+ let ``TaskSeq-prependSeq with a list`` variant =
+ Gen.getSeqImmutable variant
+ |> TaskSeq.prependSeq [ 1..10 ]
+ |> validateSequence
+
+ [)>]
+ let ``TaskSeq-prependSeq with an array`` variant =
+ Gen.getSeqImmutable variant
+ |> TaskSeq.prependSeq [| 1..10 |]
+ |> validateSequence
+
+module SideEffects =
+ [)>]
+ let ``TaskSeq-append consumes whole sequence once incl after-effects`` variant =
+ let mutable i = 0
+
+ taskSeq {
+ i <- i + 1
+ yield! [ 1..10 ]
+ i <- i + 1
+ }
+ |> TaskSeq.append (Gen.getSeqImmutable variant)
+ |> validateSequence
+ |> Task.map (fun () -> i |> should equal 2)
+
+ [)>]
+ let ``TaskSeq-appendSeq consumes whole sequence once incl after-effects`` variant =
+ let mutable i = 0
+
+ let ts = taskSeq {
+ i <- i + 1
+ yield! [ 1..10 ]
+ i <- i + 1
+ }
+
+ [| 1..10 |]
+ |> TaskSeq.appendSeq ts
+ |> validateSequence
+ |> Task.map (fun () -> i |> should equal 2)
+
+ [)>]
+ let ``TaskSeq-prependSeq consumes whole sequence once incl after-effects`` variant =
+ let mutable i = 0
+
+ taskSeq {
+ i <- i + 1
+ yield! [ 1..10 ]
+ i <- i + 1
+ }
+ |> TaskSeq.prependSeq [ 1..10 ]
+ |> validateSequence
+ |> Task.map (fun () -> i |> should equal 2)
diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Concat.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Concat.Tests.fs
index 2b50a81d..df4c07e8 100644
--- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Concat.Tests.fs
+++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Concat.Tests.fs
@@ -41,11 +41,69 @@ module EmptySeq =
module Immutable =
[)>]
- let ``TaskSeq-concat with empty sequences`` variant =
+ let ``TaskSeq-concat with three sequences of sequences`` variant =
+ taskSeq {
+ yield Gen.getSeqImmutable variant // not yield-bang!
+ yield Gen.getSeqImmutable variant
+ yield Gen.getSeqImmutable variant
+ }
+ |> TaskSeq.concat
+ |> validateSequence
+
+ [)>]
+ let ``TaskSeq-concat with three sequences of sequences and few empties`` variant =
+ taskSeq {
+ yield TaskSeq.empty
+ yield Gen.getSeqImmutable variant // not yield-bang!
+ yield TaskSeq.empty
+ yield TaskSeq.empty
+ yield Gen.getSeqImmutable variant
+ yield TaskSeq.empty
+ yield Gen.getSeqImmutable variant
+ yield TaskSeq.empty
+ yield TaskSeq.empty
+ yield TaskSeq.empty
+ yield TaskSeq.empty
+ }
+ |> TaskSeq.concat
+ |> validateSequence
+
+module SideEffect =
+ [)>]
+ let ``TaskSeq-concat consumes until the end, including side-effects`` variant =
+ let mutable i = 0
+
+ taskSeq {
+ yield Gen.getSeqImmutable variant // not yield-bang!
+ yield Gen.getSeqImmutable variant
+
+ yield taskSeq {
+ yield! [ 1..10 ]
+ i <- i + 1
+ }
+ }
+ |> TaskSeq.concat
+ |> validateSequence
+ |> Task.map (fun () -> i |> should equal 1)
+
+ [)>]
+ let ``TaskSeq-concat consumes side effects in empty sequences`` variant =
+ let mutable i = 0
+
taskSeq {
+ yield taskSeq { do i <- i + 1 }
yield Gen.getSeqImmutable variant // not yield-bang!
+ yield TaskSeq.empty
+ yield taskSeq { do i <- i + 1 }
yield Gen.getSeqImmutable variant
+ yield TaskSeq.empty
yield Gen.getSeqImmutable variant
+ yield TaskSeq.empty
+ yield TaskSeq.empty
+ yield TaskSeq.empty
+ yield TaskSeq.empty
+ yield taskSeq { do i <- i + 1 }
}
|> TaskSeq.concat
|> validateSequence
+ |> Task.map (fun () -> i |> should equal 3)
diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs
index f66d7e94..a9ac3bd9 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeq.fs
+++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs
@@ -169,6 +169,21 @@ module TaskSeq =
yield! (ts :> taskSeq<'T>)
}
+ let append (source1: #taskSeq<'T>) (source2: #taskSeq<'T>) = taskSeq {
+ yield! (source1 :> IAsyncEnumerable<'T>)
+ yield! (source2 :> IAsyncEnumerable<'T>)
+ }
+
+ let appendSeq (source1: #taskSeq<'T>) (source2: #seq<'T>) = taskSeq {
+ yield! (source1 :> IAsyncEnumerable<'T>)
+ yield! (source2 :> seq<'T>)
+ }
+
+ let prependSeq (source1: #seq<'T>) (source2: #taskSeq<'T>) = taskSeq {
+ yield! (source1 :> seq<'T>)
+ yield! (source2 :> IAsyncEnumerable<'T>)
+ }
+
//
// iter/map/collect functions
//
@@ -222,6 +237,7 @@ module TaskSeq =
| Some result -> return result
| None -> return Internal.raiseEmptySeq ()
}
+
let tryItem index source = Internal.tryItem index source
let item index source = task {
diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi
index fce10970..8787e63e 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi
+++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi
@@ -107,6 +107,39 @@ module TaskSeq =
/// Thrown when the input sequence is null.
val concat: sources: taskSeq<#taskSeq<'T>> -> taskSeq<'T>
+ ///
+ /// Concatenates task sequences and in order as a single
+ /// task sequence.
+ ///
+ ///
+ /// The first input task sequence.
+ /// The second input task sequence.
+ /// The resulting task sequence.
+ /// Thrown when either of the input sequences is null.
+ val append: source1: #taskSeq<'T> -> source2: #taskSeq<'T> -> taskSeq<'T>
+
+ ///
+ /// Concatenates a task sequence with a non-async F# in
+ /// and returns a single task sequence.
+ ///
+ ///
+ /// The input task sequence.
+ /// The input F# sequence.
+ /// The resulting task sequence.
+ /// Thrown when either of the input sequences is null.
+ val appendSeq: source1: #taskSeq<'T> -> source2: #seq<'T> -> taskSeq<'T>
+
+ ///
+ /// Concatenates a non-async F# in with a task sequence in
+ /// and returns a single task sequence.
+ ///
+ ///
+ /// The input F# sequence.
+ /// The input task sequence.
+ /// The resulting task sequence.
+ /// Thrown when either of the input sequences is null.
+ val prependSeq: source1: #seq<'T> -> source2: #taskSeq<'T> -> taskSeq<'T>
+
/// Returns taskSeq as an array. This function is blocking until the sequence is exhausted and will properly dispose of the resources.
val toList: source: taskSeq<'T> -> 'T list