From d7a94434a68d8f33b77ca543d7623312ef10fdfe Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Thu, 3 Nov 2022 19:25:33 +0100 Subject: [PATCH 1/2] Implement TaskSeq.indexed + tests & docs --- .../FSharpy.TaskSeq.Test.fsproj | 1 + .../TaskSeq.Indexed.Tests.fs | 34 +++++++++++++++++++ src/FSharpy.TaskSeq/TaskSeq.fs | 13 ++++++- src/FSharpy.TaskSeq/TaskSeq.fsi | 10 ++++++ 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 src/FSharpy.TaskSeq.Test/TaskSeq.Indexed.Tests.fs diff --git a/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj b/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj index 92376960..5e6bc07d 100644 --- a/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj +++ b/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj @@ -21,6 +21,7 @@ + diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Indexed.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Indexed.Tests.fs new file mode 100644 index 00000000..8b38a55a --- /dev/null +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Indexed.Tests.fs @@ -0,0 +1,34 @@ +module FSharpy.Tests.Indexed + +open Xunit +open FsUnit.Xunit +open FsToolkit.ErrorHandling + +open FSharpy + +// +// TaskSeq.indexed +// + +module EmptySeq = + [)>] + let ``TaskSeq-indexed on empty`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.indexed + |> verifyEmpty + +module Immutable = + [] + let ``TaskSeq-indexed starts at zero`` () = + taskSeq { yield 99 } + |> TaskSeq.indexed + |> TaskSeq.head + |> Task.map (should equal (0, 99)) + + [)>] + let ``TaskSeq-indexed`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.indexed + |> TaskSeq.toArrayAsync + |> Task.map (Array.forall (fun (x, y) -> x + 1 = y)) + |> Task.map (should be True) diff --git a/src/FSharpy.TaskSeq/TaskSeq.fs b/src/FSharpy.TaskSeq/TaskSeq.fs index 9e47f186..b07bbcba 100644 --- a/src/FSharpy.TaskSeq/TaskSeq.fs +++ b/src/FSharpy.TaskSeq/TaskSeq.fs @@ -165,7 +165,10 @@ module TaskSeq = let cast source : taskSeq<'T> = Internal.map (SimpleAction(fun (x: obj) -> x :?> 'T)) source let box source = Internal.map (SimpleAction(fun x -> box x)) source - let unbox<'U when 'U: struct> (source: taskSeq) : taskSeq<'U> = Internal.map (SimpleAction(fun x -> unbox x)) source + + let unbox<'U when 'U: struct> (source: taskSeq) : taskSeq<'U> = + Internal.map (SimpleAction(fun x -> unbox x)) source + let iter action source = Internal.iter (SimpleAction action) source let iteri action source = Internal.iter (CountableAction action) source let iterAsync action source = Internal.iter (AsyncSimpleAction action) source @@ -222,6 +225,14 @@ module TaskSeq = | None -> return invalidArg (nameof source) "The input sequence contains more than one element." } + let indexed (source: taskSeq<'T>) = taskSeq { + let mutable i = 0 + + for x in source do + yield i, x + i <- i + 1 + } + let choose chooser source = Internal.choose (TryPick chooser) source let chooseAsync chooser source = Internal.choose (TryPickAsync chooser) source let filter predicate source = Internal.filter (Predicate predicate) source diff --git a/src/FSharpy.TaskSeq/TaskSeq.fsi b/src/FSharpy.TaskSeq/TaskSeq.fsi index 7463d74a..029d7f93 100644 --- a/src/FSharpy.TaskSeq/TaskSeq.fsi +++ b/src/FSharpy.TaskSeq/TaskSeq.fsi @@ -125,6 +125,16 @@ module TaskSeq = /// Maps over the taskSeq, applying the mapper function to each item. This function is non-blocking. val map: mapper: ('T -> 'U) -> source: taskSeq<'T> -> taskSeq<'U> + /// + /// Builds a new task sequence whose elements are the corresponding elements of the input task + /// sequence paired with the integer index (from 0) of each element. + /// Does not evaluate the input sequence until requested. + /// + /// The input task sequence. + /// The resulting task sequence of tuples. + /// Thrown when the input sequence is null. + val indexed: source: taskSeq<'T> -> taskSeq + /// Maps over the taskSeq with an index, applying the mapper function to each item. This function is non-blocking. val mapi: mapper: (int -> 'T -> 'U) -> source: taskSeq<'T> -> taskSeq<'U> From 50b8c0a82031812f9715b44d1175963d62927ac0 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Thu, 3 Nov 2022 20:24:31 +0100 Subject: [PATCH 2/2] Implement TaskSeq.findIndex/tryFindIndex/findIndexAsync/tryFindIndexAsync and add tests, docs --- .../FSharpy.TaskSeq.Test.fsproj | 1 + .../TaskSeq.Find.Tests.fs | 8 +- .../TaskSeq.FindIndex.Tests.fs | 474 ++++++++++++++++++ src/FSharpy.TaskSeq/TaskSeq.fs | 16 + src/FSharpy.TaskSeq/TaskSeq.fsi | 32 ++ src/FSharpy.TaskSeq/TaskSeqInternal.fs | 32 ++ 6 files changed, 559 insertions(+), 4 deletions(-) create mode 100644 src/FSharpy.TaskSeq.Test/TaskSeq.FindIndex.Tests.fs diff --git a/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj b/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj index 5e6bc07d..9c102755 100644 --- a/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj +++ b/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj @@ -18,6 +18,7 @@ + diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Find.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Find.Tests.fs index 3b2abd98..5f69e4ce 100644 --- a/src/FSharpy.TaskSeq.Test/TaskSeq.Find.Tests.fs +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Find.Tests.fs @@ -206,7 +206,7 @@ module SideEffects = found |> should equal 3 i |> should equal 3 // only partial evaluation! - // find next item. We do get a new iterator, but mutable state is now starting at '3' + // 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.find ((=) 4) found |> should equal 4 i |> should equal 4 // only partial evaluation! @@ -226,7 +226,7 @@ module SideEffects = found |> should equal 3 i |> should equal 3 // only partial evaluation! - // find next item. We do get a new iterator, but mutable state is now starting at '3' + // 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.findAsync (fun x -> task { return x = 4 }) found |> should equal 4 i |> should equal 4 @@ -360,7 +360,7 @@ module SideEffects = found |> should equal (Some 3) i |> should equal 3 // only partial evaluation! - // find next item. We do get a new iterator, but mutable state is now starting at '3' + // 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.tryFind ((=) 4) found |> should equal (Some 4) i |> should equal 4 // only partial evaluation! @@ -380,7 +380,7 @@ module SideEffects = found |> should equal (Some 3) i |> should equal 3 // only partial evaluation! - // find next item. We do get a new iterator, but mutable state is now starting at '3' + // 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.tryFindAsync (fun x -> task { return x = 4 }) found |> should equal (Some 4) i |> should equal 4 diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.FindIndex.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.FindIndex.Tests.fs new file mode 100644 index 00000000..e99aac32 --- /dev/null +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.FindIndex.Tests.fs @@ -0,0 +1,474 @@ +module FSharpy.Tests.FindIndex + +open Xunit +open FsUnit.Xunit +open FsToolkit.ErrorHandling + +open FSharpy +open System.Collections.Generic + +// +// TaskSeq.findIndex +// TaskSeq.findIndexAsync +// TaskSeq.tryFindIndex +// TaskSeq.tryFindIndexAsync +// + +module EmptySeq = + [)>] + let ``TaskSeq-findIndex raises KeyNotFoundException`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.findIndex ((=) 12) + |> Task.ignore + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-findIndexAsync raises KeyNotFoundException`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.findIndexAsync (fun x -> task { return x = 12 }) + |> Task.ignore + |> should throwAsyncExact typeof + + + [)>] + let ``TaskSeq-tryFindIndex returns None`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.tryFindIndex ((=) 12) + |> Task.map (should be None') + + [)>] + let ``TaskSeq-tryFindIndexAsync returns None`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.tryFindIndexAsync (fun x -> task { return x = 12 }) + |> Task.map (should be None') + +module Immutable = + [)>] + let ``TaskSeq-findIndex sad path raises KeyNotFoundException`` variant = + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.findIndex ((=) 0) // dummy tasks sequence starts at 1 + |> Task.ignore + + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-findIndexAsync sad path raises KeyNotFoundException`` variant = + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.findIndexAsync (fun x -> task { return x = 0 }) // dummy tasks sequence starts at 1 + |> Task.ignore + + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-findIndex happy path middle of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.findIndex (fun x -> x < 6 && x > 4) + |> Task.map (should equal 4) // zero based + + [)>] + let ``TaskSeq-findIndexAsync happy path middle of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.findIndexAsync (fun x -> task { return x < 6 && x > 4 }) + |> Task.map (should equal 4) + + [)>] + let ``TaskSeq-findIndex happy path first item of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.findIndex ((=) 1) + |> Task.map (should equal 0) // zero based + + [)>] + let ``TaskSeq-findIndexAsync happy path first item of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.findIndexAsync (fun x -> task { return x = 1 }) + |> Task.map (should equal 0) // zero based + + [)>] + let ``TaskSeq-findIndex happy path last item of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.findIndex ((=) 10) + |> Task.map (should equal 9) // zero based + + [)>] + let ``TaskSeq-findIndexAsync happy path last item of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.findIndexAsync (fun x -> task { return x = 10 }) // dummy tasks seq ends at 50 + |> Task.map (should equal 9) // zero based + + + // + // + // tryXXX stuff + // | + // | + // V + + [)>] + let ``TaskSeq-tryFindIndex sad path returns None`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindIndex ((=) 0) + |> Task.map (should be None') + + [)>] + let ``TaskSeq-tryFindIndexAsync sad path return None`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindIndexAsync (fun x -> task { return x = 0 }) + |> Task.map (should be None') + + [)>] + let ``TaskSeq-tryFindIndex happy path middle of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindIndex (fun x -> x < 6 && x > 4) + |> Task.map (should equal (Some 4)) // zero based + + [)>] + let ``TaskSeq-tryFindIndexAsync happy path middle of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindIndexAsync (fun x -> task { return x < 6 && x > 4 }) + |> Task.map (should equal (Some 4)) // zero based + + [)>] + let ``TaskSeq-tryFindIndex happy path first item of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindIndex ((=) 1) + |> Task.map (should equal (Some 0)) // zero based + + [)>] + let ``TaskSeq-tryFindIndexAsync happy path first item of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindIndexAsync (fun x -> task { return x = 1 }) + |> Task.map (should equal (Some 0)) // zero based + + [)>] + let ``TaskSeq-tryFindIndex happy path last item of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindIndex ((=) 10) + |> Task.map (should equal (Some 9)) // zero based + + [)>] + let ``TaskSeq-tryFindIndexAsync happy path last item of seq`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindIndexAsync (fun x -> task { return x = 10 }) + |> Task.map (should equal (Some 9)) // zero based + +module SideEffects = + [)>] + let ``TaskSeq-findIndex KeyNotFoundException only sometimes for mutated state`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + let findIndexer = (=) 11 + + // first: error, item is not there + fun () -> TaskSeq.findIndex findIndexer ts |> Task.ignore + |> should throwAsyncExact typeof + + // findIndex again: no error, because of side effects + let! found = TaskSeq.findIndex findIndexer ts + found |> should equal 0 // zero based, first item in 'updated' sequence is 11 + + // findIndex once more: error, item is not there anymore. + fun () -> TaskSeq.findIndex findIndexer ts |> Task.ignore + |> should throwAsyncExact typeof + } + + [)>] + let ``TaskSeq-findIndexAsync KeyNotFoundException only sometimes for mutated state`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + let findIndexer x = task { return x = 11 } + + // first: error, item is not there + fun () -> TaskSeq.findIndexAsync findIndexer ts |> Task.ignore + |> should throwAsyncExact typeof + + // findIndex again: no error, because of side effects + let! found = TaskSeq.findIndexAsync findIndexer ts + found |> should equal 0 // zero based, first item in 'updated' sequence is 11 + + // findIndex once more: error, item is not there anymore. + fun () -> TaskSeq.findIndexAsync findIndexer ts |> Task.ignore + |> should throwAsyncExact typeof + } + + [] + let ``TaskSeq-findIndex _specialcase_ prove we don't read past the found item`` () = task { + let mutable i = 0 + + let ts = taskSeq { + for x in 10..19 do + i <- i + 1 + yield x + } + + let! found = ts |> TaskSeq.findIndex ((=) 13) + found |> should equal 3 + i |> should equal 4 // only partial evaluation! + + // findIndex next item. We do get a new iterator, but mutable state is now starting at '4' + let! found = ts |> TaskSeq.findIndex ((=) 14) + found |> should equal 4 + i |> should equal 9 // only partial evaluation! + } + + [] + let ``TaskSeq-findIndexAsync _specialcase_ prove we don't read past the found item`` () = task { + let mutable i = 0 + + let ts = taskSeq { + for x in 10..19 do + i <- i + 1 + yield x + } + + let! found = TaskSeq.findIndexAsync (fun x -> task { return x = 13 }) ts + found |> should equal 3 + i |> should equal 4 // only partial evaluation! + + // findIndex next item. We do get a new iterator, but mutable state is now starting at '4' + let! found = TaskSeq.findIndexAsync (fun x -> task { return x = 14 }) ts + found |> should equal 4 + i |> should equal 9 // started counting again + } + + [] + let ``TaskSeq-findIndex _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.findIndex ((=) 42) + found |> should equal 0 // first item has index 0 + i |> should equal 0 // because no MoveNext after found item, the last statements are not executed + } + + [] + let ``TaskSeq-findIndexAsync _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 = TaskSeq.findIndexAsync (fun x -> task { return x = 42 }) ts + found |> should equal 0 // first item has index 0 + i |> should equal 0 // because no MoveNext after found item, the last statements are not executed + } + + [] + let ``TaskSeq-findIndex _specialcase_ prove statement after yield is not evaluated`` () = task { + let mutable i = 0 + + let ts = taskSeq { + for x in 10..19 do + yield x + i <- i + 1 + } + + let! found = ts |> TaskSeq.findIndex ((=) 10) + found |> should equal 0 + i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated + + // findIndex some next item. We do get a new iterator, but mutable state is now starting at '1' + let! found = ts |> TaskSeq.findIndex ((=) 14) + found |> should equal 4 + i |> should equal 4 // only partial evaluation! + } + + [] + let ``TaskSeq-findIndexAsync _specialcase_ prove statement after yield is not evaluated`` () = task { + let mutable i = 0 + + let ts = taskSeq { + for x in 10..19 do + yield x + i <- i + 1 + } + + let! found = + ts + |> TaskSeq.findIndexAsync (fun x -> task { return x = 10 }) + + found |> should equal 0 + i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated + + // findIndex some next item. We do get a new iterator, but mutable state is now starting at '1' + let! found = + ts + |> TaskSeq.findIndexAsync (fun x -> task { return x = 14 }) + + found |> should equal 4 + i |> should equal 4 // only partial evaluation! + } + + + // + // + // tryXXX stuff + // | + // | + // V + + [)>] + let ``TaskSeq-tryFindIndex KeyNotFoundException only sometimes for mutated state`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + let findIndexer = (=) 11 + + // first: None + let! found = TaskSeq.tryFindIndex findIndexer ts + found |> should be None' + + // findIndex again: found now, because of side effects + let! found = TaskSeq.tryFindIndex findIndexer ts + found |> should equal (Some 0) // item with value '11' is at index 0 in 'updated' sequence + + // findIndex once more: None + let! found = TaskSeq.tryFindIndex findIndexer ts + found |> should be None' + } + + [)>] + let ``TaskSeq-tryFindIndexAsync KeyNotFoundException only sometimes for mutated state`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + let findIndexer x = task { return x = 11 } + + // first: None + let! found = TaskSeq.tryFindIndexAsync findIndexer ts + found |> should be None' + + // findIndex again: found now, because of side effects + let! found = TaskSeq.tryFindIndexAsync findIndexer ts + found |> should equal (Some 0) // item with value '11' is at index 0 in 'updated' sequence + + // findIndex once more: None + let! found = TaskSeq.tryFindIndexAsync findIndexer ts + found |> should be None' + } + + [] + let ``TaskSeq-tryFindIndex _specialcase_ prove we don't read past the found item`` () = task { + let mutable i = 0 + + let ts = taskSeq { + for x in 10..19 do + i <- i + 1 + yield x + } + + let! found = ts |> TaskSeq.tryFindIndex ((=) 13) + found |> should equal (Some 3) + i |> should equal 4 // only partial evaluation! + + // findIndex next item. We do get a new iterator, but mutable state is now starting at '4' + let! found = ts |> TaskSeq.tryFindIndex ((=) 14) + found |> should equal (Some 4) + i |> should equal 9 // only partial evaluation! + } + + [] + let ``TaskSeq-tryFindIndexAsync _specialcase_ prove we don't read past the found item`` () = task { + let mutable i = 0 + + let ts = taskSeq { + for x in 10..19 do + i <- i + 1 + yield x + } + + let! found = TaskSeq.tryFindIndexAsync (fun x -> task { return x = 13 }) ts + + found |> should equal (Some 3) + i |> should equal 4 // only partial evaluation! + + // findIndex next item. We do get a new iterator, but mutable state is now starting at '4' + let! found = TaskSeq.tryFindIndexAsync (fun x -> task { return x = 14 }) ts + + found |> should equal (Some 4) + i |> should equal 9 + } + + [] + let ``TaskSeq-tryFindIndex _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.tryFindIndex ((=) 42) + found |> should equal (Some 0) // first item has index 0 + i |> should equal 0 // because no MoveNext after found item, the last statements are not executed + } + + [] + let ``TaskSeq-tryFindIndexAsync _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.tryFindIndexAsync (fun x -> task { return x = 42 }) + + found |> should equal (Some 0) // first item: idx 0 + i |> should equal 0 // because no MoveNext after found item, the last statements are not executed + } + + [] + let ``TaskSeq-tryFindIndex _specialcase_ prove statement after yield is not evaluated`` () = task { + let mutable i = 0 + + let ts = taskSeq { + for x in 10..19 do + yield x + i <- i + 1 + } + + let! found = ts |> TaskSeq.tryFindIndex ((=) 10) + found |> should equal (Some 0) + i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated + + // findIndex some next item. We do get a new iterator, but mutable state is now starting at '1' + let! found = ts |> TaskSeq.tryFindIndex ((=) 14) + found |> should equal (Some 4) + i |> should equal 4 // only partial evaluation! + } + + [] + let ``TaskSeq-tryFindIndexAsync _specialcase_ prove statement after yield is not evaluated`` () = task { + let mutable i = 0 + + let ts = taskSeq { + for x in 10..19 do + yield x + i <- i + 1 + } + + let! found = + ts + |> TaskSeq.tryFindIndexAsync (fun x -> task { return x = 10 }) + + found |> should equal (Some 0) + i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated + + // findIndex some next item. We do get a new iterator, but mutable state is now starting at '1' + let! found = + ts + |> TaskSeq.tryFindIndexAsync (fun x -> task { return x = 14 }) + + found |> should equal (Some 4) + i |> should equal 4 // only partial evaluation! + } diff --git a/src/FSharpy.TaskSeq/TaskSeq.fs b/src/FSharpy.TaskSeq/TaskSeq.fs index b07bbcba..3bb1a9c7 100644 --- a/src/FSharpy.TaskSeq/TaskSeq.fs +++ b/src/FSharpy.TaskSeq/TaskSeq.fs @@ -241,6 +241,8 @@ module TaskSeq = let tryPickAsync chooser source = Internal.tryPick (TryPickAsync chooser) source let tryFind predicate source = Internal.tryFind (Predicate predicate) source let tryFindAsync predicate source = Internal.tryFind (PredicateAsync predicate) source + let tryFindIndex predicate source = Internal.tryFindIndex (Predicate predicate) source + let tryFindIndexAsync predicate source = Internal.tryFindIndex (PredicateAsync predicate) source let pick chooser source = task { match! Internal.tryPick (TryPick chooser) source with @@ -266,6 +268,20 @@ module TaskSeq = | None -> return Internal.raiseNotFound () } + let findIndex predicate source = task { + match! Internal.tryFindIndex (Predicate predicate) source with + | Some item -> return item + | None -> return Internal.raiseNotFound () + } + + let findIndexAsync predicate source = task { + match! Internal.tryFindIndex (PredicateAsync predicate) source with + | Some item -> return item + | None -> return Internal.raiseNotFound () + } + + + // // zip/unzip etc functions // diff --git a/src/FSharpy.TaskSeq/TaskSeq.fsi b/src/FSharpy.TaskSeq/TaskSeq.fsi index 029d7f93..4841289d 100644 --- a/src/FSharpy.TaskSeq/TaskSeq.fsi +++ b/src/FSharpy.TaskSeq/TaskSeq.fsi @@ -265,6 +265,20 @@ module TaskSeq = /// val tryFindAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task<'T option> + /// + /// Returns the index, starting from zero, of the task sequence in for which the given function + /// returns . Returns if no such element exists. + /// If is asynchronous, consider using . + /// + val tryFindIndex: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task + + /// + /// Returns the index, starting from zero, of the task sequence in for which the given asynchronous function + /// returns . Returns if no such element exists. + /// If does not need to be asynchronous, consider using . + /// + val tryFindIndexAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task + /// /// Applies the given function to successive elements of the task sequence @@ -302,6 +316,24 @@ module TaskSeq = /// evaluated by the function. val findAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task<'T> + /// + /// Returns the index, starting from zero, of the first element of the task sequence in for which + /// the given function returns . + /// If is asynchronous, consider using . + /// + /// Thrown if no element returns when + /// evaluated by the function. + val findIndex: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task + + /// + /// Returns the index, starting from zero, of the task sequence in for which the given + /// 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 + /// /// 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 9acfd42a..5d804b12 100644 --- a/src/FSharpy.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharpy.TaskSeq/TaskSeqInternal.fs @@ -374,6 +374,38 @@ module internal TaskSeqInternal = return foundItem } + let tryFindIndex predicate (source: taskSeq<_>) = task { + use e = source.GetAsyncEnumerator(CancellationToken()) + + let mutable go = true + let mutable isFound = false + let mutable index = -1 + let! step = e.MoveNextAsync() + go <- step + + match predicate with + | Predicate predicate -> + while go && not isFound do + index <- index + 1 + isFound <- predicate e.Current + + if not isFound then + let! step = e.MoveNextAsync() + go <- step + + | PredicateAsync predicate -> + while go && not isFound do + index <- index + 1 + let! predicateResult = predicate e.Current + isFound <- predicateResult + + if not isFound then + let! step = e.MoveNextAsync() + go <- step + + if isFound then return Some index else return None + } + let choose chooser (source: taskSeq<_>) = taskSeq { match chooser with | TryPick picker ->