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.forall and forallAsync #240

Merged
merged 3 commits into from
Mar 17, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<Compile Include="TaskSeq.FindIndex.Tests.fs" />
<Compile Include="TaskSeq.Find.Tests.fs" />
<Compile Include="TaskSeq.Fold.Tests.fs" />
<Compile Include="TaskSeq.Forall.Tests.fs" />
<Compile Include="TaskSeq.Head.Tests.fs" />
<Compile Include="TaskSeq.Indexed.Tests.fs" />
<Compile Include="TaskSeq.Init.Tests.fs" />
Expand Down
8 changes: 4 additions & 4 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Exists.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ module Immutable =

module SideEffects =
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
let ``TaskSeq-exists KeyNotFoundException only sometimes for mutated state`` variant = task {
let ``TaskSeq-exists success only sometimes for mutated state`` variant = task {
let ts = Gen.getSeqWithSideEffect variant
let finder = (=) 11

Expand All @@ -100,7 +100,7 @@ module SideEffects =
}

[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
let ``TaskSeq-existsAsync KeyNotFoundException only sometimes for mutated state`` variant = task {
let ``TaskSeq-existsAsync success only sometimes for mutated state`` variant = task {
let ts = Gen.getSeqWithSideEffect variant
let finder x = task { return x = 11 }

Expand Down Expand Up @@ -201,7 +201,7 @@ module SideEffects =
found |> should be True
i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated

// find some next item. We do get a new iterator, but mutable state is now starting at '1'
// find some next item. We do get a new iterator, but mutable state is now still starting at '0'
let! found = ts |> TaskSeq.exists ((=) 4)
found |> should be True
i |> should equal 4 // only partial evaluation!
Expand All @@ -221,7 +221,7 @@ module SideEffects =
found |> should be True
i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated

// find some next item. We do get a new iterator, but mutable state is now starting at '1'
// find some next item. We do get a new iterator, but mutable state is now still starting at '0'
let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 4 })
found |> should be True
i |> should equal 4 // only partial evaluation!
Expand Down
200 changes: 200 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Forall.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
module TaskSeq.Tests.Forall

open Xunit
open FsUnit.Xunit

open FSharp.Control

//
// TaskSeq.forall
// TaskSeq.forallAsyncc
//

module EmptySeq =
[<Fact>]
let ``Null source is invalid`` () =
assertNullArg
<| fun () -> TaskSeq.forall (fun _ -> false) null

assertNullArg
<| fun () -> TaskSeq.forallAsync (fun _ -> Task.fromResult false) null

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-forall always returns true`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.forall ((=) 12)
|> Task.map (should be True)

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-forallAsync always returns true`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.forallAsync (fun x -> task { return x = 12 })
|> Task.map (should be True)

module Immutable =
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-forall sad path returns false`` variant = task {
do!
Gen.getSeqImmutable variant
|> TaskSeq.forall ((=) 0)
|> Task.map (should be False)

do!
Gen.getSeqImmutable variant
|> TaskSeq.forall ((>) 9) // lt
|> Task.map (should be False)
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-forallAsync sad path returns false`` variant = task {
do!
Gen.getSeqImmutable variant
|> TaskSeq.forallAsync (fun x -> task { return x = 0 })
|> Task.map (should be False)

do!
Gen.getSeqImmutable variant
|> TaskSeq.forallAsync (fun x -> task { return x < 9 })
|> Task.map (should be False)
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-forall happy path whole seq true`` variant =
Gen.getSeqImmutable variant
|> TaskSeq.forall (fun x -> x < 6 || x > 5)
|> Task.map (should be True)

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-forallAsync happy path whole seq true`` variant =
Gen.getSeqImmutable variant
|> TaskSeq.forallAsync (fun x -> task { return x <= 10 && x >= 0 })
|> Task.map (should be True)

module SideEffects =
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
let ``TaskSeq-forall mutated state can change result`` variant = task {
let ts = Gen.getSeqWithSideEffect variant
let predicate x = x > 10

// first: false
let! found = TaskSeq.forall predicate ts
found |> should be False // fails on first item, not many side effects yet

// ensure side effects executes
do! consumeTaskSeq ts

// find again: found now, because of side effects
let! found = TaskSeq.forall predicate ts
found |> should be True

// find once more, still true, as numbers increase
do! consumeTaskSeq ts // ensure side effects executes
let! found = TaskSeq.forall predicate ts
found |> should be True
}

[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
let ``TaskSeq-forallAsync mutated state can change result`` variant = task {
let ts = Gen.getSeqWithSideEffect variant
let predicate x = Task.fromResult (x > 10)

// first: false
let! found = TaskSeq.forallAsync predicate ts
found |> should be False // fails on first item, not many side effects yet

// ensure side effects executes
do! consumeTaskSeq ts

// find again: found now, because of side effects
let! found = TaskSeq.forallAsync predicate ts
found |> should be True

// find once more, still true, as numbers increase
do! consumeTaskSeq ts // ensure side effects executes
let! found = TaskSeq.forallAsync predicate ts
found |> should be True
}

[<Fact>]
let ``TaskSeq-forall _specialcase_ prove we don't read past the first failing item`` () = task {
let mutable i = 0

let ts = taskSeq {
for _ in 0..9 do
i <- i + 1
yield i
}

let! found = ts |> TaskSeq.forall ((>) 3)
found |> should be False
i |> should equal 3 // only partial evaluation!

// find next item. We do get a new iterator, but mutable state is now starting at '3', so first item now returned is '4'.
let! found = ts |> TaskSeq.forall ((<=) 4)
found |> should be True
i |> should equal 13 // we evaluated to the end
}

[<Fact>]
let ``TaskSeq-forallAsync _specialcase_ prove we don't read past the first failing item`` () = task {
let mutable i = 0

let ts = taskSeq {
for _ in 0..9 do
i <- i + 1
yield i
}

let! found = ts |> TaskSeq.forallAsync (fun x -> Task.fromResult (x < 3))
found |> should be False
i |> should equal 3 // only partial evaluation!

// find next item. We do get a new iterator, but mutable state is now starting at '3', so first item now returned is '4'.
let! found =
ts
|> TaskSeq.forallAsync (fun x -> Task.fromResult (x >= 4))

found |> should be True
i |> should equal 13 // we evaluated to the end
}


[<Fact>]
let ``TaskSeq-forall _specialcase_ prove statement after first false result is not evaluated`` () = task {
let mutable i = 0

let ts = taskSeq {
for _ in 0..9 do
yield i
i <- i + 1
}

let! found = ts |> TaskSeq.forall ((>) 0)
found |> should be False
i |> should equal 0 // notice that it should be one higher if the statement after 'yield' was evaluated

// find some next item. We do get a new iterator, but mutable state is still starting at '0'
let! found = ts |> TaskSeq.forall ((>) 4)
found |> should be False
i |> should equal 4 // only partial evaluation!
}

[<Fact>]
let ``TaskSeq-forallAsync _specialcase_ prove statement after first false result is not evaluated`` () = task {
let mutable i = 0

let ts = taskSeq {
for _ in 0..9 do
yield i
i <- i + 1
}

let! found = ts |> TaskSeq.forallAsync (fun x -> Task.fromResult (x < 0))
found |> should be False
i |> should equal 0 // notice that it should be one higher if the statement after 'yield' was evaluated

// find some next item. We do get a new iterator, but mutable state is still starting at '0'
let! found = ts |> TaskSeq.forallAsync (fun x -> Task.fromResult (x < 4))
found |> should be False
i |> should equal 4 // only partial evaluation!
}
3 changes: 3 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,9 @@ type TaskSeq private () =
static member except itemsToExclude source = Internal.except itemsToExclude source
static member exceptOfSeq itemsToExclude source = Internal.exceptOfSeq itemsToExclude source

static member forall predicate source = Internal.forall (Predicate predicate) source
static member forallAsync predicate source = Internal.forall (PredicateAsync predicate) source

static member exists predicate source =
Internal.tryFind (Predicate predicate) source
|> Task.map Option.isSome
Expand Down
24 changes: 24 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeq.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,30 @@ type TaskSeq =
/// <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>
/// Tests if all elements of the sequence satisfy the given predicate. Stops evaluating
/// as soon as <paramref name="predicate" /> returns <see cref="false" />.
/// If <paramref name="predicate" /> is asynchronous, consider using <see cref="TaskSeq.forallAsync" />.
/// </summary>
///
/// <param name="predicate">A function to test an element of the input sequence.</param>
/// <param name="source">The input task sequence.</param>
/// <returns>A task that, after awaiting, holds true if every element of the sequence satisfies the predicate; false otherwise.</returns>
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
static member forall: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task<bool>

/// <summary>
/// Tests if all elements of the sequence satisfy the given asynchronous predicate. Stops evaluating
/// as soon as <paramref name="predicate" /> returns <see cref="false" />.
/// If <paramref name="predicate" /> is synchronous, consider using <see cref="TaskSeq.forall" />.
/// </summary>
///
/// <param name="predicate">A function to test an element of the input sequence.</param>
/// <param name="source">The input task sequence.</param>
/// <returns>A task that, after awaiting, holds true if every element of the sequence satisfies the predicate; false otherwise.</returns>
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
static member forallAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> Task<bool>

/// <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 Down
44 changes: 40 additions & 4 deletions src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
Original file line number Diff line number Diff line change
Expand Up @@ -690,18 +690,54 @@ module internal TaskSeqInternal =

taskSeq {
match predicate with
| Predicate predicate ->
| Predicate syncPredicate ->
for item in source do
if predicate item then
if syncPredicate item then
yield item

| PredicateAsync predicate ->
| PredicateAsync asyncPredicate ->
for item in source do
match! predicate item with
match! asyncPredicate item with
| true -> yield item
| false -> ()
}

let forall predicate (source: TaskSeq<_>) =
checkNonNull (nameof source) source

match predicate with
| Predicate syncPredicate -> task {
use e = source.GetAsyncEnumerator CancellationToken.None
let mutable state = true
let! cont = e.MoveNextAsync()
let mutable hasMore = cont

while state && hasMore do
state <- syncPredicate e.Current

if state then
let! cont = e.MoveNextAsync()
hasMore <- cont

return state
}

| PredicateAsync asyncPredicate -> task {
use e = source.GetAsyncEnumerator CancellationToken.None
let mutable state = true
let! cont = e.MoveNextAsync()
let mutable hasMore = cont

while state && hasMore do
let! pred = asyncPredicate e.Current
state <- pred

if state then
let! cont = e.MoveNextAsync()
hasMore <- cont

return state
}

let skipOrTake skipOrTake count (source: TaskSeq<_>) =
checkNonNull (nameof source) source
Expand Down
Loading