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 ff7ae5e..d625d93 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -24,6 +24,7 @@ + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Exists.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Exists.Tests.fs index 04a652f..ee65f1e 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Exists.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Exists.Tests.fs @@ -82,7 +82,7 @@ module Immutable = module SideEffects = [)>] - 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 @@ -100,7 +100,7 @@ module SideEffects = } [)>] - 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 } @@ -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! @@ -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! diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Forall.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Forall.Tests.fs new file mode 100644 index 0000000..8c47dea --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Forall.Tests.fs @@ -0,0 +1,200 @@ +module TaskSeq.Tests.Forall + +open Xunit +open FsUnit.Xunit + +open FSharp.Control + +// +// TaskSeq.forall +// TaskSeq.forallAsyncc +// + +module EmptySeq = + [] + let ``Null source is invalid`` () = + assertNullArg + <| fun () -> TaskSeq.forall (fun _ -> false) null + + assertNullArg + <| fun () -> TaskSeq.forallAsync (fun _ -> Task.fromResult false) null + + [)>] + let ``TaskSeq-forall always returns true`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.forall ((=) 12) + |> Task.map (should be True) + + [)>] + 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 = + [)>] + 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) + } + + [)>] + 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) + } + + [)>] + 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) + + [)>] + 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 = + [)>] + 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 + } + + [)>] + 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 + } + + [] + 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 + } + + [] + 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 + } + + + [] + 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! + } + + [] + 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! + } diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index 7775e2e..710dadd 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -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 diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index 3c4fd63..cb5eefe 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -875,6 +875,30 @@ type TaskSeq = /// Thrown when the input task sequence is null. static member whereAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> TaskSeq<'T> + /// + /// Tests if all elements of the sequence satisfy the given predicate. Stops evaluating + /// as soon as returns . + /// If is asynchronous, consider using . + /// + /// + /// A function to test an element of the input sequence. + /// The input task sequence. + /// A task that, after awaiting, holds true if every element of the sequence satisfies the predicate; false otherwise. + /// Thrown when the input task sequence is null. + static member forall: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task + + /// + /// Tests if all elements of the sequence satisfy the given asynchronous predicate. Stops evaluating + /// as soon as returns . + /// If is synchronous, consider using . + /// + /// + /// A function to test an element of the input sequence. + /// The input task sequence. + /// A task that, after awaiting, holds true if every element of the sequence satisfies the predicate; false otherwise. + /// Thrown when the input task sequence is null. + static member forallAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task + /// /// 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 diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index 5827637..d7773ce 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -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