Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement TaskSeq.where and TaskSeq.whereAsync #217

Merged
merged 3 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|------------------|--------------------|----------------------|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
Expand Down Expand Up @@ -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` | | |
Expand Down Expand Up @@ -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<TaskSeq<'T> option>
val where: predicate: ('T -> bool) -> source: TaskSeq<'T> -> TaskSeq<'T>
val whereAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> TaskSeq<'T>
val unbox<'U when 'U: struct> : source: TaskSeq<obj> -> TaskSeq<'U>
val zip: source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'T * 'U>
```
Expand Down Expand Up @@ -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/
9 changes: 4 additions & 5 deletions assets/nuget-package-readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|------------------|--------------------|----------------------|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
Expand Down Expand Up @@ -235,7 +233,7 @@ This is what was implemented, planned or skipped:
| &#x2705; [#76][] | | `tryTail` | | |
| | `unfold` | `unfold` | `unfoldAsync` | |
| | `updateAt` | `updateAt` | | |
| | `where` | `where` | `whereAsync` | |
| &#x2705; [#217][]| `where` | `where` | `whereAsync` | |
| | `windowed` | `windowed` | | |
| &#x2705; [#2][] | `zip` | `zip` | | |
| | `zip3` | `zip3` | | |
Expand Down Expand Up @@ -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
[#209]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/209
[#217]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/217
7 changes: 4 additions & 3 deletions release-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@ 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
- adds `let!` and `do!` support for F#'s Async<'T>, #79, #114
- 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.
Expand Down
119 changes: 80 additions & 39 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Filter.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,67 +10,108 @@ open FSharp.Control
//
// TaskSeq.filter
// TaskSeq.filterAsync
// TaskSeq.where
// TaskSeq.whereAsync
//


module EmptySeq =
[<Fact>]
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


[<Theory; ClassData(typeof<TestEmptyVariants>)>]
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)
}

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
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 =
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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"
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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 =
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
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"
}

[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
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"
}
7 changes: 7 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TestUtils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)) }

Expand Down
2 changes: 2 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 28 additions & 1 deletion src/FSharp.Control.TaskSeq/TaskSeq.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,34 @@ type TaskSeq =
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
static member filterAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> TaskSeq<'T>

/// <summary>
/// Returns a new task sequence containing only the elements of the collection
/// for which the given function <paramref name="predicate" /> returns <see cref="true" />.
/// If <paramref name="predicate" /> is asynchronous, consider using <see cref="TaskSeq.whereAsync" />.
///
/// Alias for <see cref="TaskSeq.filter" />.
/// </summary>
///
/// <param name="predicate">A function to test whether an item in the input sequence should be included in the output or not.</param>
/// <param name="source">The input task sequence.</param>
/// <returns>The resulting task sequence.</returns>
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
static member where: predicate: ('T -> bool) -> source: TaskSeq<'T> -> TaskSeq<'T>

/// <summary>
/// Returns a new task sequence containing only the elements of the input sequence
/// for which the given function <paramref name="predicate" /> returns <see cref="true" />.
/// If <paramref name="predicate" /> is synchronous, consider using <see cref="TaskSeq.where" />.
///
/// Alias for <see cref="TaskSeq.filterAsync" />.
/// </summary>
///
/// <param name="predicate">An asynchronous function to test whether an item in the input sequence should be included in the output or not.</param>
/// <param name="source">The input task sequence.</param>
/// <returns>The resulting task sequence.</returns>
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
static member whereAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> TaskSeq<'T>

/// <summary>
/// Returns a task sequence that, when iterated, skips <paramref name="count" /> elements of the underlying
/// sequence, and then yields the remainder. Raises an exception if there are not <paramref name="count" />
Expand All @@ -742,7 +770,6 @@ type TaskSeq =
/// </exception>
static member skip: count: int -> source: TaskSeq<'T> -> TaskSeq<'T>


/// <summary>
/// Returns a task sequence that, when iterated, drops at most <paramref name="count" /> elements of the
/// underlying sequence, and then returns the remainder of the elements, if any.
Expand Down