diff --git a/README.md b/README.md index cdf279f7..d8b23491 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ The _resumable state machine_ backing the `taskSeq` CE is now finished and _rest We are working hard on getting a full set of module functions on `TaskSeq` that can be used with `IAsyncEnumerable` sequences. Our guide is the set of F# `Seq` functions in F# Core and, where applicable, the functions provided by `AsyncSeq`. Each implemented function is documented through XML doc comments to provide the necessary context-sensitive help. -The following is the progress report: +This is what has been implemented so far, is planned or skipped: | Done | `Seq` | `TaskSeq` | Variants | Remarks | |------------------|--------------------|----------------------|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -353,7 +353,7 @@ The following is the progress report: | ✅ [#76][] | | `tryTail` | | | | | `unfold` | `unfold` | `unfoldAsync` | | | | `updateAt` | `updateAt` | | | -| | `where` | `where` | `whereAsync` | | +| ✅ [#217][]| `where` | `where` | `whereAsync` | | | | `windowed` | `windowed` | | | | ✅ [#2][] | `zip` | `zip` | | | | | `zip3` | `zip3` | | | @@ -551,6 +551,8 @@ module TaskSeq = val tryPick: chooser: ('T -> 'U option) -> source: TaskSeq<'T> -> Task<'U option> val tryPickAsync: chooser: ('T -> #Task<'U option>) -> source: TaskSeq<'T> -> Task<'U option> val tryTail: source: TaskSeq<'T> -> Task option> + val where: predicate: ('T -> bool) -> source: TaskSeq<'T> -> TaskSeq<'T> + val whereAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> TaskSeq<'T> val unbox<'U when 'U: struct> : source: TaskSeq -> TaskSeq<'U> val zip: source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'T * 'U> ``` @@ -600,6 +602,7 @@ module TaskSeq = [#126]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/126 [#133]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/133 [#209]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/209 +[#217]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/217 [issues]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues [nuget]: https://www.nuget.org/packages/FSharp.Control.TaskSeq/ diff --git a/assets/nuget-package-readme.md b/assets/nuget-package-readme.md index 440af182..1a7651e6 100644 --- a/assets/nuget-package-readme.md +++ b/assets/nuget-package-readme.md @@ -103,11 +103,9 @@ let feedFromTwitter user pwd = taskSeq { ### `TaskSeq` module functions -We are working hard on getting a full set of module functions on `TaskSeq` that can be used with `IAsyncEnumerable` sequences. Our guide is the set of F# `Seq` functions in F# Core and, where applicable, the functions provided from `AsyncSeq`. Each implemented function is documented through XML doc comments to provide the necessary context-sensitive help. - We are working hard on getting a full set of module functions on `TaskSeq` that can be used with `IAsyncEnumerable` sequences. Our guide is the set of F# `Seq` functions in F# Core and, where applicable, the functions provided by `AsyncSeq`. Each implemented function is documented through XML doc comments to provide the necessary context-sensitive help. -This is what was implemented, planned or skipped: +This is what has been implemented so far, is planned or skipped: | Done | `Seq` | `TaskSeq` | Variants | Remarks | |------------------|--------------------|----------------------|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -235,7 +233,7 @@ This is what was implemented, planned or skipped: | ✅ [#76][] | | `tryTail` | | | | | `unfold` | `unfold` | `unfoldAsync` | | | | `updateAt` | `updateAt` | | | -| | `where` | `where` | `whereAsync` | | +| ✅ [#217][]| `where` | `where` | `whereAsync` | | | | `windowed` | `windowed` | | | | ✅ [#2][] | `zip` | `zip` | | | | | `zip3` | `zip3` | | | @@ -308,4 +306,5 @@ _The motivation for `readOnly` in `Seq` is that a cast from a mutable array or l [#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 -[#209]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/209 \ No newline at end of file +[#209]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/209 +[#217]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/217 diff --git a/release-notes.txt b/release-notes.txt index ebd40c4c..06930db4 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -5,14 +5,15 @@ Release notes: - new surface area functions, fixes #208: * TaskSeq.take, TaskSeq.skip, #209 * TaskSeq.truncate, TaskSeq.drop, #209 + * TaskSeq.where, TaskSeq.whereAsync, #217 - Performance: less thread hops with 'StartImmediateAsTask' instead of 'StartAsTask', fixes #135 - BINARY INCOMPATIBILITY: 'TaskSeq' module is now static members on 'TaskSeq<_>', fixes #184 - - DEPRECATIONS (warning FS0044): + - DEPRECATIONS (warning FS0044): - type 'taskSeq<_>' is renamed to 'TaskSeq<_>', fixes #193 - function 'ValueTask.ofIValueTaskSource` renamed to `ValueTask.ofSource`, fixes #193 - function `ValueTask.FromResult` is renamed to `ValueTask.fromResult`, fixes #193 - + 0.4.0-alpha.1 - fixes not calling Dispose for 'use!', 'use', or `finally` blocks #157 (by @bartelink) - BREAKING CHANGE: null args now raise ArgumentNullException instead of NullReferenceException, #127 @@ -20,7 +21,7 @@ Release notes: - adds TaskSeq.takeWhile, takeWhileAsync, takeWhileInclusive, takeWhileInclusiveAsync, #126 (by @bartelink) - adds AsyncSeq vs TaskSeq comparison chart, #131 - removes release-notes.txt from file dependencies, but keep in the package, #138 - + 0.3.0 - internal renames, improved doc comments, signature files for complex types, hide internal-only types, fixes #112. - adds support for static TaskLike, allowing the same let! and do! overloads that F# task supports, fixes #110. diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Filter.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Filter.Tests.fs index fb67ad9f..0e189cc5 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Filter.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Filter.Tests.fs @@ -10,67 +10,108 @@ open FSharp.Control // // TaskSeq.filter // TaskSeq.filterAsync +// TaskSeq.where +// TaskSeq.whereAsync // module EmptySeq = [] - let ``Null source is invalid`` () = + let ``TaskSeq-filter or where with null source raises`` () = assertNullArg <| fun () -> TaskSeq.filter (fun _ -> false) null assertNullArg <| fun () -> TaskSeq.filterAsync (fun _ -> Task.fromResult false) null + assertNullArg + <| fun () -> TaskSeq.where (fun _ -> false) null + + assertNullArg + <| fun () -> TaskSeq.whereAsync (fun _ -> Task.fromResult false) null + [)>] - let ``TaskSeq-filter has no effect`` variant = - Gen.getEmptyVariant variant - |> TaskSeq.filter ((=) 12) - |> TaskSeq.toListAsync - |> Task.map (List.isEmpty >> should be True) + let ``TaskSeq-filter or where has no effect`` variant = task { + do! + Gen.getEmptyVariant variant + |> TaskSeq.filter ((=) 12) + |> TaskSeq.toListAsync + |> Task.map (List.isEmpty >> should be True) + + do! + Gen.getEmptyVariant variant + |> TaskSeq.where ((=) 12) + |> TaskSeq.toListAsync + |> Task.map (List.isEmpty >> should be True) + } [)>] - let ``TaskSeq-filterAsync has no effect`` variant = - Gen.getEmptyVariant variant - |> TaskSeq.filterAsync (fun x -> task { return x = 12 }) - |> TaskSeq.toListAsync - |> Task.map (List.isEmpty >> should be True) + let ``TaskSeq-filterAsync or whereAsync has no effect`` variant = task { + do! + Gen.getEmptyVariant variant + |> TaskSeq.filterAsync (fun x -> task { return x = 12 }) + |> TaskSeq.toListAsync + |> Task.map (List.isEmpty >> should be True) + + do! + Gen.getEmptyVariant variant + |> TaskSeq.whereAsync (fun x -> task { return x = 12 }) + |> TaskSeq.toListAsync + |> Task.map (List.isEmpty >> should be True) + } module Immutable = [)>] - let ``TaskSeq-filter filters correctly`` variant = - Gen.getSeqImmutable variant - |> TaskSeq.filter ((<=) 5) // greater than - |> TaskSeq.map char - |> TaskSeq.map ((+) '@') - |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal "EFGHIJ") + let ``TaskSeq-filter or where filters correctly`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.filter ((<=) 5) // greater than + |> verifyDigitsAsString "EFGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.where ((>) 5) // greater than + |> verifyDigitsAsString "ABCD" + } [)>] - let ``TaskSeq-filterAsync filters correctly`` variant = - Gen.getSeqImmutable variant - |> TaskSeq.filterAsync (fun x -> task { return x <= 5 }) - |> TaskSeq.map char - |> TaskSeq.map ((+) '@') - |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal "ABCDE") + let ``TaskSeq-filterAsync or whereAsync filters correctly`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.filterAsync (fun x -> task { return x <= 5 }) + |> verifyDigitsAsString "ABCDE" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.whereAsync (fun x -> task { return x > 5 }) + |> verifyDigitsAsString "FGHIJ" + + } module SideEffects = [)>] - let ``TaskSeq-filter filters correctly`` variant = - Gen.getSeqWithSideEffect variant - |> TaskSeq.filter ((<=) 5) // greater than - |> TaskSeq.map char - |> TaskSeq.map ((+) '@') - |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal "EFGHIJ") + let ``TaskSeq-filter filters correctly`` variant = task { + do! + Gen.getSeqWithSideEffect variant + |> TaskSeq.filter ((<=) 5) // greater than or equal + |> verifyDigitsAsString "EFGHIJ" + + do! + Gen.getSeqWithSideEffect variant + |> TaskSeq.where ((>) 5) // less than + |> verifyDigitsAsString "ABCD" + } [)>] - let ``TaskSeq-filterAsync filters correctly`` variant = - Gen.getSeqWithSideEffect variant - |> TaskSeq.filterAsync (fun x -> task { return x <= 5 }) - |> TaskSeq.map char - |> TaskSeq.map ((+) '@') - |> TaskSeq.toArrayAsync - |> Task.map (String >> should equal "ABCDE") + let ``TaskSeq-filterAsync filters correctly`` variant = task { + do! + Gen.getSeqWithSideEffect variant + |> TaskSeq.filterAsync (fun x -> task { return x <= 5 }) + |> verifyDigitsAsString "ABCDE" + + do! + Gen.getSeqWithSideEffect variant + |> TaskSeq.whereAsync (fun x -> task { return x > 5 && x < 9 }) + |> verifyDigitsAsString "FGH" + } diff --git a/src/FSharp.Control.TaskSeq.Test/TestUtils.fs b/src/FSharp.Control.TaskSeq.Test/TestUtils.fs index d8afe1b6..c98b4797 100644 --- a/src/FSharp.Control.TaskSeq.Test/TestUtils.fs +++ b/src/FSharp.Control.TaskSeq.Test/TestUtils.fs @@ -141,6 +141,13 @@ module TestUtils = |> TaskSeq.toArrayAsync |> Task.map (should equal [| 1..10 |]) + /// Turns a sequence of numbers into a string, starting with A for '1' + let verifyDigitsAsString expected = + TaskSeq.map char + >> TaskSeq.map ((+) '@') + >> TaskSeq.toArrayAsync + >> Task.map (String >> should equal expected) + /// Delays (no spin-wait!) between 20 and 70ms, assuming a 15.6ms resolution clock let longDelay () = task { do! Task.Delay(Random().Next(20, 70)) } diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index 42c00ee5..be9961f7 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -285,6 +285,8 @@ type TaskSeq private () = static member filter predicate source = Internal.filter (Predicate predicate) source static member filterAsync predicate source = Internal.filter (PredicateAsync predicate) source + static member where predicate source = Internal.filter (Predicate predicate) source + static member whereAsync predicate source = Internal.filter (PredicateAsync predicate) source static member skip count source = Internal.skipOrTake Skip count source static member drop count source = Internal.skipOrTake Drop count source diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index d9de462d..68b8d4e2 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -725,6 +725,34 @@ type TaskSeq = /// Thrown when the input task sequence is null. static member filterAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> TaskSeq<'T> + /// + /// Returns a new task sequence containing only the elements of the collection + /// for which the given function returns . + /// If is asynchronous, consider using . + /// + /// Alias for . + /// + /// + /// A function to test whether an item in the input sequence should be included in the output or not. + /// The input task sequence. + /// The resulting task sequence. + /// Thrown when the input task sequence is null. + static member where: predicate: ('T -> bool) -> source: TaskSeq<'T> -> TaskSeq<'T> + + /// + /// Returns a new task sequence containing only the elements of the input sequence + /// for which the given function returns . + /// If is synchronous, consider using . + /// + /// Alias for . + /// + /// + /// An asynchronous function to test whether an item in the input sequence should be included in the output or not. + /// The input task sequence. + /// The resulting task sequence. + /// Thrown when the input task sequence is null. + static member whereAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> TaskSeq<'T> + /// /// Returns a task sequence that, when iterated, skips elements of the underlying /// sequence, and then yields the remainder. Raises an exception if there are not @@ -742,7 +770,6 @@ type TaskSeq = /// static member skip: count: int -> source: TaskSeq<'T> -> TaskSeq<'T> - /// /// Returns a task sequence that, when iterated, drops at most elements of the /// underlying sequence, and then returns the remainder of the elements, if any.