diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.SplitAt.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.SplitAt.Tests.fs index 2dfc8a6..563f404 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.SplitAt.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.SplitAt.Tests.fs @@ -95,3 +95,77 @@ module Immutable = let combined = Array.append prefix restArr combined |> should equal data } + +module SideEffects = + [)>] + let ``TaskSeq-splitAt prefix gets first n elements from side-effect seq`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + let! prefix, _ = TaskSeq.splitAt 3 ts + prefix |> should equal [| 1; 2; 3 |] + } + + [)>] + let ``TaskSeq-splitAt rest yields remaining elements from side-effect seq`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + let! _, rest = TaskSeq.splitAt 3 ts + let! restArr = TaskSeq.toArrayAsync rest + restArr |> should equal [| 4..10 |] + } + + [)>] + let ``TaskSeq-splitAt prefix and rest together cover all elements of side-effect seq`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + let! prefix, rest = TaskSeq.splitAt 5 ts + let! restArr = TaskSeq.toArrayAsync rest + let combined = Array.append prefix restArr + combined |> should equal [| 1..10 |] + } + + [] + let ``TaskSeq-splitAt rest is lazy: side effects in rest not triggered until consumed`` () = task { + let mutable restSideEffectCount = 0 + + let ts = taskSeq { + yield 1 + yield 2 + yield 3 + + // These yields are in the "rest" portion + restSideEffectCount <- restSideEffectCount + 1 + yield 4 + restSideEffectCount <- restSideEffectCount + 1 + yield 5 + } + + let! _prefix, rest = TaskSeq.splitAt 3 ts + // rest has NOT been consumed yet; the side effects in it should not have fired + restSideEffectCount |> should equal 0 + + // Now consume rest + let! restArr = TaskSeq.toArrayAsync rest + restArr |> should equal [| 4; 5 |] + restSideEffectCount |> should equal 2 + } + + [] + let ``TaskSeq-splitAt second evaluation of side-effect seq yields next batch`` () = task { + let mutable i = 0 + + let ts = taskSeq { + for _ = 1 to 10 do + i <- i + 1 + yield i + } + + // First split + let! prefix1, rest1 = TaskSeq.splitAt 4 ts + let! restArr1 = TaskSeq.toArrayAsync rest1 + prefix1 |> should equal [| 1; 2; 3; 4 |] + restArr1 |> should equal [| 5..10 |] + + // Second split of the same 'ts' uses the same 'i' capture; i is now 10 + let! prefix2, rest2 = TaskSeq.splitAt 4 ts + let! restArr2 = TaskSeq.toArrayAsync rest2 + prefix2 |> should equal [| 11; 12; 13; 14 |] + restArr2 |> should equal [| 15..20 |] + } diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.ZipWith.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.ZipWith.Tests.fs index c6c5b6a..0618677 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.ZipWith.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.ZipWith.Tests.fs @@ -172,3 +172,86 @@ module Immutable = viaZipWith |> should equal viaZipMap } + +module SideEffects = + [)>] + let ``TaskSeq-zipWith on two side-effect seqs combines elements correctly`` variant = task { + let s1 = Gen.getSeqWithSideEffect variant + let s2 = Gen.getSeqWithSideEffect variant + + // Both sequences yield 1..10 on first iteration; side effects increment independently + let! result = TaskSeq.zipWith (+) s1 s2 |> TaskSeq.toArrayAsync + + result + |> should equal (Array.init 10 (fun i -> (i + 1) + (i + 1))) + } + + [)>] + let ``TaskSeq-zipWithAsync on two side-effect seqs combines elements correctly`` variant = task { + let s1 = Gen.getSeqWithSideEffect variant + let s2 = Gen.getSeqWithSideEffect variant + + let! result = + TaskSeq.zipWithAsync (fun a b -> Task.fromResult (a * b)) s1 s2 + |> TaskSeq.toArrayAsync + + result + |> should equal (Array.init 10 (fun i -> (i + 1) * (i + 1))) + } + + [] + let ``TaskSeq-zipWith consumes both sequences one element at a time`` () = task { + let mutable count1 = 0 + let mutable count2 = 0 + + let s1 = taskSeq { + for i in 1..5 do + count1 <- count1 + 1 + yield i + } + + let s2 = taskSeq { + for i in 10..14 do + count2 <- count2 + 1 + yield i + } + + let! result = TaskSeq.zipWith (+) s1 s2 |> TaskSeq.toArrayAsync + result |> should equal [| 11; 13; 15; 17; 19 |] + count1 |> should equal 5 + count2 |> should equal 5 + } + + [] + let ``TaskSeq-zipWith truncates at shorter side-effect seq, output is correct`` () = task { + let mutable longCount = 0 + + let short = taskSeq { yield! [ 1; 2 ] } + + let long = taskSeq { + for i in 10..20 do + longCount <- longCount + 1 + yield i + } + + let! result = TaskSeq.zipWith (+) short long |> TaskSeq.toArrayAsync + result |> should equal [| 11; 13 |] + // The implementation reads one element from each sequence to check for stop condition, + // so the longer sequence is advanced one step beyond the last paired element. + longCount |> should be (greaterThanOrEqualTo 2) + longCount |> should be (lessThanOrEqualTo 3) + } + + [)>] + let ``TaskSeq-zipWith3 on three side-effect seqs combines elements correctly`` variant = task { + let s1 = Gen.getSeqWithSideEffect variant + let s2 = Gen.getSeqWithSideEffect variant + let s3 = Gen.getSeqWithSideEffect variant + + let! result = + TaskSeq.zipWith3 (fun a b c -> a + b + c) s1 s2 s3 + |> TaskSeq.toArrayAsync + + result + |> should equal (Array.init 10 (fun i -> 3 * (i + 1))) + }