diff --git a/README.md b/README.md
index 0c55fab8..950cbcdc 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.
@@ -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` | | |
-| | `takeWhile` | `takeWhile` | `takeWhileAsync` | |
+| ✅ [#126][]| `takeWhile` | `takeWhile` | `takeWhileAsync` | |
+| ✅ [#126][]| | | `takeWhileInclusive` | |
+| ✅ [#126][]| | | `takeWhileInclusiveAsync`| |
| ✅ [#2][] | `toArray` | `toArray` | `toArrayAsync` | |
| ✅ [#2][] | | `toIList` | `toIListAsync` | |
| ✅ [#2][] | `toList` | `toList` | `toListAsync` | |
@@ -545,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/
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.
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..037acb8f 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
@@ -38,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
new file mode 100644
index 00000000..bc8d27a4
--- /dev/null
+++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs
@@ -0,0 +1,258 @@
+module TaskSeq.Tests.TakeWhile
+
+open System
+open Xunit
+open FsUnit.Xunit
+open FsToolkit.ErrorHandling
+
+open FSharp.Control
+
+//
+// TaskSeq.takeWhile
+// TaskSeq.takeWhileAsync
+// TaskSeq.takeWhileInclusive
+// 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)
+
+ /// adds '@' to each number and concatenates the chars before calling 'should equal'
+ 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+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-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
+ }
+
+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"
+ }
+
+ [)>]
+ 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-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-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 =
+ [)>]
+ let ``TaskSeq-takeWhile filters correctly`` variant =
+ Gen.getSeqWithSideEffect variant
+ |> TaskSeq.takeWhile condWithGuard
+ |> verifyAsString "ABCDE"
+
+ [)>]
+ let ``TaskSeq-takeWhileAsync filters correctly`` variant =
+ Gen.getSeqWithSideEffect variant
+ |> TaskSeq.takeWhileAsync (fun x -> task { return condWithGuard x })
+ |> 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
+ 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 expected = if inclusive then [| 42; 84 |] else [| 42 |]
+
+ 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-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
+ yield x
+ x <- x + 2
+ yield x * 2
+ x <- x + 200 // as previously proven, we should not trigger this
+ }
+
+ let expectedFirst = if inclusive then [| 42; 44 * 2 |] else [| 42 |]
+ let expectedRepeat = if inclusive then [| 45; 47 * 2 |] else [| 45 |]
+
+ let! first = items |> functionToTest |> TaskSeq.toArrayAsync
+ x |> should equal 44
+ let! repeat = items |> functionToTest |> 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)
+ }
+
+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")
diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs
index 05f5313c..c4690d86 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 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/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..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)
@@ -531,6 +536,58 @@ module internal TaskSeqInternal =
| true -> yield item
| false -> ()
}
+
+ let takeWhile whileKind predicate (source: taskSeq<_>) = taskSeq {
+ use e = source.GetAsyncEnumerator(CancellationToken())
+ let! step = e.MoveNextAsync()
+ let mutable more = step
+
+ match whileKind, predicate with
+ | Exclusive, Predicate predicate ->
+ while more do
+ let value = e.Current
+ more <- predicate value
+
+ 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
+
+ | Exclusive, PredicateAsync predicate ->
+ while more do
+ let value = e.Current
+ let! passed = predicate value
+ more <- passed
+
+ 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
+ }
+
// Consider turning using an F# version of this instead?
// https://github.com/i3arnon/ConcurrentHashSet
type ConcurrentHashSet<'T when 'T: equality>(ct) =