From e9c947b92426e083f63ea6694b6c118e11e15444 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sat, 16 Mar 2024 18:34:19 +0100 Subject: [PATCH 1/3] Implement `TaskSeq.forall` and `forallAsync` --- src/FSharp.Control.TaskSeq/TaskSeq.fs | 3 ++ src/FSharp.Control.TaskSeq/TaskSeq.fsi | 24 ++++++++++ src/FSharp.Control.TaskSeq/TaskSeqInternal.fs | 44 +++++++++++++++++-- 3 files changed, 67 insertions(+), 4 deletions(-) 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 From 15b9649a0e18929859663018ade67bd6cdf093d8 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 17 Mar 2024 00:57:45 +0100 Subject: [PATCH 2/3] Add unit tests for `TaskSeq.forall` and `TaskSeq.forallAsync` --- .../FSharp.Control.TaskSeq.Test.fsproj | 1 + .../TaskSeq.Forall.Tests.fs | 228 ++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 src/FSharp.Control.TaskSeq.Test/TaskSeq.Forall.Tests.fs 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.Forall.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Forall.Tests.fs new file mode 100644 index 0000000..04a652f --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Forall.Tests.fs @@ -0,0 +1,228 @@ +module TaskSeq.Tests.Exists + +open Xunit +open FsUnit.Xunit + +open FSharp.Control + +// +// TaskSeq.exists +// TaskSeq.existsAsyncc +// + +module EmptySeq = + [] + let ``Null source is invalid`` () = + assertNullArg + <| fun () -> TaskSeq.exists (fun _ -> false) null + + assertNullArg + <| fun () -> TaskSeq.existsAsync (fun _ -> Task.fromResult false) null + + [)>] + let ``TaskSeq-exists returns false`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.exists ((=) 12) + |> Task.map (should be False) + + [)>] + let ``TaskSeq-existsAsync returns false`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.existsAsync (fun x -> task { return x = 12 }) + |> Task.map (should be False) + +module Immutable = + [)>] + let ``TaskSeq-exists sad path returns false`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.exists ((=) 0) + |> Task.map (should be False) + + [)>] + let ``TaskSeq-existsAsync sad path return false`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.existsAsync (fun x -> task { return x = 0 }) + |> Task.map (should be False) + + [)>] + let ``TaskSeq-exists happy path middle of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.exists (fun x -> x < 6 && x > 4) + |> Task.map (should be True) + + [)>] + let ``TaskSeq-existsAsync happy path middle of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.existsAsync (fun x -> task { return x < 6 && x > 4 }) + |> Task.map (should be True) + + [)>] + let ``TaskSeq-exists happy path first item of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.exists ((=) 1) + |> Task.map (should be True) + + [)>] + let ``TaskSeq-existsAsync happy path first item of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.existsAsync (fun x -> task { return x = 1 }) + |> Task.map (should be True) + + [)>] + let ``TaskSeq-exists happy path last item of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.exists ((=) 10) + |> Task.map (should be True) + + [)>] + let ``TaskSeq-existsAsync happy path last item of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.existsAsync (fun x -> task { return x = 10 }) + |> Task.map (should be True) + +module SideEffects = + [)>] + let ``TaskSeq-exists KeyNotFoundException only sometimes for mutated state`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + let finder = (=) 11 + + // first: false + let! found = TaskSeq.exists finder ts + found |> should be False + + // find again: found now, because of side effects + let! found = TaskSeq.exists finder ts + found |> should be True + + // find once more: false + let! found = TaskSeq.exists finder ts + found |> should be False + } + + [)>] + let ``TaskSeq-existsAsync KeyNotFoundException only sometimes for mutated state`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + let finder x = task { return x = 11 } + + // first: false + let! found = TaskSeq.existsAsync finder ts + found |> should be False + + // find again: found now, because of side effects + let! found = TaskSeq.existsAsync finder ts + found |> should be True + + // find once more: false + let! found = TaskSeq.existsAsync finder ts + found |> should be False + } + + [] + let ``TaskSeq-exists _specialcase_ prove we don't read past the found item`` () = task { + let mutable i = 0 + + let ts = taskSeq { + for _ in 0..9 do + i <- i + 1 + yield i + } + + let! found = ts |> TaskSeq.exists ((=) 3) + found |> should be True + 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.exists ((=) 4) + found |> should be True + i |> should equal 4 // only partial evaluation! + } + + [] + let ``TaskSeq-existsAsync _specialcase_ prove we don't read past the found item`` () = task { + let mutable i = 0 + + let ts = taskSeq { + for _ in 0..9 do + i <- i + 1 + yield i + } + + let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 3 }) + found |> should be True + 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.existsAsync (fun x -> task { return x = 4 }) + found |> should be True + i |> should equal 4 + } + + [] + let ``TaskSeq-exists _specialcase_ prove we don't read past the found item v2`` () = task { + let mutable i = 0 + + let ts = taskSeq { + yield 42 + i <- i + 1 + i <- i + 1 + } + + let! found = ts |> TaskSeq.exists ((=) 42) + found |> should be True + i |> should equal 0 // because no MoveNext after found item, the last statements are not executed + } + + [] + let ``TaskSeq-existsAsync _specialcase_ prove we don't read past the found item v2`` () = task { + let mutable i = 0 + + let ts = taskSeq { + yield 42 + i <- i + 1 + i <- i + 1 + } + + let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 42 }) + found |> should be True + i |> should equal 0 // because no MoveNext after found item, the last statements are not executed + } + + [] + let ``TaskSeq-exists _specialcase_ prove statement after yield 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.exists ((=) 0) + 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' + let! found = ts |> TaskSeq.exists ((=) 4) + found |> should be True + i |> should equal 4 // only partial evaluation! + } + + [] + let ``TaskSeq-existsAsync _specialcase_ prove statement after yield 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.existsAsync (fun x -> task { return x = 0 }) + 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' + let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 4 }) + found |> should be True + i |> should equal 4 // only partial evaluation! + } From 219f89daeccf50b4cf4b6e6571c48f631d3e474d Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sun, 17 Mar 2024 00:58:08 +0100 Subject: [PATCH 3/3] Fix some naming and test comments in `TaskSeq.exists` tests --- .../TaskSeq.Exists.Tests.fs | 8 +- .../TaskSeq.Forall.Tests.fs | 202 ++++++++---------- 2 files changed, 91 insertions(+), 119 deletions(-) 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 index 04a652f..8c47dea 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Forall.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Forall.Tests.fs @@ -1,4 +1,4 @@ -module TaskSeq.Tests.Exists +module TaskSeq.Tests.Forall open Xunit open FsUnit.Xunit @@ -6,119 +6,117 @@ open FsUnit.Xunit open FSharp.Control // -// TaskSeq.exists -// TaskSeq.existsAsyncc +// TaskSeq.forall +// TaskSeq.forallAsyncc // module EmptySeq = [] let ``Null source is invalid`` () = assertNullArg - <| fun () -> TaskSeq.exists (fun _ -> false) null + <| fun () -> TaskSeq.forall (fun _ -> false) null assertNullArg - <| fun () -> TaskSeq.existsAsync (fun _ -> Task.fromResult false) null + <| fun () -> TaskSeq.forallAsync (fun _ -> Task.fromResult false) null [)>] - let ``TaskSeq-exists returns false`` variant = + let ``TaskSeq-forall always returns true`` variant = Gen.getEmptyVariant variant - |> TaskSeq.exists ((=) 12) - |> Task.map (should be False) + |> TaskSeq.forall ((=) 12) + |> Task.map (should be True) [)>] - let ``TaskSeq-existsAsync returns false`` variant = + let ``TaskSeq-forallAsync always returns true`` variant = Gen.getEmptyVariant variant - |> TaskSeq.existsAsync (fun x -> task { return x = 12 }) - |> Task.map (should be False) - -module Immutable = - [)>] - let ``TaskSeq-exists sad path returns false`` variant = - Gen.getSeqImmutable variant - |> TaskSeq.exists ((=) 0) - |> Task.map (should be False) - - [)>] - let ``TaskSeq-existsAsync sad path return false`` variant = - Gen.getSeqImmutable variant - |> TaskSeq.existsAsync (fun x -> task { return x = 0 }) - |> Task.map (should be False) - - [)>] - let ``TaskSeq-exists happy path middle of seq`` variant = - Gen.getSeqImmutable variant - |> TaskSeq.exists (fun x -> x < 6 && x > 4) - |> Task.map (should be True) - - [)>] - let ``TaskSeq-existsAsync happy path middle of seq`` variant = - Gen.getSeqImmutable variant - |> TaskSeq.existsAsync (fun x -> task { return x < 6 && x > 4 }) + |> TaskSeq.forallAsync (fun x -> task { return x = 12 }) |> Task.map (should be True) +module Immutable = [)>] - let ``TaskSeq-exists happy path first item of seq`` variant = - Gen.getSeqImmutable variant - |> TaskSeq.exists ((=) 1) - |> Task.map (should be True) + 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-existsAsync happy path first item of seq`` variant = - Gen.getSeqImmutable variant - |> TaskSeq.existsAsync (fun x -> task { return x = 1 }) - |> Task.map (should be True) + 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-exists happy path last item of seq`` variant = + let ``TaskSeq-forall happy path whole seq true`` variant = Gen.getSeqImmutable variant - |> TaskSeq.exists ((=) 10) + |> TaskSeq.forall (fun x -> x < 6 || x > 5) |> Task.map (should be True) [)>] - let ``TaskSeq-existsAsync happy path last item of seq`` variant = + let ``TaskSeq-forallAsync happy path whole seq true`` variant = Gen.getSeqImmutable variant - |> TaskSeq.existsAsync (fun x -> task { return x = 10 }) + |> TaskSeq.forallAsync (fun x -> task { return x <= 10 && x >= 0 }) |> Task.map (should be True) module SideEffects = [)>] - let ``TaskSeq-exists KeyNotFoundException only sometimes for mutated state`` variant = task { + let ``TaskSeq-forall mutated state can change result`` variant = task { let ts = Gen.getSeqWithSideEffect variant - let finder = (=) 11 + let predicate x = x > 10 // first: false - let! found = TaskSeq.exists finder ts - found |> should be 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.exists finder ts + let! found = TaskSeq.forall predicate ts found |> should be True - // find once more: false - let! found = TaskSeq.exists finder ts - found |> should be False + // 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-existsAsync KeyNotFoundException only sometimes for mutated state`` variant = task { + let ``TaskSeq-forallAsync mutated state can change result`` variant = task { let ts = Gen.getSeqWithSideEffect variant - let finder x = task { return x = 11 } + let predicate x = Task.fromResult (x > 10) // first: false - let! found = TaskSeq.existsAsync finder ts - found |> should be 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.existsAsync finder ts + let! found = TaskSeq.forallAsync predicate ts found |> should be True - // find once more: false - let! found = TaskSeq.existsAsync finder ts - found |> should be False + // 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-exists _specialcase_ prove we don't read past the found item`` () = task { + let ``TaskSeq-forall _specialcase_ prove we don't read past the first failing item`` () = task { let mutable i = 0 let ts = taskSeq { @@ -127,18 +125,18 @@ module SideEffects = yield i } - let! found = ts |> TaskSeq.exists ((=) 3) - found |> should be True + 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.exists ((=) 4) + let! found = ts |> TaskSeq.forall ((<=) 4) found |> should be True - i |> should equal 4 // only partial evaluation! + i |> should equal 13 // we evaluated to the end } [] - let ``TaskSeq-existsAsync _specialcase_ prove we don't read past the found item`` () = task { + let ``TaskSeq-forallAsync _specialcase_ prove we don't read past the first failing item`` () = task { let mutable i = 0 let ts = taskSeq { @@ -147,48 +145,22 @@ module SideEffects = yield i } - let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 3 }) - found |> should be True + 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.existsAsync (fun x -> task { return x = 4 }) - found |> should be True - i |> should equal 4 - } + let! found = + ts + |> TaskSeq.forallAsync (fun x -> Task.fromResult (x >= 4)) - [] - let ``TaskSeq-exists _specialcase_ prove we don't read past the found item v2`` () = task { - let mutable i = 0 - - let ts = taskSeq { - yield 42 - i <- i + 1 - i <- i + 1 - } - - let! found = ts |> TaskSeq.exists ((=) 42) found |> should be True - i |> should equal 0 // because no MoveNext after found item, the last statements are not executed + i |> should equal 13 // we evaluated to the end } - [] - let ``TaskSeq-existsAsync _specialcase_ prove we don't read past the found item v2`` () = task { - let mutable i = 0 - - let ts = taskSeq { - yield 42 - i <- i + 1 - i <- i + 1 - } - - let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 42 }) - found |> should be True - i |> should equal 0 // because no MoveNext after found item, the last statements are not executed - } [] - let ``TaskSeq-exists _specialcase_ prove statement after yield is not evaluated`` () = task { + let ``TaskSeq-forall _specialcase_ prove statement after first false result is not evaluated`` () = task { let mutable i = 0 let ts = taskSeq { @@ -197,18 +169,18 @@ module SideEffects = i <- i + 1 } - let! found = ts |> TaskSeq.exists ((=) 0) - found |> should be True - i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated + 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 now starting at '1' - let! found = ts |> TaskSeq.exists ((=) 4) - found |> should be True + // 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-existsAsync _specialcase_ prove statement after yield is not evaluated`` () = task { + let ``TaskSeq-forallAsync _specialcase_ prove statement after first false result is not evaluated`` () = task { let mutable i = 0 let ts = taskSeq { @@ -217,12 +189,12 @@ module SideEffects = i <- i + 1 } - let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 0 }) - found |> should be True - i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated + 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 now starting at '1' - let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 4 }) - found |> should be True + // 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! }