From 930f9132c58c79112db8f34d01264902f2b9ab6d Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Fri, 4 Nov 2022 00:36:15 +0100 Subject: [PATCH] Implement TaskSeq.exists/existsAsync and contains, plus tests and xml docs --- .../FSharpy.TaskSeq.Test.fsproj | 2 + .../TaskSeq.Contains.Tests.fs | 116 +++++++++ .../TaskSeq.Exists.Tests.fs | 221 ++++++++++++++++++ src/FSharpy.TaskSeq/TaskSeq.fs | 12 + src/FSharpy.TaskSeq/TaskSeq.fsi | 41 ++++ src/FSharpy.TaskSeq/TaskSeqInternal.fs | 2 +- 6 files changed, 393 insertions(+), 1 deletion(-) create mode 100644 src/FSharpy.TaskSeq.Test/TaskSeq.Contains.Tests.fs create mode 100644 src/FSharpy.TaskSeq.Test/TaskSeq.Exists.Tests.fs diff --git a/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj b/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj index 19815165..e7bfdf2b 100644 --- a/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj +++ b/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj @@ -16,8 +16,10 @@ + + diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Contains.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Contains.Tests.fs new file mode 100644 index 00000000..a4442fb8 --- /dev/null +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Contains.Tests.fs @@ -0,0 +1,116 @@ +module FSharpy.Tests.Contains + +open Xunit +open FsUnit.Xunit +open FsToolkit.ErrorHandling + +open FSharpy + +// +// TaskSeq.contains +// + +module EmptySeq = + [)>] + let ``TaskSeq-contains returns false`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.contains 12 + |> Task.map (should be False) + +module Immutable = + [)>] + let ``TaskSeq-contains sad path returns false`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.contains 0 + |> Task.map (should be False) + + [)>] + let ``TaskSeq-contains happy path middle of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.contains 5 + |> Task.map (should be True) + + [)>] + let ``TaskSeq-contains happy path first item of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.contains 1 + |> Task.map (should be True) + + [)>] + let ``TaskSeq-contains happy path last item of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.contains 10 + |> Task.map (should be True) + +module SideEffects = + [)>] + let ``TaskSeq-contains KeyNotFoundException only sometimes for mutated state`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + + // first: false + let! found = TaskSeq.contains 11 ts + found |> should be False + + // find again: found now, because of side effects + let! found = TaskSeq.contains 11 ts + found |> should be True + + // find once more: false + let! found = TaskSeq.contains 11 ts + found |> should be False + } + + [] + let ``TaskSeq-contains _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.contains 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.contains 4 + found |> should be True + i |> should equal 4 // only partial evaluation! + } + + [] + let ``TaskSeq-contains _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.contains 42 + found |> should be True + i |> should equal 0 // because no MoveNext after found item, the last statements are not executed + } + + [] + let ``TaskSeq-contains _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.contains 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.contains 4 + found |> should be True + i |> should equal 4 // only partial evaluation! + } diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Exists.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Exists.Tests.fs new file mode 100644 index 00000000..a717b18f --- /dev/null +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Exists.Tests.fs @@ -0,0 +1,221 @@ +module FSharpy.Tests.Exists + +open Xunit +open FsUnit.Xunit +open FsToolkit.ErrorHandling + +open FSharpy + +// +// TaskSeq.exists +// TaskSeq.existsAsyncc +// + +module EmptySeq = + [)>] + 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! + } diff --git a/src/FSharpy.TaskSeq/TaskSeq.fs b/src/FSharpy.TaskSeq/TaskSeq.fs index 64cd33c9..1bc469bc 100644 --- a/src/FSharpy.TaskSeq/TaskSeq.fs +++ b/src/FSharpy.TaskSeq/TaskSeq.fs @@ -254,6 +254,18 @@ module TaskSeq = let tryFindIndex predicate source = Internal.tryFindIndex (Predicate predicate) source let tryFindIndexAsync predicate source = Internal.tryFindIndex (PredicateAsync predicate) source + let exists predicate source = + Internal.tryFind (Predicate predicate) source + |> Task.map (Option.isSome) + + let existsAsync predicate source = + Internal.tryFind (PredicateAsync predicate) source + |> Task.map (Option.isSome) + + let contains value source = + Internal.tryFind (Predicate((=) value)) source + |> Task.map (Option.isSome) + let pick chooser source = task { match! Internal.tryPick (TryPick chooser) source with | Some item -> return item diff --git a/src/FSharpy.TaskSeq/TaskSeq.fsi b/src/FSharpy.TaskSeq/TaskSeq.fsi index 493111fa..4ca35ba1 100644 --- a/src/FSharpy.TaskSeq/TaskSeq.fsi +++ b/src/FSharpy.TaskSeq/TaskSeq.fsi @@ -405,10 +405,51 @@ module TaskSeq = /// asynchronous function returns . /// If does not need to be asynchronous, consider using . /// + /// /// Thrown if no element returns when /// evaluated by the function. val findIndexAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task + /// + /// Tests if the sequence contains the specified element. Returns + /// if contains the specified element; + /// otherwise. + /// + /// + /// The value to locate in the input sequence. + /// The input sequence. + /// True if the input sequence contains the specified element; false otherwise. + /// Thrown when the input sequence is null. + val contains<'T when 'T: equality> : value: 'T -> source: taskSeq<'T> -> Task + + /// + /// Tests if any element of the task sequence in satisfies + /// the given . + /// The function is applied to the elements of the input sequence. If any application + /// returns then the overall result is and no further elements are evaluated and tested. + /// Otherwise, is returned. + /// + /// + /// A function to test each item of the input sequence. + /// The input sequence. /// + /// True if any result from the predicate is true; false otherwise. /// + /// Thrown when the input sequence is null. + val exists: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task + + /// + /// Tests if any element of the task sequence in satisfies + /// the given async . + /// The function is applied to the elements of the input sequence. If any application + /// returns then the overall result is and no further elements are evaluated and tested. + /// Otherwise, is returned. + /// + /// + /// A function to test each item of the input sequence. + /// The input sequence. /// + /// True if any result from the predicate is true; false otherwise. /// + /// Thrown when the input sequence is null. + val existsAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task + /// /// Zips two task sequences, returning a taskSeq of the tuples of each sequence, in order. May raise ArgumentException /// if the sequences are or unequal length. diff --git a/src/FSharpy.TaskSeq/TaskSeqInternal.fs b/src/FSharpy.TaskSeq/TaskSeqInternal.fs index 7d3870ef..256ad7ad 100644 --- a/src/FSharpy.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharpy.TaskSeq/TaskSeqInternal.fs @@ -29,7 +29,7 @@ type ChooserAction<'T, 'U, 'TaskOption when 'TaskOption :> Task<'U option>> = | TryPickAsync of async_try_pick: ('T -> 'TaskOption) [] -type PredicateAction<'T, 'U, 'TaskBool when 'TaskBool :> Task> = +type PredicateAction<'T, 'TaskBool when 'TaskBool :> Task> = | Predicate of try_filter: ('T -> bool) | PredicateAsync of async_try_filter: ('T -> 'TaskBool)