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 4c28722..ff7ae5e 100644
--- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
+++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
@@ -27,6 +27,7 @@
+
@@ -36,6 +37,7 @@
+
@@ -43,6 +45,7 @@
+
diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.InsertAt.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.InsertAt.Tests.fs
new file mode 100644
index 0000000..ad8a8c8
--- /dev/null
+++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.InsertAt.Tests.fs
@@ -0,0 +1,548 @@
+module TaskSeq.Tests.InsertAt
+
+open System
+
+open Xunit
+open FsUnit.Xunit
+
+open FSharp.Control
+
+
+//
+// TaskSeq.insertAt
+// TaskSeq.insertManyAt
+//
+
+exception SideEffectPastEnd of string
+
+module EmptySeq =
+ [)>]
+ let ``TaskSeq-insertAt(0) on empty input returns singleton`` variant =
+ Gen.getEmptyVariant variant
+ |> TaskSeq.insertAt 0 42
+ |> verifySingleton 42
+
+ [)>]
+ let ``TaskSeq-insertAt(1) on empty input should throw ArgumentException`` variant =
+ fun () ->
+ Gen.getEmptyVariant variant
+ |> TaskSeq.insertAt 1 42
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ []
+ let ``TaskSeq-insertAt(-1) should throw ArgumentException on any input`` () =
+ fun () ->
+ TaskSeq.empty
+ |> TaskSeq.insertAt -1 42
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ TaskSeq.init 10 id
+ |> TaskSeq.insertAt -1 42
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ []
+ let ``TaskSeq-insertAt(-1) should throw ArgumentException before awaiting`` () =
+ fun () ->
+ taskSeq {
+ do! longDelay ()
+
+ if false then
+ yield 0 // type inference
+ }
+ |> TaskSeq.insertAt -1 42
+ |> ignore // throws even without running the async. Bad coding, don't ignore a task!
+
+ // test without awaiting the async
+ |> should throw typeof
+
+ [)>]
+ let ``TaskSeq-insertManyAt(0) on empty input returns singleton`` variant =
+ Gen.getEmptyVariant variant
+ |> TaskSeq.insertManyAt 0 (TaskSeq.ofArray [| 42; 43; 44 |])
+ |> verifyDigitsAsString "jkl"
+
+ [)>]
+ let ``TaskSeq-insertManyAt(1) on empty input should throw InvalidOperation`` variant =
+ fun () ->
+ Gen.getEmptyVariant variant
+ |> TaskSeq.insertManyAt 1 (TaskSeq.ofArray [| 42; 43; 44 |])
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ []
+ let ``TaskSeq-insertManyAt(-1) should throw ArgumentException on any input`` () =
+ fun () ->
+ TaskSeq.empty
+ |> TaskSeq.insertManyAt -1 (TaskSeq.ofArray [| 42; 43; 44 |])
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ TaskSeq.init 10 id
+ |> TaskSeq.insertManyAt -1 (TaskSeq.ofArray [| 42; 43; 44 |])
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ []
+ let ``TaskSeq-insertManyAt(-1) should throw ArgumentException before awaiting`` () =
+ fun () ->
+ taskSeq {
+ do! longDelay ()
+
+ if false then
+ yield 0 // type inference
+ }
+ |> TaskSeq.insertManyAt -1 (TaskSeq.ofArray [| 42; 43; 44 |])
+ |> ignore // throws even without running the async. Bad coding, don't ignore a task!
+
+ // test without awaiting the async
+ |> should throw typeof
+
+ []
+ let ``TaskSeq-insertManyAt() with empty sequenc as source`` () =
+ TaskSeq.empty
+ |> TaskSeq.insertManyAt 0 TaskSeq.empty
+ |> verifyEmpty
+
+ []
+ let ``TaskSeq-insertManyAt() with empty sequence as source applies to non-empty sequence`` () =
+ TaskSeq.init 10 id
+ |> TaskSeq.insertManyAt 2 TaskSeq.empty
+ |> verify0To9
+
+module Immutable =
+ [)>]
+ let ``TaskSeq-insertAt can insert after end of sequence`` variant = task {
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertAt 10 99
+ |> verifyDigitsAsString "ABCDEFGHIJ£"
+ }
+
+ [)>]
+ let ``TaskSeq-insertAt inserts item immediately after the indexed position`` variant = task {
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertAt 0 99
+ |> verifyDigitsAsString "£ABCDEFGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertAt 1 99
+ |> verifyDigitsAsString "A£BCDEFGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertAt 5 99
+ |> verifyDigitsAsString "ABCDE£FGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertAt 10 99
+ |> verifyDigitsAsString "ABCDEFGHIJ£"
+ }
+
+
+ [)>]
+ let ``TaskSeq-insertAt can be repeated in a chain`` variant = task {
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertAt 0 99
+ |> TaskSeq.insertAt 0 99
+ |> TaskSeq.insertAt 0 99
+ |> TaskSeq.insertAt 0 99
+ |> TaskSeq.insertAt 0 99
+ |> verifyDigitsAsString "£££££ABCDEFGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertAt 10 99
+ |> TaskSeq.insertAt 11 99
+ |> TaskSeq.insertAt 12 99
+ |> TaskSeq.insertAt 13 99
+ |> TaskSeq.insertAt 14 99
+ |> verifyDigitsAsString "ABCDEFGHIJ£££££"
+ }
+
+ [)>]
+ let ``TaskSeq-insertAt applies to a position in the new sequence`` variant = task {
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertAt 0 99
+ |> TaskSeq.insertAt 2 99
+ |> TaskSeq.insertAt 4 99
+ |> TaskSeq.insertAt 6 99
+ |> TaskSeq.insertAt 8 99
+ |> TaskSeq.insertAt 10 99
+ |> TaskSeq.insertAt 12 99
+ |> TaskSeq.insertAt 14 99
+ |> TaskSeq.insertAt 16 99
+ |> TaskSeq.insertAt 18 99
+ |> TaskSeq.insertAt 20 99
+ |> verifyDigitsAsString "£A£B£C£D£E£F£G£H£I£J£"
+ }
+
+ []
+ let ``TaskSeq-insertAt can be applied to an infinite task sequence`` () =
+ TaskSeq.initInfinite id
+ |> TaskSeq.insertAt 100 12345
+ |> TaskSeq.item 100
+ |> Task.map (should equal 12345)
+
+
+ [)>]
+ let ``TaskSeq-insertAt throws when there are not enough elements`` variant =
+ fun () ->
+ TaskSeq.singleton 1
+ // insert after 1
+ |> TaskSeq.insertAt 2 99
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertAt 11 99
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertAt 10_000_000 99
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ [)>]
+ let ``TaskSeq-insertManyAt can insert after end of sequence`` variant = task {
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertManyAt 10 (TaskSeq.ofArray [| 99; 100; 101 |])
+ |> verifyDigitsAsString "ABCDEFGHIJ£¤¥"
+ }
+
+ [)>]
+ let ``TaskSeq-insertManyAt inserts item immediately after the indexed position`` variant = task {
+ let values = TaskSeq.ofArray [| 99; 100; 101 |]
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertManyAt 0 values
+ |> verifyDigitsAsString "£¤¥ABCDEFGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertManyAt 1 values
+ |> verifyDigitsAsString "A£¤¥BCDEFGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertManyAt 5 values
+ |> verifyDigitsAsString "ABCDE£¤¥FGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertManyAt 10 values
+ |> verifyDigitsAsString "ABCDEFGHIJ£¤¥"
+ }
+
+
+ [)>]
+ let ``TaskSeq-insertManyAt can be repeated in a chain`` variant = task {
+ let values = TaskSeq.ofArray [| 99; 100; 101 |]
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertManyAt 0 values
+ |> TaskSeq.insertManyAt 0 values
+ |> TaskSeq.insertManyAt 0 values
+ |> TaskSeq.insertManyAt 0 values
+ |> TaskSeq.insertManyAt 0 values
+ |> verifyDigitsAsString "£¤¥£¤¥£¤¥£¤¥£¤¥ABCDEFGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertManyAt 10 values
+ |> TaskSeq.insertManyAt 11 values
+ |> TaskSeq.insertManyAt 12 values
+ |> TaskSeq.insertManyAt 13 values
+ |> TaskSeq.insertManyAt 14 values
+ |> verifyDigitsAsString "ABCDEFGHIJ£££££¤¥¤¥¤¥¤¥¤¥"
+ }
+
+ [)>]
+ let ``TaskSeq-insertManyAt applies to a position in the new sequence`` variant = task {
+ let values = TaskSeq.ofArray [| 99; 100; 101 |]
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertManyAt 0 values
+ |> TaskSeq.insertManyAt 4 values
+ |> TaskSeq.insertManyAt 8 values
+ |> TaskSeq.insertManyAt 12 values
+ |> TaskSeq.insertManyAt 16 values
+ |> TaskSeq.insertManyAt 20 values
+ |> TaskSeq.insertManyAt 24 values
+ |> TaskSeq.insertManyAt 28 values
+ |> TaskSeq.insertManyAt 32 values
+ |> TaskSeq.insertManyAt 36 values
+ |> TaskSeq.insertManyAt 40 values
+ |> verifyDigitsAsString "£¤¥A£¤¥B£¤¥C£¤¥D£¤¥E£¤¥F£¤¥G£¤¥H£¤¥I£¤¥J£¤¥"
+ }
+
+ []
+ let ``TaskSeq-insertManyAt (infinite) can be applied to an infinite task sequence`` () =
+ TaskSeq.initInfinite id
+ |> TaskSeq.insertManyAt 100 (TaskSeq.init 10 id)
+ |> TaskSeq.item 109
+ |> Task.map (should equal 9)
+
+
+
+ []
+ let ``TaskSeq-insertManyAt (infinite) with infinite task sequence as argument`` () =
+ TaskSeq.init 100 id
+ |> TaskSeq.insertManyAt 100 (TaskSeq.initInfinite id)
+ |> TaskSeq.item 1999
+ |> Task.map (should equal 1899) // the inserted infinite sequence started at 100, with value 0.
+
+ []
+ let ``TaskSeq-insertManyAt (infinite) with source and values both as infinite task sequence`` () = task {
+
+ // using two infinite task sequences
+ let ts =
+ TaskSeq.initInfinite id
+ |> TaskSeq.insertManyAt 1000 (TaskSeq.initInfinite id)
+
+ // the inserted infinite sequence started at 1000, with value 0.
+ do! ts |> TaskSeq.item 999 |> Task.map (should equal 999)
+ do! ts |> TaskSeq.item 1000 |> Task.map (should equal 0)
+ do! ts |> TaskSeq.item 2000 |> Task.map (should equal 1000)
+ }
+
+ [)>]
+ let ``TaskSeq-insertManyAt throws when there are not enough elements`` variant =
+ let values = TaskSeq.ofArray [| 99; 100; 101 |]
+
+ fun () ->
+ TaskSeq.singleton 1
+ // insert after 1
+ |> TaskSeq.insertManyAt 2 values
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertManyAt 11 values
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ Gen.getSeqImmutable variant
+ |> TaskSeq.insertManyAt 10_000_000 values
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+
+
+module SideEffects =
+
+ // PoC test
+ []
+ let ``Seq-insertAt (poc-proof) will execute side effect before index`` () =
+ // NOTE: this test is for documentation purposes only, to show this behavior that is tested in this module
+ // this shows that Seq.insertAt executes more side effects than necessary.
+
+ let mutable x = 42
+
+ let items = seq {
+ x <- x + 1 // we are proving this gets executed with insertAt(0)
+ yield x
+ yield x * 2
+ }
+
+ items
+ |> Seq.insertAt 0 99
+ |> Seq.item 0 // put enumerator to inserted item
+ |> ignore
+
+ x |> should equal 43 // one time side effect executed. QED
+
+ []
+ let ``TaskSeq-insertAt(0) will execute side effects at start of sequence`` () =
+ // NOTE: while not strictly necessary, this mirrors behavior of Seq.insertAt
+
+ let mutable x = 42 // for this test, the potential mutation should not actually occur
+
+ let items = taskSeq {
+ x <- x + 1 // this is executed even with insertAt(0)
+ yield x
+ yield x * 2
+ }
+
+ items
+ |> TaskSeq.insertAt 0 99
+ |> TaskSeq.item 0 // consume only the first item
+ |> Task.map (should equal 99)
+ |> Task.map (fun () -> x |> should equal 43) // the mutable was updated
+
+ []
+ let ``TaskSeq-insertAt will execute last side effect when inserting past end`` () =
+ let mutable x = 42
+
+ let items = taskSeq {
+ yield x
+ yield x * 2
+ yield x * 4
+ x <- x + 1 // this is executed when inserting past last item
+ }
+
+ items
+ |> TaskSeq.insertAt 3 99
+ |> TaskSeq.item 3
+ |> Task.map (should equal 99)
+ |> Task.map (fun () -> x |> should equal 43) // as with 'seq', see first test in this block, we execute the side effect at index
+
+
+ []
+ let ``TaskSeq-insertAt will execute side effect just before index`` () =
+ let mutable x = 42
+
+ let items = taskSeq {
+ yield x
+ x <- x + 1 // this is executed, even though we insert after the first item
+ yield x * 2
+ yield x * 4
+ }
+
+ items
+ |> TaskSeq.insertAt 1 99
+ |> TaskSeq.item 1
+ |> Task.map (should equal 99)
+ |> Task.map (fun () -> x |> should equal 43) // as with 'seq', see first test in this block, we execute the side effect at index
+
+ []
+ let ``TaskSeq-insertAt exception at insertion index is thrown`` () =
+ fun () ->
+ taskSeq {
+ yield 1
+ yield! [ 2; 3 ]
+ do SideEffectPastEnd "at the end" |> raise // this is raised
+ yield 4
+ }
+ |> TaskSeq.insertAt 3 99
+ |> TaskSeq.item 3
+ |> Task.ignore
+
+ |> should throwAsyncExact typeof
+
+ []
+ let ``TaskSeq-insertAt prove that an exception from the taskSeq is thrown instead of exception from function`` () =
+ let items = taskSeq {
+ yield 42
+ yield! [ 1; 2 ]
+ do SideEffectPastEnd "at the end" |> raise // we SHOULD get here before ArgumentException is raised
+ }
+
+ fun () -> items |> TaskSeq.insertAt 4 99 |> consumeTaskSeq // this would raise ArgumentException normally, but not now
+ |> should throwAsyncExact typeof
+
+ []
+ let ``TaskSeq-insertManyAt(0) will execute side effects at start of sequence`` () =
+ // NOTE: while not strictly necessary, this mirrors behavior of Seq.insertManyAt
+
+ let mutable x = 42 // for this test, the potential mutation should not actually occur
+
+ let items = taskSeq {
+ x <- x + 1 // this is executed even with insertManyAt(0)
+ yield x
+ yield x * 2
+ }
+
+ items
+ |> TaskSeq.insertManyAt 0 (taskSeq { yield! [ 99; 100 ] })
+ |> TaskSeq.item 0 // consume only the first item
+ |> Task.map (should equal 99)
+ |> Task.map (fun () -> x |> should equal 43) // the mutable was updated
+
+ []
+ let ``TaskSeq-insertManyAt will execute last side effect when inserting past end`` () =
+ let mutable x = 42
+
+ let items = taskSeq {
+ yield x
+ yield x * 2
+ yield x * 4
+ x <- x + 1 // this is executed when inserting past last item
+ }
+
+ items
+ |> TaskSeq.insertManyAt 3 (taskSeq { yield! [ 99; 100 ] })
+ |> TaskSeq.item 3
+ |> Task.map (should equal 99)
+ |> Task.map (fun () -> x |> should equal 43) // as with 'seq', see first test in this block, we execute the side effect at index
+
+
+ []
+ let ``TaskSeq-insertManyAt will execute side effect just before index`` () =
+ let mutable x = 42
+
+ let items = taskSeq {
+ yield x
+ x <- x + 1 // this is executed, even though we insert after the first item
+ yield x * 2
+ yield x * 4
+ }
+
+ items
+ |> TaskSeq.insertManyAt 1 (taskSeq { yield! [ 99; 100 ] })
+ |> TaskSeq.item 1
+ |> Task.map (should equal 99)
+ |> Task.map (fun () -> x |> should equal 43) // as with 'seq', see first test in this block, we execute the side effect at index
+
+ []
+ let ``TaskSeq-insertManyAt exception at insertion index is thrown`` () =
+ fun () ->
+ taskSeq {
+ yield 1
+ yield! [ 2; 3 ]
+ do SideEffectPastEnd "at the end" |> raise // this is raised
+ yield 4
+ }
+ |> TaskSeq.insertManyAt 3 (taskSeq { yield! [ 99; 100 ] })
+ |> TaskSeq.item 3
+ |> Task.ignore
+
+ |> should throwAsyncExact typeof
+
+ []
+ let ``TaskSeq-insertManyAt prove that an exception from the taskSeq is thrown instead of exception from function`` () =
+ let items = taskSeq {
+ yield 42
+ yield! [ 1; 2 ]
+ do SideEffectPastEnd "at the end" |> raise // we SHOULD get here before ArgumentException is raised
+ }
+
+ fun () ->
+ items
+ |> TaskSeq.insertManyAt 4 (taskSeq { yield! [ 99; 100 ] })
+ |> consumeTaskSeq // this would raise ArgumentException normally, but not now
+
+ |> should throwAsyncExact typeof
diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.RemoveAt.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.RemoveAt.Tests.fs
new file mode 100644
index 0000000..9c60052
--- /dev/null
+++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.RemoveAt.Tests.fs
@@ -0,0 +1,292 @@
+module TaskSeq.Tests.RemoveAt
+
+open System
+
+open Xunit
+open FsUnit.Xunit
+
+open FSharp.Control
+
+
+//
+// TaskSeq.removeAt
+// TaskSeq.removeManyAt
+//
+
+exception SideEffectPastEnd of string
+
+module EmptySeq =
+ [)>]
+ let ``TaskSeq-removeAt(0) on empty input raises`` variant =
+ fun () ->
+ Gen.getEmptyVariant variant
+ |> TaskSeq.removeAt 0
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ [)>]
+ let ``TaskSeq-removeManyAt(0) on empty input raises`` variant =
+ fun () ->
+ Gen.getEmptyVariant variant
+ |> TaskSeq.removeManyAt 0 0
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ [)>]
+ let ``TaskSeq-removeAt(-1) on empty input should throw ArgumentException without consuming`` variant =
+ fun () ->
+ Gen.getEmptyVariant variant
+ |> TaskSeq.removeAt -1
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ fun () -> Gen.getEmptyVariant variant |> TaskSeq.removeAt -1 |> ignore // task is not awaited
+
+ |> should throw typeof
+
+ [)>]
+ let ``TaskSeq-removeManyAt(-1) on empty input should throw ArgumentException without consuming`` variant =
+ fun () ->
+ Gen.getEmptyVariant variant
+ |> TaskSeq.removeManyAt -1 0
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ Gen.getEmptyVariant variant
+ |> TaskSeq.removeManyAt -1 0
+ |> ignore
+
+ |> should throw typeof
+
+module Immutable =
+ [)>]
+ let ``TaskSeq-removeAt can remove last item`` variant = task {
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeAt 9
+ |> verifyDigitsAsString "ABCDEFGHI"
+ }
+
+ [)>]
+ let ``TaskSeq-removeAt removes the item at indexed positions`` variant = task {
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeAt 0
+ |> verifyDigitsAsString "BCDEFGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeAt 1
+ |> verifyDigitsAsString "ACDEFGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeAt 5
+ |> verifyDigitsAsString "ABCDEGHIJ"
+
+ }
+
+
+ [)>]
+ let ``TaskSeq-removeAt can be repeated in a chain`` variant = task {
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeAt 0
+ |> TaskSeq.removeAt 0
+ |> TaskSeq.removeAt 0
+ |> TaskSeq.removeAt 0
+ |> TaskSeq.removeAt 0
+ |> verifyDigitsAsString "FGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeAt 9
+ |> TaskSeq.removeAt 8
+ |> TaskSeq.removeAt 7
+ |> TaskSeq.removeAt 6
+ |> TaskSeq.removeAt 5 // sequence gets shorter, pick last
+ |> verifyDigitsAsString "ABCDE"
+ }
+
+ []
+ let ``TaskSeq-removeAt can be applied to an infinite task sequence`` () =
+ TaskSeq.initInfinite id
+ |> TaskSeq.removeAt 10_000
+ |> TaskSeq.item 10_000
+ |> Task.map (should equal 10_001)
+
+
+ [)>]
+ let ``TaskSeq-removeAt throws when there are not enough elements`` variant =
+ fun () ->
+ TaskSeq.singleton 1
+ // remove after 1
+ |> TaskSeq.removeAt 2
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeAt 10
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeAt 10_000_000
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ [)>]
+ let ``TaskSeq-removeManyAt can remove last item`` variant = task {
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeManyAt 9 1
+ |> verifyDigitsAsString "ABCDEFGHI"
+ }
+
+ [)>]
+ let ``TaskSeq-removeManyAt can remove multiple items`` variant = task {
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeManyAt 1 5
+ |> verifyDigitsAsString "AGHIJ"
+ }
+
+ [)>]
+ let ``TaskSeq-removeManyAt can with a large count past the end of the sequence is fine`` variant = task {
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeManyAt 2 20_000 // try to remove too many is fine, like Seq.removeManyAt (regardless the docs at time of writing)
+ |> verifyDigitsAsString "AB"
+ }
+
+ [)>]
+ let ``TaskSeq-removeManyAt does not remove any item when count is zero`` variant = task {
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeManyAt 9 0
+ |> verifyDigitsAsString "ABCDEFGHIJ"
+ }
+
+ [)>]
+ let ``TaskSeq-removeManyAt does not remove any item when count is negative`` variant = task {
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeManyAt 1 -99
+ |> verifyDigitsAsString "ABCDEFGHIJ"
+ }
+
+ [)>]
+ let ``TaskSeq-removeManyAt removes items at indexed positions`` variant = task {
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeManyAt 0 5
+ |> verifyDigitsAsString "FGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeManyAt 1 3
+ |> verifyDigitsAsString "AEFGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeManyAt 5 5
+ |> verifyDigitsAsString "ABCDE"
+
+ }
+
+
+ [)>]
+ let ``TaskSeq-removeManyAt can be repeated in a chain`` variant = task {
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeManyAt 0 1
+ |> TaskSeq.removeManyAt 0 2
+ |> TaskSeq.removeManyAt 0 3
+ |> verifyDigitsAsString "GHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeManyAt 9 1 // pick last, result ABCDEFGHIJ
+ |> TaskSeq.removeManyAt 6 1 // then from 6th pos, result ABCDEFHI
+ |> TaskSeq.removeManyAt 3 2 // from 3rd pos take 2, result ABCFHI
+ |> TaskSeq.removeManyAt 0 2 // from start, take 2, result CFHI
+ |> verifyDigitsAsString "CFHI"
+ }
+
+ []
+ let ``TaskSeq-removeManyAt can be applied to an infinite task sequence`` () =
+ TaskSeq.initInfinite id
+ |> TaskSeq.removeManyAt 10_000 5_000
+ |> TaskSeq.item 12_000
+ |> Task.map (should equal 17_000)
+
+
+ [)>]
+ let ``TaskSeq-removeManyAt throws when there are not enough elements for index`` variant =
+ // NOTE: only raises if INDEX is out of bounds, not when COUNT is out of bounds!!!
+
+ fun () ->
+ TaskSeq.singleton 1
+ // remove after 1
+ |> TaskSeq.removeManyAt 2 0 // regardless of count, it should raise
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeManyAt 10 5
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ Gen.getSeqImmutable variant
+ |> TaskSeq.removeManyAt 10_000_000 -5 // even with neg. count
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+
+
+module SideEffects =
+
+ // NOTES:
+ //
+ // no tests, it is not possible to create a meaningful side-effect test, as any consuming after
+ // removing an item would logically require the side effect to be executed the normal way
+
+ // PoC test
+ []
+ let ``Seq-removeAt (poc-proof) will execute side effect before index`` () =
+ // NOTE: this test is for documentation purposes only, to show this behavior that is tested in this module
+ // this shows that Seq.removeAt executes more side effects than necessary.
+
+ let mutable x = 42
+
+ let items = seq {
+ yield x
+ x <- x + 1 // we are proving this gets executed with removeAt(0), BUT this is the result of Seq.item
+ yield x * 2
+ }
+
+ items
+ |> Seq.removeAt 0
+ |> Seq.item 0 // consume anything (this is why there's nothing to test with Seq.removeAt, as this is always true after removing an item)
+ |> ignore
+
+ x |> should equal 43 // one time side effect executed. QED
diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.UpdateAt.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.UpdateAt.Tests.fs
new file mode 100644
index 0000000..0bf7e9d
--- /dev/null
+++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.UpdateAt.Tests.fs
@@ -0,0 +1,254 @@
+module TaskSeq.Tests.UpdateAt
+
+open System
+
+open Xunit
+open FsUnit.Xunit
+
+open FSharp.Control
+
+
+//
+// TaskSeq.updateAt
+//
+
+exception SideEffectPastEnd of string
+
+module EmptySeq =
+ [)>]
+ let ``TaskSeq-updateAt(0) on empty input should throw ArgumentException`` variant =
+ fun () ->
+ Gen.getEmptyVariant variant
+ |> TaskSeq.updateAt 0 42
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ []
+ let ``TaskSeq-updateAt(-1) should throw ArgumentException on any input`` () =
+ fun () ->
+ TaskSeq.empty
+ |> TaskSeq.updateAt -1 42
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ TaskSeq.init 10 id
+ |> TaskSeq.updateAt -1 42
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ []
+ let ``TaskSeq-updateAt(-1) should throw ArgumentException before awaiting`` () =
+ fun () ->
+ taskSeq {
+ do! longDelay ()
+
+ if false then
+ yield 0 // type inference
+ }
+ |> TaskSeq.updateAt -1 42
+ |> ignore // throws even without running the async. Bad coding, don't ignore a task!
+
+ // test without awaiting the async
+ |> should throw typeof
+
+module Immutable =
+ [)>]
+ let ``TaskSeq-updateAt can update at end of sequence`` variant = task {
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.updateAt 9 99
+ |> verifyDigitsAsString "ABCDEFGHI£"
+ }
+
+ [)>]
+ let ``TaskSeq-updateAt past end of sequence throws ArgumentException`` variant =
+ fun () ->
+ Gen.getSeqImmutable variant
+ |> TaskSeq.updateAt 10 99
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ [)>]
+ let ``TaskSeq-updateAt updates item immediately after the indexed position`` variant = task {
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.updateAt 0 99
+ |> verifyDigitsAsString "£BCDEFGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.updateAt 1 99
+ |> verifyDigitsAsString "A£CDEFGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.updateAt 5 99
+ |> verifyDigitsAsString "ABCDE£GHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.updateAt 9 99
+ |> verifyDigitsAsString "ABCDEFGHI£"
+ }
+
+
+ [)>]
+ let ``TaskSeq-updateAt can be repeated in a chain`` variant = task {
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.updateAt 0 99
+ |> TaskSeq.updateAt 1 99
+ |> TaskSeq.updateAt 2 99
+ |> TaskSeq.updateAt 3 99
+ |> TaskSeq.updateAt 4 99
+ |> verifyDigitsAsString "£££££FGHIJ"
+
+ do!
+ Gen.getSeqImmutable variant
+ |> TaskSeq.updateAt 9 99
+ |> TaskSeq.updateAt 8 99
+ |> TaskSeq.updateAt 6 99
+ |> TaskSeq.updateAt 4 99
+ |> TaskSeq.updateAt 2 99
+ |> verifyDigitsAsString "AB£D£F£H££"
+ }
+
+
+ []
+ let ``TaskSeq-updateAt can be applied to an infinite task sequence`` () =
+ TaskSeq.initInfinite id
+ |> TaskSeq.updateAt 1_000_000 12345
+ |> TaskSeq.item 1_000_000
+ |> Task.map (should equal 12345)
+
+
+ [)>]
+ let ``TaskSeq-updateAt throws when there are not enough elements`` variant =
+ fun () ->
+ TaskSeq.singleton 1
+ // update after 1
+ |> TaskSeq.updateAt 2 99
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ Gen.getSeqImmutable variant
+ |> TaskSeq.updateAt 10 99
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ Gen.getSeqImmutable variant
+ |> TaskSeq.updateAt 10_000_000 99
+ |> consumeTaskSeq
+
+ |> should throwAsyncExact typeof
+
+
+module SideEffects =
+
+ // PoC test
+ []
+ let ``Seq-updateAt (poc-proof) will NOT execute side effect just after index`` () =
+ // NOTE: this test is for documentation purposes only, to show this behavior that is tested in this module
+ // this shows that Seq.updateAt executes no extra side effects.
+
+ let mutable x = 42
+
+ let items = seq {
+ yield x
+ x <- x + 1 // we are proving this gets executed with updateAt(0)
+ yield x * 2
+ }
+
+ items
+ |> Seq.updateAt 0 99
+ |> Seq.item 0 // put enumerator to updated item
+ |> ignore
+
+ x |> should equal 42 // one time side effect executed. QED
+
+ []
+ let ``TaskSeq-updateAt(0) will execute side effects at start of sequence`` () =
+ // NOTE: while not strictly necessary, this mirrors behavior of Seq.updateAt
+
+ let mutable x = 42 // for this test, the potential mutation should not actually occur
+
+ let items = taskSeq {
+ x <- x + 1 // this is executed even with updateAt(0)
+ yield x
+ yield x * 2
+ }
+
+ items
+ |> TaskSeq.updateAt 0 99
+ |> TaskSeq.item 0 // consume only the first item
+ |> Task.map (should equal 99)
+ |> Task.map (fun () -> x |> should equal 43) // the mutable was updated
+
+ []
+ let ``TaskSeq-updateAt will NOT execute last side effect when inserting past end`` () =
+ let mutable x = 42
+
+ let items = taskSeq {
+ yield x
+ yield x * 2
+ yield x * 4
+ x <- x + 1 // this is executed when inserting past last item
+ }
+
+ items
+ |> TaskSeq.updateAt 2 99
+ |> TaskSeq.item 2
+ |> Task.map (should equal 99)
+ |> Task.map (fun () -> x |> should equal 42) // as with 'seq', see first test in this block, we prove NO SIDE EFFECTS
+
+
+ []
+ let ``TaskSeq-updateAt will NOT execute side effect just before index`` () =
+ let mutable x = 42
+
+ let items = taskSeq {
+ yield x
+ x <- x + 1 // this is executed, even though we insert after the first item
+ yield x * 2
+ yield x * 4
+ }
+
+ items
+ |> TaskSeq.updateAt 0 99
+ |> TaskSeq.item 0
+ |> Task.map (should equal 99)
+ |> Task.map (fun () -> x |> should equal 42) // as with 'seq', see first test in this block, we prove NO SIDE EFFECTS
+
+ []
+ let ``TaskSeq-updateAt exception at update index is NOT thrown`` () =
+ taskSeq {
+ yield 1
+ yield! [ 2; 3 ]
+ do SideEffectPastEnd "at the end" |> raise // this is NOT raised
+ yield 4
+ }
+ |> TaskSeq.updateAt 2 99
+ |> TaskSeq.item 2
+ |> Task.map (should equal 99)
+
+ []
+ let ``TaskSeq-updateAt prove that an exception from the taskSeq is thrown instead of exception from function`` () =
+ let items = taskSeq {
+ yield 42
+ yield! [ 1; 2 ]
+ do SideEffectPastEnd "at the end" |> raise // we SHOULD get here before ArgumentException is raised
+ }
+
+ fun () -> items |> TaskSeq.updateAt 4 99 |> consumeTaskSeq // this would raise ArgumentException normally, but not now
+ |> should throwAsyncExact typeof
diff --git a/src/FSharp.Control.TaskSeq.Test/TestUtils.fs b/src/FSharp.Control.TaskSeq.Test/TestUtils.fs
index a1488a9..67225b1 100644
--- a/src/FSharp.Control.TaskSeq.Test/TestUtils.fs
+++ b/src/FSharp.Control.TaskSeq.Test/TestUtils.fs
@@ -139,12 +139,24 @@ module TestUtils =
|> TaskSeq.toArrayAsync
|> Task.map (Array.isEmpty >> should be True)
+ /// Verifies that a task sequence contains exactly one item
+ let verifySingleton value ts =
+ ts
+ |> TaskSeq.toArrayAsync
+ |> Task.map (should equal [| value |])
+
/// Verifies that a task sequence contains integers 1-10, by converting to an array and comparing.
let verify1To10 ts =
ts
|> TaskSeq.toArrayAsync
|> Task.map (should equal [| 1..10 |])
+ /// Verifies that a task sequence contains integers 1-10, by converting to an array and comparing.
+ let verify0To9 ts =
+ ts
+ |> TaskSeq.toArrayAsync
+ |> Task.map (should equal [| 0..9 |])
+
/// Turns a sequence of integers into a string, starting with A for '1', Z for 26 etc.
let verifyDigitsAsString expected =
TaskSeq.map (char: int -> char)
diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs
index 520b3a3..357a378 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeq.fs
+++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs
@@ -315,6 +315,12 @@ type TaskSeq private () =
static member tryFindIndex predicate source = Internal.tryFindIndex (Predicate predicate) source
static member tryFindIndexAsync predicate source = Internal.tryFindIndex (PredicateAsync predicate) source
+ static member insertAt index value source = Internal.insertAt index (One value) source
+ static member insertManyAt index values source = Internal.insertAt index (Many values) source
+ static member removeAt index source = Internal.removeAt index source
+ static member removeManyAt index count source = Internal.removeManyAt index count source
+ static member updateAt index value source = Internal.updateAt index value source
+
static member except itemsToExclude source = Internal.except itemsToExclude source
static member exceptOfSeq itemsToExclude source = Internal.exceptOfSeq itemsToExclude source
diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi
index 579b55c..5a3b16f 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi
+++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi
@@ -1241,6 +1241,7 @@ type TaskSeq =
///
/// The first input task sequence.
/// The second input task sequence.
+ /// The result task sequence of tuples.
/// Thrown when either of the two input task sequences is null.
static member zip: source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'T * 'U>
@@ -1275,3 +1276,65 @@ type TaskSeq =
/// Thrown when the input task sequence is null.
static member foldAsync:
folder: ('State -> 'T -> #Task<'State>) -> state: 'State -> source: TaskSeq<'T> -> Task<'State>
+
+ ///
+ /// Return a new task sequence with a new item inserted before the given index.
+ ///
+ ///
+ /// The index where the item should be inserted.
+ /// The value to insert.
+ /// The input task sequence.
+ /// The result task sequence.
+ /// Thrown when the input task sequence is null.
+ /// Thrown when index is below 0 or greater than source length.
+ static member insertAt: index: int -> value: 'T -> source: TaskSeq<'T> -> TaskSeq<'T>
+
+ ///
+ /// Return a new task sequence with the new items inserted before the given index.
+ ///
+ ///
+ /// The index where the items should be inserted.
+ /// The values to insert.
+ /// The input task sequence.
+ /// The result task sequence.
+ /// Thrown when the input task sequence is null.
+ /// Thrown when index is below 0 or greater than source length.
+ static member insertManyAt: index: int -> values: TaskSeq<'T> -> source: TaskSeq<'T> -> TaskSeq<'T>
+
+ ///
+ /// Return a new task sequence with the item at the given index removed.
+ ///
+ ///
+ /// The index where the item should be removed.
+ /// The input task sequence.
+ /// The result task sequence.
+ /// Thrown when the input task sequence is null.
+ /// Thrown when index is below 0 or greater than source length.
+ static member removeAt: index: int -> source: TaskSeq<'T> -> TaskSeq<'T>
+
+ ///
+ /// Return a new task sequence with the number of items starting at a given index removed.
+ /// If is negative or zero, no items are removed. If
+ /// + is greater than source length, but is not, then
+ /// all items until end of sequence are removed.
+ ///
+ ///
+ /// The index where the items should be removed.
+ /// The number of items to remove.
+ /// The input task sequence.
+ /// The result task sequence.
+ /// Thrown when the input task sequence is null.
+ /// Thrown when index is below 0 or greater than source length.
+ static member removeManyAt: index: int -> count: int -> source: TaskSeq<'T> -> TaskSeq<'T>
+
+ ///
+ /// Return a new task sequence with the item at a given index set to the new value.
+ ///
+ ///
+ /// The index of the item to be replaced.
+ /// The new value.
+ /// The input task sequence.
+ /// The result task sequence.
+ /// Thrown when the input task sequence is null.
+ /// Thrown when index is below 0 or greater than source length.
+ static member updateAt: index: int -> value: 'T -> source: TaskSeq<'T> -> TaskSeq<'T>
diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
index c48821f..5827637 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
+++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
@@ -49,6 +49,11 @@ type internal InitAction<'T, 'TaskT when 'TaskT :> Task<'T>> =
| InitAction of init_item: (int -> 'T)
| InitActionAsync of async_init_item: (int -> 'TaskT)
+[]
+type internal ManyOrOne<'T> =
+ | Many of source_seq: TaskSeq<'T>
+ | One of source_item: 'T
+
module internal TaskSeqInternal =
/// Raise an NRE for arguments that are null. Only used for 'source' parameters, never for function parameters.
let inline checkNonNull argName arg =
@@ -57,7 +62,10 @@ module internal TaskSeqInternal =
let inline raiseEmptySeq () = invalidArg "source" "The input task sequence was empty."
- let inline raiseCannotBeNegative name = invalidArg name "The value must be non-negative"
+ let inline raiseCannotBeNegative name = invalidArg name "The value must be non-negative."
+
+ let inline raiseOutOfBounds name =
+ invalidArg name "The value or index must be within the bounds of the task sequence."
let inline raiseInsufficient () =
// this is correct, it is NOT an InvalidOperationException (see Seq.fs in F# Core)
@@ -861,6 +869,90 @@ module internal TaskSeqInternal =
yield e.Current
}
+ /// InsertAt or InsertManyAt
+ let insertAt index valueOrValues (source: TaskSeq<_>) =
+ if index < 0 then
+ raiseCannotBeNegative (nameof index)
+
+ taskSeq {
+ let mutable i = 0
+
+ for item in source do
+ if i = index then
+ match valueOrValues with
+ | Many values -> yield! values
+ | One value -> yield value
+
+ yield item
+ i <- i + 1
+
+ // allow inserting at the end
+ if i = index then
+ match valueOrValues with
+ | Many values -> yield! values
+ | One value -> yield value
+
+ if i < index then
+ raiseOutOfBounds (nameof index)
+ }
+
+ let removeAt index (source: TaskSeq<'T>) =
+ if index < 0 then
+ raiseCannotBeNegative (nameof index)
+
+ taskSeq {
+ let mutable i = 0
+
+ for item in source do
+ if i <> index then
+ yield item
+
+ i <- i + 1
+
+ // cannot remove past end of sequence
+ if i <= index then
+ raiseOutOfBounds (nameof index)
+ }
+
+ let removeManyAt index count (source: TaskSeq<'T>) =
+ if index < 0 then
+ raiseCannotBeNegative (nameof index)
+
+ taskSeq {
+ let mutable i = 0
+ let indexEnd = index + count
+
+ for item in source do
+ if i < index || i >= indexEnd then
+ yield item
+
+ i <- i + 1
+
+ // cannot remove past end of sequence
+ if i <= index then
+ raiseOutOfBounds (nameof index)
+ }
+
+ let updateAt index value (source: TaskSeq<'T>) =
+ if index < 0 then
+ raiseCannotBeNegative (nameof index)
+
+ taskSeq {
+ let mutable i = 0
+
+ for item in source do
+ if i <> index then // most common scenario on top (cpu prediction)
+ yield item
+ else
+ yield value
+
+ i <- i + 1
+
+ // cannot update past end of sequence
+ if i <= index then
+ raiseOutOfBounds (nameof index)
+ }
+
// Consider turning using an F# version of this instead?
// https://github.com/i3arnon/ConcurrentHashSet
type ConcurrentHashSet<'T when 'T: equality>(ct) =