From 370679a7ce6e080ab3383a9a6b66533fd549b0ae Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sun, 11 Dec 2022 10:06:29 +0000 Subject: [PATCH 01/14] Add takeWhile, takeWhileInclusive --- src/FSharp.Control.TaskSeq/TaskSeq.fs | 4 ++ src/FSharp.Control.TaskSeq/TaskSeq.fsi | 30 +++++++++++ src/FSharp.Control.TaskSeq/TaskSeqInternal.fs | 51 +++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index 05f5313c..a315d177 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -253,6 +253,10 @@ module TaskSeq = let chooseAsync chooser source = Internal.choose (TryPickAsync chooser) source let filter predicate source = Internal.filter (Predicate predicate) source let filterAsync predicate source = Internal.filter (PredicateAsync predicate) source + let takeWhile predicate source = Internal.takeWhile (Predicate predicate) source + let takeWhileAsync predicate source = Internal.takeWhile (PredicateAsync predicate) source + let takeWhileInclusive predicate source = Internal.takeWhileInclusive (Predicate predicate) source + let takeWhileInclusiveAsync predicate source = Internal.takeWhileInclusive (PredicateAsync predicate) source let tryPick chooser source = Internal.tryPick (TryPick chooser) source let tryPickAsync chooser source = Internal.tryPick (TryPickAsync chooser) source let tryFind predicate source = Internal.tryFind (Predicate predicate) source diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index 1f0f1497..0a4cfc59 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -365,6 +365,36 @@ module TaskSeq = /// val filter: predicate: ('T -> bool) -> source: taskSeq<'T> -> taskSeq<'T> + /// + /// Yields items from the source while the function returns . + /// The first result concludes consumption of the source. + /// If is asynchronous, consider using . + /// + val takeWhile: predicate: ('T -> bool) -> source: taskSeq<'T> -> taskSeq<'T> + + /// + /// Yields items from the source while the asynchronous function returns . + /// The first result concludes consumption of the source. + /// If does not need to be asynchronous, consider using . + /// + val takeWhileAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> taskSeq<'T> + + /// + /// Yields items from the source while the function returns . + /// The first result concludes consumption of the source, but is included in the result. + /// If is asynchronous, consider using . + /// If the final item is not desired, consider using . + /// + val takeWhileInclusive: predicate: ('T -> bool) -> source: taskSeq<'T> -> taskSeq<'T> + + /// + /// Yields items from the source while the asynchronous function returns . + /// The first result concludes consumption of the source, but is included in the result. + /// If does not need to be asynchronous, consider using . + /// If the final item is not desired, consider using . + /// + val takeWhileInclusiveAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> taskSeq<'T> + /// /// Returns a new collection containing only the elements of the collection /// for which the given asynchronous function returns . diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index 8f7446ef..62f2884a 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -531,6 +531,57 @@ module internal TaskSeqInternal = | true -> yield item | false -> () } + + let takeWhile predicate (source: taskSeq<_>) = taskSeq { + use e = source.GetAsyncEnumerator(CancellationToken()) + let! step = e.MoveNextAsync() + let mutable go = step + + match predicate with + | Predicate predicate -> + while go do + let value = e.Current + if predicate value then + yield value + let! more = e.MoveNextAsync() + go <- more + else go <- false + | PredicateAsync predicate -> + while go do + let value = e.Current + match! predicate value with + | true -> + yield value + let! more = e.MoveNextAsync() + go <- more + | false -> go <- false + } + + let takeWhileInclusive predicate (source: taskSeq<_>) = taskSeq { + use e = source.GetAsyncEnumerator(CancellationToken()) + let! step = e.MoveNextAsync() + let mutable go = step + + match predicate with + | Predicate predicate -> + while go do + let value = e.Current + yield value + if predicate value then + let! more = e.MoveNextAsync() + go <- more + else go <- false + | PredicateAsync predicate -> + while go do + let value = e.Current + yield value + match! predicate value with + | true -> + let! more = e.MoveNextAsync() + go <- more + | false -> go <- false + } + // Consider turning using an F# version of this instead? // https://github.com/i3arnon/ConcurrentHashSet type ConcurrentHashSet<'T when 'T: equality>(ct) = From 197318b3677709324fc47d5a9c7a26cd3e6aa602 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sun, 11 Dec 2022 10:25:16 +0000 Subject: [PATCH 02/14] README updates --- README.md | 262 +++++++++++++++++++++++++++--------------------------- 1 file changed, 133 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 0c55fab8..9afdd1c3 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Since the introduction of `task` in F# the call for a native implementation of _ ### 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. +As with `seq` and `Seq`, this library comes with a bunch of well-known collection functions, like `TaskSeq.empty`, `isEmpty` or `TaskSeq.map`, `iter`, `collect`, `fold` and `TaskSeq.find`, `pick`, `choose`, `filter`, `takeWhile`. Where applicable, these come with async variants, like `TaskSeq.mapAsync` `iterAsync`, `collectAsync`, `foldAsync` and `TaskSeq.findAsync`, `pickAsync`, `chooseAsync`, `filterAsync`, `takeWhileAsync` which allows the applied function to be asynchronous. [See below](#current-set-of-taskseq-utility-functions) for a full list of currently implemented functions and their variants. @@ -187,134 +187,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` | 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` | | | -| ✅ [#83][] | `except` | `except` | | | -| ✅ [#83][] | | `exceptOfSeq` | | | -| ✅ [#70][] | `exists` | `exists` | `existsAsync` | | -| | `exists2` | `exists2` | | | -| ✅ [#23][] | `filter` | `filter` | `filterAsync` | | -| ✅ [#23][] | `find` | `find` | `findAsync` | | -| 🚫 | `findBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | -| ✅ [#68][] | `findIndex` | `findIndex` | `findIndexAsync` | | -| 🚫 | `findIndexBack` | n/a | n/a | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | -| ✅ [#2][] | `fold` | `fold` | `foldAsync` | | -| | `fold2` | `fold2` | `fold2Async` | | -| 🚫 | `foldBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | -| 🚫 | `foldBack2` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | -| | `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.") | -| ✅ [#90][] | `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` | | | +| 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` | | | +| ✅ [#83][] | `except` | `except` | | | +| ✅ [#83][] | | `exceptOfSeq` | | | +| ✅ [#70][] | `exists` | `exists` | `existsAsync` | | +| | `exists2` | `exists2` | | | +| ✅ [#23][] | `filter` | `filter` | `filterAsync` | | +| ✅ [#23][] | `find` | `find` | `findAsync` | | +| 🚫 | `findBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | +| ✅ [#68][] | `findIndex` | `findIndex` | `findIndexAsync` | | +| 🚫 | `findIndexBack` | n/a | n/a | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | +| ✅ [#2][] | `fold` | `fold` | `foldAsync` | | +| | `fold2` | `fold2` | `fold2Async` | | +| 🚫 | `foldBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | +| 🚫 | `foldBack2` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | +| | `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.") | +| ✅ [#90][] | `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` | | | +| ✅ [#126][] | `takeWhile` | `takeWhile` | `takeWhileAsync`, `takeWhileInclusive`, `takeWhileInclusiveAsync` | | +| ✅ [#2][] | `toArray` | `toArray` | `toArrayAsync` | | +| ✅ [#2][] | | `toIList` | `toIListAsync` | | +| ✅ [#2][] | `toList` | `toList` | `toListAsync` | | +| ✅ [#2][] | | `toResizeArray` | `toResizeArrayAsync` | | +| ✅ [#2][] | | `toSeq` | `toSeqAsync` | | +| | | […] | | | +| ❓ | `transpose` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | +| | `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._ @@ -441,6 +441,10 @@ module TaskSeq = val existsAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task val filter: predicate: ('T -> bool) -> source: taskSeq<'T> -> taskSeq<'T> val filterAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> taskSeq<'T> + val takeWhile: predicate: ('T -> bool) -> source: taskSeq<'T> -> taskSeq<'T> + val takeWhileAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> taskSeq<'T> + val takeWhileInclusive: predicate: ('T -> bool) -> source: taskSeq<'T> -> taskSeq<'T> + val takeWhileInclusiveAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> taskSeq<'T> val find: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task<'T> val findAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task<'T> val findIndex: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task From 0d343739505cd9d61f0051af3b3098eb66ae9c1a Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sun, 11 Dec 2022 10:55:15 +0000 Subject: [PATCH 03/14] Add inital tests --- .../FSharp.Control.TaskSeq.Test.fsproj | 4 +- .../TaskSeq.TakeWhile.Tests.fs | 66 +++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs 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 611663b8..edfaa2d8 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -2,9 +2,6 @@ net6.0 - - false - false @@ -23,6 +20,7 @@ + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs new file mode 100644 index 00000000..8ca2a387 --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs @@ -0,0 +1,66 @@ +module TaskSeq.Tests.TakeWhile + +open System +open Xunit +open FsUnit.Xunit +open FsToolkit.ErrorHandling + +open FSharp.Control + +// +// TaskSeq.takeWhile +// TaskSeq.takeWhileAsync +// + +module EmptySeq = + [)>] + let ``TaskSeq-takeWhile has no effect`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.takeWhile ((=) 12) + |> TaskSeq.toListAsync + |> Task.map (List.isEmpty >> should be True) + + [)>] + let ``TaskSeq-takeWhileAsync has no effect`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.takeWhileAsync (fun x -> task { return x = 12 }) + |> TaskSeq.toListAsync + |> Task.map (List.isEmpty >> should be True) + +module Immutable = + [)>] + let ``TaskSeq-takeWhile filters correctly`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.takeWhile (fun x -> x <= 5) + |> TaskSeq.map char + |> TaskSeq.map ((+) '@') + |> TaskSeq.toArrayAsync + |> Task.map (String >> should equal "ABCDE") + + [)>] + let ``TaskSeq-takeWhileAsync filters correctly`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.takeWhileAsync (fun x -> task { return x <= 5 }) + |> TaskSeq.map char + |> TaskSeq.map ((+) '@') + |> TaskSeq.toArrayAsync + |> Task.map (String >> should equal "ABCDE") + +module SideEffects = + [)>] + let ``TaskSeq-takeWhile filters correctly`` variant = + Gen.getSeqWithSideEffect variant + |> TaskSeq.takeWhile (fun x -> x <= 5) + |> TaskSeq.map char + |> TaskSeq.map ((+) '@') + |> TaskSeq.toArrayAsync + |> Task.map (String >> should equal "ABCDE") + + [)>] + let ``TaskSeq-takeWhileAsync filters correctly`` variant = + Gen.getSeqWithSideEffect variant + |> TaskSeq.takeWhileAsync (fun x -> task { return x <= 5 }) + |> TaskSeq.map char + |> TaskSeq.map ((+) '@') + |> TaskSeq.toArrayAsync + |> Task.map (String >> should equal "ABCDE") From 4763eaa239f4912bd8985d5cc78586d12664dcc0 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sun, 11 Dec 2022 11:04:30 +0000 Subject: [PATCH 04/14] Cover termination implicitly --- .../TaskSeq.TakeWhile.Tests.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs index 8ca2a387..dab9b748 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs @@ -31,7 +31,7 @@ module Immutable = [)>] let ``TaskSeq-takeWhile filters correctly`` variant = Gen.getSeqImmutable variant - |> TaskSeq.takeWhile (fun x -> x <= 5) + |> TaskSeq.takeWhile (fun x -> x <> 6) |> TaskSeq.map char |> TaskSeq.map ((+) '@') |> TaskSeq.toArrayAsync @@ -40,7 +40,7 @@ module Immutable = [)>] let ``TaskSeq-takeWhileAsync filters correctly`` variant = Gen.getSeqImmutable variant - |> TaskSeq.takeWhileAsync (fun x -> task { return x <= 5 }) + |> TaskSeq.takeWhileAsync (fun x -> task { return x <> 6 }) |> TaskSeq.map char |> TaskSeq.map ((+) '@') |> TaskSeq.toArrayAsync @@ -50,7 +50,7 @@ module SideEffects = [)>] let ``TaskSeq-takeWhile filters correctly`` variant = Gen.getSeqWithSideEffect variant - |> TaskSeq.takeWhile (fun x -> x <= 5) + |> TaskSeq.takeWhile (fun x -> x <> 6) |> TaskSeq.map char |> TaskSeq.map ((+) '@') |> TaskSeq.toArrayAsync @@ -59,7 +59,7 @@ module SideEffects = [)>] let ``TaskSeq-takeWhileAsync filters correctly`` variant = Gen.getSeqWithSideEffect variant - |> TaskSeq.takeWhileAsync (fun x -> task { return x <= 5 }) + |> TaskSeq.takeWhileAsync (fun x -> task { return x <> 6 }) |> TaskSeq.map char |> TaskSeq.map ((+) '@') |> TaskSeq.toArrayAsync From ff9411517fbdf8b257cc5be7a1a63223f3bb64d9 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sun, 11 Dec 2022 11:28:50 +0000 Subject: [PATCH 05/14] Cover termination --- .../TaskSeq.TakeWhile.Tests.fs | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs index dab9b748..b14c39fd 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs @@ -27,11 +27,41 @@ module EmptySeq = |> TaskSeq.toListAsync |> Task.map (List.isEmpty >> should be True) +module Terminates = + [] + let ``TaskSeq-takeWhile stops after predicate fails`` () = + seq { 1; 2; 3; failwith "Too far" } + |> TaskSeq.ofSeq + |> TaskSeq.takeWhile (fun x -> x <= 2) + |> TaskSeq.map char + |> TaskSeq.map ((+) '@') + |> TaskSeq.toArrayAsync + |> Task.map (String >> should equal "AB") + + [] + let ``TaskSeq-takeWhileAsync stops after predicate fails`` () = + taskSeq { 1; 2; 3; failwith "Too far" } + |> TaskSeq.takeWhileAsync (fun x -> task { return x <= 2 }) + |> TaskSeq.map char + |> TaskSeq.map ((+) '@') + |> TaskSeq.toArrayAsync + |> Task.map (String >> should equal "AB") + +// This is the base condition as one would expect in actual code +let inline cond x = x <> 6 + +// For each of the tests below, we add a guard that will trigger if the predicate is passed items known to be beyond the +// first failing item in the known sequence (which is 1..10) +let inline condWithGuard x = + let res = cond x + if x > 6 then failwith "Test sequence should not be enumerated beyond the first item failing the predicate" + res + module Immutable = [)>] let ``TaskSeq-takeWhile filters correctly`` variant = Gen.getSeqImmutable variant - |> TaskSeq.takeWhile (fun x -> x <> 6) + |> TaskSeq.takeWhile condWithGuard |> TaskSeq.map char |> TaskSeq.map ((+) '@') |> TaskSeq.toArrayAsync @@ -40,7 +70,7 @@ module Immutable = [)>] let ``TaskSeq-takeWhileAsync filters correctly`` variant = Gen.getSeqImmutable variant - |> TaskSeq.takeWhileAsync (fun x -> task { return x <> 6 }) + |> TaskSeq.takeWhileAsync (fun x -> task { return condWithGuard x }) |> TaskSeq.map char |> TaskSeq.map ((+) '@') |> TaskSeq.toArrayAsync @@ -50,7 +80,7 @@ module SideEffects = [)>] let ``TaskSeq-takeWhile filters correctly`` variant = Gen.getSeqWithSideEffect variant - |> TaskSeq.takeWhile (fun x -> x <> 6) + |> TaskSeq.takeWhile condWithGuard |> TaskSeq.map char |> TaskSeq.map ((+) '@') |> TaskSeq.toArrayAsync @@ -59,7 +89,7 @@ module SideEffects = [)>] let ``TaskSeq-takeWhileAsync filters correctly`` variant = Gen.getSeqWithSideEffect variant - |> TaskSeq.takeWhileAsync (fun x -> task { return x <> 6 }) + |> TaskSeq.takeWhileAsync (fun x -> task { return condWithGuard x }) |> TaskSeq.map char |> TaskSeq.map ((+) '@') |> TaskSeq.toArrayAsync From d1e80058d48307da6d03038d0ae0823d6e234968 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sun, 11 Dec 2022 11:41:46 +0000 Subject: [PATCH 06/14] Add more complete example-based tests --- .../TaskSeq.TakeWhile.Tests.fs | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs index b14c39fd..5543d392 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs @@ -27,9 +27,31 @@ module EmptySeq = |> TaskSeq.toListAsync |> Task.map (List.isEmpty >> should be True) -module Terminates = +// The primary requirement is that items after the item failing the predicate must be excluded +module TakeWhileExcludesEverythingAfterFail = [] - let ``TaskSeq-takeWhile stops after predicate fails`` () = + let ``TaskSeq-takeWhile excludes all items after predicate fails`` () = + seq { 1; 2; 2; 3; 2; 1 } + |> TaskSeq.ofSeq + |> TaskSeq.takeWhile (fun x -> x <= 2) + |> TaskSeq.map char + |> TaskSeq.map ((+) '@') + |> TaskSeq.toArrayAsync + |> Task.map (String >> should equal "AB") + + [] + let ``TaskSeq-takeWhileAsync excludes all items after after predicate fails`` () = + taskSeq { 1; 2; 2; 3; 2; 1 } + |> TaskSeq.takeWhileAsync (fun x -> task { return x <= 2 }) + |> TaskSeq.map char + |> TaskSeq.map ((+) '@') + |> TaskSeq.toArrayAsync + |> Task.map (String >> should equal "AB") + +// Covers the fact that it's not sufficient to merely exclude successor items - it's also critical that the enumeration terminates +module TakeWhileTerminatesOnFail = + [] + let ``TaskSeq-takeWhile stops consuming after predicate fails`` () = seq { 1; 2; 3; failwith "Too far" } |> TaskSeq.ofSeq |> TaskSeq.takeWhile (fun x -> x <= 2) @@ -39,7 +61,7 @@ module Terminates = |> Task.map (String >> should equal "AB") [] - let ``TaskSeq-takeWhileAsync stops after predicate fails`` () = + let ``TaskSeq-takeWhileAsync stops consuming after predicate fails`` () = taskSeq { 1; 2; 3; failwith "Too far" } |> TaskSeq.takeWhileAsync (fun x -> task { return x <= 2 }) |> TaskSeq.map char From 50b6a54c0224e5e2860d5a4936678ca9cb5d7b7c Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Sun, 11 Dec 2022 11:57:13 +0000 Subject: [PATCH 07/14] Cover inclusive variants --- .../TaskSeq.TakeWhile.Tests.fs | 61 ++++++++++++------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs index 5543d392..7e8e133e 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs @@ -10,6 +10,8 @@ open FSharp.Control // // TaskSeq.takeWhile // TaskSeq.takeWhileAsync +// TaskSeq.takeWhileInclusive +// TaskSeq.takeWhileInclusiveAsync // module EmptySeq = @@ -28,46 +30,61 @@ module EmptySeq = |> Task.map (List.isEmpty >> should be True) // The primary requirement is that items after the item failing the predicate must be excluded -module TakeWhileExcludesEverythingAfterFail = - [] - let ``TaskSeq-takeWhile excludes all items after predicate fails`` () = - seq { 1; 2; 2; 3; 2; 1 } +module FiltersAfterFail = + [] + let ``TaskSeq-takeWhile(Inclusive)? excludes all items after predicate fails`` inclusive = + // The only real difference in semantics between the base and the *Inclusive variant lies in whether the final item is returned + // NOTE the semantics are very clear on only propagating a single failing item in the inclusive case + let f, expected = + if inclusive then TaskSeq.takeWhileInclusive, "ABBC" + else TaskSeq.takeWhile, "ABB" + seq { 1; 2; 2; 3; 3; 2; 1 } |> TaskSeq.ofSeq - |> TaskSeq.takeWhile (fun x -> x <= 2) + |> f (fun x -> x <= 2) |> TaskSeq.map char |> TaskSeq.map ((+) '@') |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal "AB") + |> Task.map (String >> should equal expected) - [] - let ``TaskSeq-takeWhileAsync excludes all items after after predicate fails`` () = - taskSeq { 1; 2; 2; 3; 2; 1 } - |> TaskSeq.takeWhileAsync (fun x -> task { return x <= 2 }) + // Same as preceding test, just with Async functions + [] + let ``TaskSeq-takeWhile(Inclusive)?Async excludes all items after after predicate fails`` inclusive = + let f, expected = + if inclusive then TaskSeq.takeWhileInclusiveAsync, "ABBC" + else TaskSeq.takeWhileAsync, "ABB" + taskSeq { 1; 2; 2; 3; 3; 2; 1 } + |> f (fun x -> task { return x <= 2 }) |> TaskSeq.map char |> TaskSeq.map ((+) '@') |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal "AB") + |> Task.map (String >> should equal expected) // Covers the fact that it's not sufficient to merely exclude successor items - it's also critical that the enumeration terminates -module TakeWhileTerminatesOnFail = - [] - let ``TaskSeq-takeWhile stops consuming after predicate fails`` () = - seq { 1; 2; 3; failwith "Too far" } +module StopsEnumeratingAfterFail = + [] + let ``TaskSeq-takeWhile(Inclusive)? stops consuming after predicate fails`` inclusive = + let f, expected = + if inclusive then TaskSeq.takeWhileInclusive, "ABBC" + else TaskSeq.takeWhile, "ABB" + seq { 1; 2; 2; 3; 3; failwith "Too far" } |> TaskSeq.ofSeq - |> TaskSeq.takeWhile (fun x -> x <= 2) + |> f (fun x -> x <= 2) |> TaskSeq.map char |> TaskSeq.map ((+) '@') |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal "AB") + |> Task.map (String >> should equal expected) - [] - let ``TaskSeq-takeWhileAsync stops consuming after predicate fails`` () = - taskSeq { 1; 2; 3; failwith "Too far" } - |> TaskSeq.takeWhileAsync (fun x -> task { return x <= 2 }) + [] + let ``TaskSeq-takeWhile(Inclusive)?Async stops consuming after predicate fails`` inclusive = + let f, expected = + if inclusive then TaskSeq.takeWhileInclusiveAsync, "ABBC" + else TaskSeq.takeWhileAsync, "ABB" + taskSeq { 1; 2; 2; 3; 3; failwith "Too far" } + |> f (fun x -> task { return x <= 2 }) |> TaskSeq.map char |> TaskSeq.map ((+) '@') |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal "AB") + |> Task.map (String >> should equal expected) // This is the base condition as one would expect in actual code let inline cond x = x <> 6 From 6ddf16ed128702e663db43cc07b3b952b5726428 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Mon, 12 Dec 2022 10:30:16 +0000 Subject: [PATCH 08/14] Address @abelbraaksma review comments --- README.md | 260 +++++++++--------- .../TaskSeq.TakeWhile.Tests.fs | 6 +- src/FSharp.Control.TaskSeq/TaskSeq.fs | 8 +- src/FSharp.Control.TaskSeq/TaskSeqInternal.fs | 53 +--- 4 files changed, 150 insertions(+), 177 deletions(-) diff --git a/README.md b/README.md index 9afdd1c3..d9a82b5e 100644 --- a/README.md +++ b/README.md @@ -187,134 +187,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` | 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` | | | -| ✅ [#83][] | `except` | `except` | | | -| ✅ [#83][] | | `exceptOfSeq` | | | -| ✅ [#70][] | `exists` | `exists` | `existsAsync` | | -| | `exists2` | `exists2` | | | -| ✅ [#23][] | `filter` | `filter` | `filterAsync` | | -| ✅ [#23][] | `find` | `find` | `findAsync` | | -| 🚫 | `findBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | -| ✅ [#68][] | `findIndex` | `findIndex` | `findIndexAsync` | | -| 🚫 | `findIndexBack` | n/a | n/a | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | -| ✅ [#2][] | `fold` | `fold` | `foldAsync` | | -| | `fold2` | `fold2` | `fold2Async` | | -| 🚫 | `foldBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | -| 🚫 | `foldBack2` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | -| | `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.") | -| ✅ [#90][] | `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` | | | -| ✅ [#126][] | `takeWhile` | `takeWhile` | `takeWhileAsync`, `takeWhileInclusive`, `takeWhileInclusiveAsync` | | -| ✅ [#2][] | `toArray` | `toArray` | `toArrayAsync` | | -| ✅ [#2][] | | `toIList` | `toIListAsync` | | -| ✅ [#2][] | `toList` | `toList` | `toListAsync` | | -| ✅ [#2][] | | `toResizeArray` | `toResizeArrayAsync` | | -| ✅ [#2][] | | `toSeq` | `toSeqAsync` | | -| | | […] | | | -| ❓ | `transpose` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | -| | `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` | | | +| 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` | | | +| ✅ [#83][] | `except` | `except` | | | +| ✅ [#83][] | | `exceptOfSeq` | | | +| ✅ [#70][] | `exists` | `exists` | `existsAsync` | | +| | `exists2` | `exists2` | | | +| ✅ [#23][] | `filter` | `filter` | `filterAsync` | | +| ✅ [#23][] | `find` | `find` | `findAsync` | | +| 🚫 | `findBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | +| ✅ [#68][] | `findIndex` | `findIndex` | `findIndexAsync` | | +| 🚫 | `findIndexBack` | n/a | n/a | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | +| ✅ [#2][] | `fold` | `fold` | `foldAsync` | | +| | `fold2` | `fold2` | `fold2Async` | | +| 🚫 | `foldBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | +| 🚫 | `foldBack2` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | +| | `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.") | +| ✅ [#90][] | `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` | | | +| ✅ [#126][] | `takeWhile` | `takeWhile` | `takeWhileAsync`, `takeWhileInclusive`, `takeWhileInclusiveAsync` | | +| ✅ [#2][] | `toArray` | `toArray` | `toArrayAsync` | | +| ✅ [#2][] | | `toIList` | `toIListAsync` | | +| ✅ [#2][] | `toList` | `toList` | `toListAsync` | | +| ✅ [#2][] | | `toResizeArray` | `toResizeArrayAsync` | | +| ✅ [#2][] | | `toSeq` | `toSeqAsync` | | +| | | […] | | | +| ❓ | `transpose` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | +| | `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._ @@ -441,10 +441,6 @@ module TaskSeq = val existsAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task val filter: predicate: ('T -> bool) -> source: taskSeq<'T> -> taskSeq<'T> val filterAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> taskSeq<'T> - val takeWhile: predicate: ('T -> bool) -> source: taskSeq<'T> -> taskSeq<'T> - val takeWhileAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> taskSeq<'T> - val takeWhileInclusive: predicate: ('T -> bool) -> source: taskSeq<'T> -> taskSeq<'T> - val takeWhileInclusiveAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> taskSeq<'T> val find: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task<'T> val findAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task<'T> val findIndex: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs index 7e8e133e..c4438507 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs @@ -86,11 +86,11 @@ module StopsEnumeratingAfterFail = |> TaskSeq.toArrayAsync |> Task.map (String >> should equal expected) -// This is the base condition as one would expect in actual code +/// This is the base condition as one would expect in actual code let inline cond x = x <> 6 -// For each of the tests below, we add a guard that will trigger if the predicate is passed items known to be beyond the -// first failing item in the known sequence (which is 1..10) +/// For each of the tests below, we add a guard that will trigger if the predicate is passed items known to be beyond the +/// first failing item in the known sequence (which is 1..10) let inline condWithGuard x = let res = cond x if x > 6 then failwith "Test sequence should not be enumerated beyond the first item failing the predicate" diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index a315d177..78719650 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -253,10 +253,10 @@ module TaskSeq = let chooseAsync chooser source = Internal.choose (TryPickAsync chooser) source let filter predicate source = Internal.filter (Predicate predicate) source let filterAsync predicate source = Internal.filter (PredicateAsync predicate) source - let takeWhile predicate source = Internal.takeWhile (Predicate predicate) source - let takeWhileAsync predicate source = Internal.takeWhile (PredicateAsync predicate) source - let takeWhileInclusive predicate source = Internal.takeWhileInclusive (Predicate predicate) source - let takeWhileInclusiveAsync predicate source = Internal.takeWhileInclusive (PredicateAsync predicate) source + let takeWhile predicate source = Internal.takeWhile false (Predicate predicate) source + let takeWhileAsync predicate source = Internal.takeWhile false (PredicateAsync predicate) source + let takeWhileInclusive predicate source = Internal.takeWhile true (Predicate predicate) source + let takeWhileInclusiveAsync predicate source = Internal.takeWhile true (PredicateAsync predicate) source let tryPick chooser source = Internal.tryPick (TryPick chooser) source let tryPickAsync chooser source = Internal.tryPick (TryPickAsync chooser) source let tryFind predicate source = Internal.tryFind (Predicate predicate) source diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index 62f2884a..584586e6 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -532,54 +532,31 @@ module internal TaskSeqInternal = | false -> () } - let takeWhile predicate (source: taskSeq<_>) = taskSeq { + let takeWhile inclusive predicate (source: taskSeq<_>) = taskSeq { use e = source.GetAsyncEnumerator(CancellationToken()) let! step = e.MoveNextAsync() - let mutable go = step + let mutable more = step match predicate with | Predicate predicate -> - while go do + while more do let value = e.Current - if predicate value then + more <- predicate value + if more || inclusive then yield value - let! more = e.MoveNextAsync() - go <- more - else go <- false + if more then + let! ok = e.MoveNextAsync() + more <- ok | PredicateAsync predicate -> - while go do + while more do let value = e.Current - match! predicate value with - | true -> + let! passed = predicate value + more <- passed + if more || inclusive then yield value - let! more = e.MoveNextAsync() - go <- more - | false -> go <- false - } - - let takeWhileInclusive predicate (source: taskSeq<_>) = taskSeq { - use e = source.GetAsyncEnumerator(CancellationToken()) - let! step = e.MoveNextAsync() - let mutable go = step - - match predicate with - | Predicate predicate -> - while go do - let value = e.Current - yield value - if predicate value then - let! more = e.MoveNextAsync() - go <- more - else go <- false - | PredicateAsync predicate -> - while go do - let value = e.Current - yield value - match! predicate value with - | true -> - let! more = e.MoveNextAsync() - go <- more - | false -> go <- false + if more then + let! ok = e.MoveNextAsync() + more <- ok } // Consider turning using an F# version of this instead? From 86c4adc3ed7095d9ac6cfc0e1c3647a75506f7cd Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Tue, 13 Dec 2022 03:45:35 +0000 Subject: [PATCH 09/14] Extend SideEffect tests --- .../TaskSeq.TakeWhile.Tests.fs | 84 +++++++++++++++++++ src/FSharp.Control.TaskSeq/TaskSeq.fs | 2 +- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs index c4438507..fcbf09b8 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs @@ -133,3 +133,87 @@ module SideEffects = |> TaskSeq.map ((+) '@') |> TaskSeq.toArrayAsync |> Task.map (String >> should equal "ABCDE") + + [] + let ``TaskSeq-takeWhile(Inclusive)?(Async)? __special-case__ prove it does not read beyond the failing yield`` (inclusive, async) = task { + let mutable x = 42 // for this test, the potential mutation should not actually occur + + let items = taskSeq { + yield x // Always passes the test; always returned + yield x * 2 // the failing item (which will also be yielded in the result when using *Inclusive) + x <- x + 1 // we are proving we never get here + } + + let f = + match inclusive, async with + | false, false -> TaskSeq.takeWhile (fun x -> x = 42) + | true, false -> TaskSeq.takeWhileInclusive (fun x -> x = 42) + | false, true -> TaskSeq.takeWhileAsync (fun x -> task { return x = 42 }) + | true, true -> TaskSeq.takeWhileInclusiveAsync (fun x -> task { return x = 42 }) + + let expected = if inclusive then [| 42; 84 |] else [| 42 |] + + let! first = items |> f |> TaskSeq.toArrayAsync + let! repeat = items |> f |> TaskSeq.toArrayAsync + + first |> should equal expected + repeat |> should equal expected + x |> should equal 42 + } + + [] + let ``TaskSeq-takeWhile(Inclusive)?(Async)? __special-case__ prove side effects are executed`` (inclusive, async) = task { + let mutable x = 41 + + let items = taskSeq { + x <- x + 1 + yield x + x <- x + 2 + yield x * 2 + x <- x + 200 // as previously proven, we should not trigger this + } + + let f = + match inclusive, async with + | false, false -> TaskSeq.takeWhile (fun x -> x < 50) + | true, false -> TaskSeq.takeWhileInclusive (fun x -> x < 50) + | false, true -> TaskSeq.takeWhileAsync (fun x -> task { return x < 50 }) + | true, true -> TaskSeq.takeWhileInclusiveAsync (fun x -> task { return x < 50 }) + + let expectedFirst = if inclusive then [| 42; 44*2 |] else [| 42 |] + let expectedRepeat = if inclusive then [| 45; 47*2 |] else [| 45 |] + + let! first = items |> f |> TaskSeq.toArrayAsync + x |> should equal 44 + let! repeat = items |> f |> TaskSeq.toArrayAsync + x |> should equal 47 + + first |> should equal expectedFirst + repeat |> should equal expectedRepeat + } + + [)>] + let ``TaskSeq-takeWhile consumes the prefix of a longer sequence, with mutation`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + + let! first = TaskSeq.takeWhile (fun x -> x < 5) ts |> TaskSeq.toArrayAsync + let expected = [| 1..4 |] + first |> should equal expected + + // side effect, reiterating causes it to resume from where we left it (minus the failing item) + let! repeat = TaskSeq.takeWhile (fun x -> x < 5) ts |> TaskSeq.toArrayAsync + repeat |> should not' (equal expected) + } + + [)>] + let ``TaskSeq-takeWhileInclusiveAsync consumes the prefix for a longer sequence, with mutation`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + + let! first = TaskSeq.takeWhileInclusiveAsync (fun x -> task { return x < 5 }) ts |> TaskSeq.toArrayAsync + let expected = [| 1..5 |] + first |> should equal expected + + // side effect, reiterating causes it to resume from where we left it (minus the failing item) + let! repeat = TaskSeq.takeWhileInclusiveAsync (fun x -> task { return x < 5 }) ts |> TaskSeq.toArrayAsync + repeat |> should not' (equal expected) + } diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index 78719650..a59f807d 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -253,7 +253,7 @@ module TaskSeq = let chooseAsync chooser source = Internal.choose (TryPickAsync chooser) source let filter predicate source = Internal.filter (Predicate predicate) source let filterAsync predicate source = Internal.filter (PredicateAsync predicate) source - let takeWhile predicate source = Internal.takeWhile false (Predicate predicate) source + let takeWhile predicate source = Internal.takeWhile (*inclusive:*)false (Predicate predicate) source let takeWhileAsync predicate source = Internal.takeWhile false (PredicateAsync predicate) source let takeWhileInclusive predicate source = Internal.takeWhile true (Predicate predicate) source let takeWhileInclusiveAsync predicate source = Internal.takeWhile true (PredicateAsync predicate) source From 11ce9d139b119f00eba98f09db1af3b607632590 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Wed, 14 Dec 2022 03:19:04 +0100 Subject: [PATCH 10/14] Update release-notes.txt for adding `TaskSeq.takeWhileXXX` and fix readme.md --- README.md | 6 +++++- release-notes.txt | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d9a82b5e..c38760bd 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,8 @@ The following is the progress report: | ✅ [#90][] | `singleton` | `singleton` | | | | | `skip` | `skip` | | | | | `skipWhile` | `skipWhile` | `skipWhileAsync` | | +| | | | `skipWhileInclusive` | | +| | | | `skipWhileInclusiveAsync` | | | ❓ | `sort` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | | ❓ | `sortBy` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | | ❓ | `sortByAscending` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | @@ -289,7 +291,9 @@ The following is the progress report: | | `sumBy` | `sumBy` | `sumByAsync` | | | ✅ [#76][] | `tail` | `tail` | | | | | `take` | `take` | | | -| ✅ [#126][] | `takeWhile` | `takeWhile` | `takeWhileAsync`, `takeWhileInclusive`, `takeWhileInclusiveAsync` | | +| ✅ [#126][]| `takeWhile` | `takeWhile` | `takeWhileAsync` | | +| ✅ [#126][]| | | `takeWhileInclusive` | | +| ✅ [#126][]| | | `takeWhileInclusiveAsync`| | | ✅ [#2][] | `toArray` | `toArray` | `toArrayAsync` | | | ✅ [#2][] | | `toIList` | `toIListAsync` | | | ✅ [#2][] | `toList` | `toList` | `toListAsync` | | diff --git a/release-notes.txt b/release-notes.txt index d80f41d1..b17a305b 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -1,5 +1,7 @@ Release notes: +0.4.x (unreleased) + - adds TaskSeq.takeWhile, takeWhileAsync, takeWhileInclusive, takeWhileInclusiveAsync, #126 (by @bartelink) 0.3.0 - internal renames, improved doc comments, signature files for complex types, hide internal-only types, fixes #112. From c8dc715e0d990ebd82c34a458d4347be6cac8d6b Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Wed, 14 Dec 2022 04:01:12 +0100 Subject: [PATCH 11/14] Small refactoring of new tests, each original test is still there, but function-choice is now lifted --- .../FSharp.Control.TaskSeq.Test.fsproj | 2 +- .../TaskSeq.TakeWhile.Tests.fs | 217 +++++++++--------- 2 files changed, 104 insertions(+), 115 deletions(-) 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 edfaa2d8..037acb8f 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -20,7 +20,6 @@ - @@ -36,6 +35,7 @@ + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs index fcbf09b8..95d56ce4 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs @@ -14,129 +14,115 @@ open FSharp.Control // TaskSeq.takeWhileInclusiveAsync // +[] +module With = + /// The only real difference in semantics between the base and the *Inclusive variant lies in whether the final item is returned. + /// NOTE the semantics are very clear on only propagating a single failing item in the inclusive case. + let getFunction inclusive isAsync = + match inclusive, isAsync with + | false, false -> TaskSeq.takeWhile + | false, true -> fun pred -> TaskSeq.takeWhileAsync (pred >> Task.fromResult) + | true, false -> TaskSeq.takeWhileInclusive + | true, true -> fun pred -> TaskSeq.takeWhileInclusiveAsync (pred >> Task.fromResult) + + let verifyAsString expected = + TaskSeq.map char + >> TaskSeq.map ((+) '@') + >> TaskSeq.toArrayAsync + >> Task.map (String >> should equal expected) + + /// This is the base condition as one would expect in actual code + let inline cond x = x <> 6 + + /// For each of the tests below, we add a guard that will trigger if the predicate is passed items known to be beyond the + /// first failing item in the known sequence (which is 1..10) + let inline condWithGuard x = + let res = cond x + + if x > 6 then + failwith "Test sequence should not be enumerated beyond the first item failing the predicate" + + res + module EmptySeq = [)>] let ``TaskSeq-takeWhile has no effect`` variant = Gen.getEmptyVariant variant |> TaskSeq.takeWhile ((=) 12) - |> TaskSeq.toListAsync - |> Task.map (List.isEmpty >> should be True) + |> verifyEmpty [)>] let ``TaskSeq-takeWhileAsync has no effect`` variant = Gen.getEmptyVariant variant |> TaskSeq.takeWhileAsync (fun x -> task { return x = 12 }) - |> TaskSeq.toListAsync - |> Task.map (List.isEmpty >> should be True) - -// The primary requirement is that items after the item failing the predicate must be excluded -module FiltersAfterFail = - [] - let ``TaskSeq-takeWhile(Inclusive)? excludes all items after predicate fails`` inclusive = - // The only real difference in semantics between the base and the *Inclusive variant lies in whether the final item is returned - // NOTE the semantics are very clear on only propagating a single failing item in the inclusive case - let f, expected = - if inclusive then TaskSeq.takeWhileInclusive, "ABBC" - else TaskSeq.takeWhile, "ABB" - seq { 1; 2; 2; 3; 3; 2; 1 } + |> verifyEmpty + +module Other = + [] + [] + [] + [] + [] + let ``TaskSeq-takeWhileXXX exclude all items after predicate fails`` (inclusive, isAsync) = + let functionToTest = With.getFunction inclusive isAsync + + [ 1; 2; 2; 3; 3; 2; 1 ] |> TaskSeq.ofSeq - |> f (fun x -> x <= 2) - |> TaskSeq.map char - |> TaskSeq.map ((+) '@') - |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal expected) - - // Same as preceding test, just with Async functions - [] - let ``TaskSeq-takeWhile(Inclusive)?Async excludes all items after after predicate fails`` inclusive = - let f, expected = - if inclusive then TaskSeq.takeWhileInclusiveAsync, "ABBC" - else TaskSeq.takeWhileAsync, "ABB" - taskSeq { 1; 2; 2; 3; 3; 2; 1 } - |> f (fun x -> task { return x <= 2 }) - |> TaskSeq.map char - |> TaskSeq.map ((+) '@') - |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal expected) - -// Covers the fact that it's not sufficient to merely exclude successor items - it's also critical that the enumeration terminates -module StopsEnumeratingAfterFail = - [] - let ``TaskSeq-takeWhile(Inclusive)? stops consuming after predicate fails`` inclusive = - let f, expected = - if inclusive then TaskSeq.takeWhileInclusive, "ABBC" - else TaskSeq.takeWhile, "ABB" - seq { 1; 2; 2; 3; 3; failwith "Too far" } + |> functionToTest (fun x -> x <= 2) + |> verifyAsString (if inclusive then "ABBC" else "ABB") + + [] + [] + [] + [] + [] + let ``TaskSeq-takeWhileXXX stops consuming after predicate fails`` (inclusive, isAsync) = + let functionToTest = With.getFunction inclusive isAsync + + seq { + yield! [ 1; 2; 2; 3; 3 ] + yield failwith "Too far" + } |> TaskSeq.ofSeq - |> f (fun x -> x <= 2) - |> TaskSeq.map char - |> TaskSeq.map ((+) '@') - |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal expected) - - [] - let ``TaskSeq-takeWhile(Inclusive)?Async stops consuming after predicate fails`` inclusive = - let f, expected = - if inclusive then TaskSeq.takeWhileInclusiveAsync, "ABBC" - else TaskSeq.takeWhileAsync, "ABB" - taskSeq { 1; 2; 2; 3; 3; failwith "Too far" } - |> f (fun x -> task { return x <= 2 }) - |> TaskSeq.map char - |> TaskSeq.map ((+) '@') - |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal expected) - -/// This is the base condition as one would expect in actual code -let inline cond x = x <> 6 - -/// For each of the tests below, we add a guard that will trigger if the predicate is passed items known to be beyond the -/// first failing item in the known sequence (which is 1..10) -let inline condWithGuard x = - let res = cond x - if x > 6 then failwith "Test sequence should not be enumerated beyond the first item failing the predicate" - res + |> functionToTest (fun x -> x <= 2) + |> verifyAsString (if inclusive then "ABBC" else "ABB") + module Immutable = + [)>] let ``TaskSeq-takeWhile filters correctly`` variant = Gen.getSeqImmutable variant |> TaskSeq.takeWhile condWithGuard - |> TaskSeq.map char - |> TaskSeq.map ((+) '@') - |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal "ABCDE") + |> verifyAsString "ABCDE" [)>] let ``TaskSeq-takeWhileAsync filters correctly`` variant = Gen.getSeqImmutable variant |> TaskSeq.takeWhileAsync (fun x -> task { return condWithGuard x }) - |> TaskSeq.map char - |> TaskSeq.map ((+) '@') - |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal "ABCDE") + |> verifyAsString "ABCDE" module SideEffects = [)>] let ``TaskSeq-takeWhile filters correctly`` variant = Gen.getSeqWithSideEffect variant |> TaskSeq.takeWhile condWithGuard - |> TaskSeq.map char - |> TaskSeq.map ((+) '@') - |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal "ABCDE") + |> verifyAsString "ABCDE" [)>] let ``TaskSeq-takeWhileAsync filters correctly`` variant = Gen.getSeqWithSideEffect variant |> TaskSeq.takeWhileAsync (fun x -> task { return condWithGuard x }) - |> TaskSeq.map char - |> TaskSeq.map ((+) '@') - |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal "ABCDE") - - [] - let ``TaskSeq-takeWhile(Inclusive)?(Async)? __special-case__ prove it does not read beyond the failing yield`` (inclusive, async) = task { + |> verifyAsString "ABCDE" + + [] + [] + [] + [] + [] + let ``TaskSeq-takeWhileXXX prove it does not read beyond the failing yield`` (inclusive, isAsync) = task { let mutable x = 42 // for this test, the potential mutation should not actually occur + let functionToTest = getFunction inclusive isAsync ((=) 42) let items = taskSeq { yield x // Always passes the test; always returned @@ -144,26 +130,24 @@ module SideEffects = x <- x + 1 // we are proving we never get here } - let f = - match inclusive, async with - | false, false -> TaskSeq.takeWhile (fun x -> x = 42) - | true, false -> TaskSeq.takeWhileInclusive (fun x -> x = 42) - | false, true -> TaskSeq.takeWhileAsync (fun x -> task { return x = 42 }) - | true, true -> TaskSeq.takeWhileInclusiveAsync (fun x -> task { return x = 42 }) - let expected = if inclusive then [| 42; 84 |] else [| 42 |] - let! first = items |> f |> TaskSeq.toArrayAsync - let! repeat = items |> f |> TaskSeq.toArrayAsync + let! first = items |> functionToTest |> TaskSeq.toArrayAsync + let! repeat = items |> functionToTest |> TaskSeq.toArrayAsync first |> should equal expected repeat |> should equal expected x |> should equal 42 } - [] - let ``TaskSeq-takeWhile(Inclusive)?(Async)? __special-case__ prove side effects are executed`` (inclusive, async) = task { + [] + [] + [] + [] + [] + let ``TaskSeq-takeWhileXXX prove side effects are executed`` (inclusive, isAsync) = task { let mutable x = 41 + let functionToTest = getFunction inclusive isAsync ((>) 50) let items = taskSeq { x <- x + 1 @@ -173,19 +157,12 @@ module SideEffects = x <- x + 200 // as previously proven, we should not trigger this } - let f = - match inclusive, async with - | false, false -> TaskSeq.takeWhile (fun x -> x < 50) - | true, false -> TaskSeq.takeWhileInclusive (fun x -> x < 50) - | false, true -> TaskSeq.takeWhileAsync (fun x -> task { return x < 50 }) - | true, true -> TaskSeq.takeWhileInclusiveAsync (fun x -> task { return x < 50 }) + let expectedFirst = if inclusive then [| 42; 44 * 2 |] else [| 42 |] + let expectedRepeat = if inclusive then [| 45; 47 * 2 |] else [| 45 |] - let expectedFirst = if inclusive then [| 42; 44*2 |] else [| 42 |] - let expectedRepeat = if inclusive then [| 45; 47*2 |] else [| 45 |] - - let! first = items |> f |> TaskSeq.toArrayAsync + let! first = items |> functionToTest |> TaskSeq.toArrayAsync x |> should equal 44 - let! repeat = items |> f |> TaskSeq.toArrayAsync + let! repeat = items |> functionToTest |> TaskSeq.toArrayAsync x |> should equal 47 first |> should equal expectedFirst @@ -196,12 +173,18 @@ module SideEffects = let ``TaskSeq-takeWhile consumes the prefix of a longer sequence, with mutation`` variant = task { let ts = Gen.getSeqWithSideEffect variant - let! first = TaskSeq.takeWhile (fun x -> x < 5) ts |> TaskSeq.toArrayAsync + let! first = + TaskSeq.takeWhile (fun x -> x < 5) ts + |> TaskSeq.toArrayAsync + let expected = [| 1..4 |] first |> should equal expected // side effect, reiterating causes it to resume from where we left it (minus the failing item) - let! repeat = TaskSeq.takeWhile (fun x -> x < 5) ts |> TaskSeq.toArrayAsync + let! repeat = + TaskSeq.takeWhile (fun x -> x < 5) ts + |> TaskSeq.toArrayAsync + repeat |> should not' (equal expected) } @@ -209,11 +192,17 @@ module SideEffects = let ``TaskSeq-takeWhileInclusiveAsync consumes the prefix for a longer sequence, with mutation`` variant = task { let ts = Gen.getSeqWithSideEffect variant - let! first = TaskSeq.takeWhileInclusiveAsync (fun x -> task { return x < 5 }) ts |> TaskSeq.toArrayAsync + let! first = + TaskSeq.takeWhileInclusiveAsync (fun x -> task { return x < 5 }) ts + |> TaskSeq.toArrayAsync + let expected = [| 1..5 |] first |> should equal expected // side effect, reiterating causes it to resume from where we left it (minus the failing item) - let! repeat = TaskSeq.takeWhileInclusiveAsync (fun x -> task { return x < 5 }) ts |> TaskSeq.toArrayAsync + let! repeat = + TaskSeq.takeWhileInclusiveAsync (fun x -> task { return x < 5 }) ts + |> TaskSeq.toArrayAsync + repeat |> should not' (equal expected) } From 4f0ee2f0409005077403f7f53add90adeee9d06b Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Wed, 14 Dec 2022 04:11:54 +0100 Subject: [PATCH 12/14] Use a struct DU for differentiating between exclusive/inclusive while, and remove a few branching tests --- src/FSharp.Control.TaskSeq/TaskSeq.fs | 8 ++-- src/FSharp.Control.TaskSeq/TaskSeqInternal.fs | 41 ++++++++++++++++--- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index a59f807d..c4690d86 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -253,10 +253,10 @@ module TaskSeq = let chooseAsync chooser source = Internal.choose (TryPickAsync chooser) source let filter predicate source = Internal.filter (Predicate predicate) source let filterAsync predicate source = Internal.filter (PredicateAsync predicate) source - let takeWhile predicate source = Internal.takeWhile (*inclusive:*)false (Predicate predicate) source - let takeWhileAsync predicate source = Internal.takeWhile false (PredicateAsync predicate) source - let takeWhileInclusive predicate source = Internal.takeWhile true (Predicate predicate) source - let takeWhileInclusiveAsync predicate source = Internal.takeWhile true (PredicateAsync predicate) source + let takeWhile predicate source = Internal.takeWhile Exclusive (Predicate predicate) source + let takeWhileAsync predicate source = Internal.takeWhile Exclusive (PredicateAsync predicate) source + let takeWhileInclusive predicate source = Internal.takeWhile Inclusive (Predicate predicate) source + let takeWhileInclusiveAsync predicate source = Internal.takeWhile Inclusive (PredicateAsync predicate) source let tryPick chooser source = Internal.tryPick (TryPick chooser) source let tryPickAsync chooser source = Internal.tryPick (TryPickAsync chooser) source let tryFind predicate source = Internal.tryFind (Predicate predicate) source diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index 584586e6..89ce51e3 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -11,6 +11,11 @@ type internal AsyncEnumStatus = | WithCurrent | AfterAll +[] +type internal WhileKind = + | Inclusive + | Exclusive + [] type internal Action<'T, 'U, 'TaskU when 'TaskU :> Task<'U>> = | CountableAction of countable_action: (int -> 'T -> 'U) @@ -532,28 +537,52 @@ module internal TaskSeqInternal = | false -> () } - let takeWhile inclusive predicate (source: taskSeq<_>) = taskSeq { + let takeWhile whileKind predicate (source: taskSeq<_>) = taskSeq { use e = source.GetAsyncEnumerator(CancellationToken()) let! step = e.MoveNextAsync() let mutable more = step - match predicate with - | Predicate predicate -> + match whileKind, predicate with + | Exclusive, Predicate predicate -> while more do let value = e.Current more <- predicate value - if more || inclusive then + + if more then yield value + let! ok = e.MoveNextAsync() + more <- ok + + | Inclusive, Predicate predicate -> + while more do + let value = e.Current + more <- predicate value + + yield value + if more then let! ok = e.MoveNextAsync() more <- ok - | PredicateAsync predicate -> + + | Exclusive, PredicateAsync predicate -> while more do let value = e.Current let! passed = predicate value more <- passed - if more || inclusive then + + if more then yield value + let! ok = e.MoveNextAsync() + more <- ok + + | Inclusive, PredicateAsync predicate -> + while more do + let value = e.Current + let! passed = predicate value + more <- passed + + yield value + if more then let! ok = e.MoveNextAsync() more <- ok From 4b3f62f31eb1b790a87a99df896dd8f661780582 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Wed, 14 Dec 2022 04:32:08 +0100 Subject: [PATCH 13/14] Expand the tests a bit for takeWhile, make sure each use-case is covered by all four new functions --- .../TaskSeq.TakeWhile.Tests.fs | 142 ++++++++++++------ 1 file changed, 96 insertions(+), 46 deletions(-) diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs index 95d56ce4..bc8d27a4 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs @@ -25,6 +25,7 @@ module With = | true, false -> TaskSeq.takeWhileInclusive | true, true -> fun pred -> TaskSeq.takeWhileInclusiveAsync (pred >> Task.fromResult) + /// adds '@' to each number and concatenates the chars before calling 'should equal' let verifyAsString expected = TaskSeq.map char >> TaskSeq.map ((+) '@') @@ -46,61 +47,80 @@ module With = module EmptySeq = [)>] - let ``TaskSeq-takeWhile has no effect`` variant = - Gen.getEmptyVariant variant - |> TaskSeq.takeWhile ((=) 12) - |> verifyEmpty + let ``TaskSeq-takeWhile+A has no effect`` variant = task { + do! Gen.getEmptyVariant variant + |> TaskSeq.takeWhile ((=) 12) + |> verifyEmpty + + do! Gen.getEmptyVariant variant + |> TaskSeq.takeWhileAsync ((=) 12 >> Task.fromResult) + |> verifyEmpty + } [)>] - let ``TaskSeq-takeWhileAsync has no effect`` variant = - Gen.getEmptyVariant variant - |> TaskSeq.takeWhileAsync (fun x -> task { return x = 12 }) - |> verifyEmpty - -module Other = - [] - [] - [] - [] - [] - let ``TaskSeq-takeWhileXXX exclude all items after predicate fails`` (inclusive, isAsync) = - let functionToTest = With.getFunction inclusive isAsync - - [ 1; 2; 2; 3; 3; 2; 1 ] - |> TaskSeq.ofSeq - |> functionToTest (fun x -> x <= 2) - |> verifyAsString (if inclusive then "ABBC" else "ABB") - - [] - [] - [] - [] - [] - let ``TaskSeq-takeWhileXXX stops consuming after predicate fails`` (inclusive, isAsync) = - let functionToTest = With.getFunction inclusive isAsync + let ``TaskSeq-takeWhileInclusive+A has no effect`` variant = task { + do! Gen.getEmptyVariant variant + |> TaskSeq.takeWhileInclusive ((=) 12) + |> verifyEmpty + + do! Gen.getEmptyVariant variant + |> TaskSeq.takeWhileInclusiveAsync ((=) 12 >> Task.fromResult) + |> verifyEmpty + } - seq { - yield! [ 1; 2; 2; 3; 3 ] - yield failwith "Too far" - } - |> TaskSeq.ofSeq - |> functionToTest (fun x -> x <= 2) - |> verifyAsString (if inclusive then "ABBC" else "ABB") +module Immutable = + [)>] + let ``TaskSeq-takeWhile+A filters correctly`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.takeWhile condWithGuard + |> verifyAsString "ABCDE" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.takeWhileAsync (fun x -> task { return condWithGuard x }) + |> verifyAsString "ABCDE" + } -module Immutable = + [)>] + let ``TaskSeq-takeWhile+A does not pick first item when false`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.takeWhile ((=) 0) + |> verifyAsString "" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.takeWhileAsync ((=) 0 >> Task.fromResult) + |> verifyAsString "" + } [)>] - let ``TaskSeq-takeWhile filters correctly`` variant = - Gen.getSeqImmutable variant - |> TaskSeq.takeWhile condWithGuard - |> verifyAsString "ABCDE" + let ``TaskSeq-takeWhileInclusive+A filters correctly`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.takeWhileInclusive condWithGuard + |> verifyAsString "ABCDEF" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.takeWhileInclusiveAsync (fun x -> task { return condWithGuard x }) + |> verifyAsString "ABCDEF" + } [)>] - let ``TaskSeq-takeWhileAsync filters correctly`` variant = - Gen.getSeqImmutable variant - |> TaskSeq.takeWhileAsync (fun x -> task { return condWithGuard x }) - |> verifyAsString "ABCDE" + let ``TaskSeq-takeWhileInclusive+A always pick at least the first item`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.takeWhileInclusive ((=) 0) + |> verifyAsString "A" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.takeWhileInclusiveAsync ((=) 0 >> Task.fromResult) + |> verifyAsString "A" + } module SideEffects = [)>] @@ -206,3 +226,33 @@ module SideEffects = repeat |> should not' (equal expected) } + +module Other = + [] + [] + [] + [] + [] + let ``TaskSeq-takeWhileXXX exclude all items after predicate fails`` (inclusive, isAsync) = + let functionToTest = With.getFunction inclusive isAsync + + [ 1; 2; 2; 3; 3; 2; 1 ] + |> TaskSeq.ofSeq + |> functionToTest (fun x -> x <= 2) + |> verifyAsString (if inclusive then "ABBC" else "ABB") + + [] + [] + [] + [] + [] + let ``TaskSeq-takeWhileXXX stops consuming after predicate fails`` (inclusive, isAsync) = + let functionToTest = With.getFunction inclusive isAsync + + seq { + yield! [ 1; 2; 2; 3; 3 ] + yield failwith "Too far" + } + |> TaskSeq.ofSeq + |> functionToTest (fun x -> x <= 2) + |> verifyAsString (if inclusive then "ABBC" else "ABB") From ea22f4879ca171b84232ad3dfe467c74111f38bd Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Wed, 14 Dec 2022 04:33:48 +0100 Subject: [PATCH 14/14] Fix absent link --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c38760bd..950cbcdc 100644 --- a/README.md +++ b/README.md @@ -549,6 +549,7 @@ module TaskSeq = [#82]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/82 [#83]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/83 [#90]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/90 +[#126]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/126 [issues]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues [nuget]: https://www.nuget.org/packages/FSharp.Control.TaskSeq/