Skip to content

Commit ffeb50e

Browse files
authored
Merge branch 'main' into repo-assist/feat-scan-2026-03-8079b757d4337d53
2 parents 6b51736 + 0bdf1d6 commit ffeb50e

File tree

10 files changed

+617
-0
lines changed

10 files changed

+617
-0
lines changed

release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Release notes:
44
0.5.0
55
- update engineering to .NET 9/10
66
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
7+
- adds TaskSeq.pairwise, #289
78

89
0.4.0
910
- overhaul all doc comments, add exceptions, improve IDE quick-info experience, #136, #220, #234

src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
<Compile Include="TaskSeq.ExactlyOne.Tests.fs" />
2121
<Compile Include="TaskSeq.Except.Tests.fs" />
2222
<Compile Include="TaskSeq.DistinctUntilChanged.Tests.fs" />
23+
<Compile Include="TaskSeq.Pairwise.Tests.fs" />
2324
<Compile Include="TaskSeq.Exists.Tests.fs" />
2425
<Compile Include="TaskSeq.Filter.Tests.fs" />
2526
<Compile Include="TaskSeq.FindIndex.Tests.fs" />
2627
<Compile Include="TaskSeq.Find.Tests.fs" />
2728
<Compile Include="TaskSeq.Fold.Tests.fs" />
2829
<Compile Include="TaskSeq.Scan.Tests.fs" />
30+
<Compile Include="TaskSeq.Reduce.Tests.fs" />
2931
<Compile Include="TaskSeq.Forall.Tests.fs" />
3032
<Compile Include="TaskSeq.Head.Tests.fs" />
3133
<Compile Include="TaskSeq.Indexed.Tests.fs" />

src/FSharp.Control.TaskSeq.Test/TaskSeq.Choose.Tests.fs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,84 @@ module Immutable =
6666
String letters2 |> should equal "ABCDE"
6767
}
6868

69+
module Immutable2 =
70+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
71+
let ``TaskSeq-choose returns all when chooser always returns Some`` variant = task {
72+
let ts = Gen.getSeqImmutable variant
73+
let! xs = ts |> TaskSeq.choose Some |> TaskSeq.toArrayAsync
74+
xs |> should equal [| 1..10 |]
75+
}
76+
77+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
78+
let ``TaskSeq-chooseAsync returns all when chooser always returns Some`` variant = task {
79+
let ts = Gen.getSeqImmutable variant
80+
81+
let! xs =
82+
ts
83+
|> TaskSeq.chooseAsync (fun x -> task { return Some x })
84+
|> TaskSeq.toArrayAsync
85+
86+
xs |> should equal [| 1..10 |]
87+
}
88+
89+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
90+
let ``TaskSeq-choose returns empty when chooser always returns None`` variant = task {
91+
let ts = Gen.getSeqImmutable variant
92+
93+
do! ts |> TaskSeq.choose (fun _ -> None) |> verifyEmpty
94+
}
95+
96+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
97+
let ``TaskSeq-chooseAsync returns empty when chooser always returns None`` variant = task {
98+
let ts = Gen.getSeqImmutable variant
99+
100+
do!
101+
ts
102+
|> TaskSeq.chooseAsync (fun _ -> task { return None })
103+
|> verifyEmpty
104+
}
105+
106+
[<Fact>]
107+
let ``TaskSeq-choose with singleton sequence and Some chooser returns singleton`` () = task {
108+
let! xs =
109+
taskSeq { yield 42 }
110+
|> TaskSeq.choose (fun x -> Some(x * 2))
111+
|> TaskSeq.toListAsync
112+
113+
xs |> should equal [ 84 ]
114+
}
115+
116+
[<Fact>]
117+
let ``TaskSeq-choose with singleton sequence and None chooser returns empty`` () =
118+
taskSeq { yield 42 }
119+
|> TaskSeq.choose (fun _ -> None)
120+
|> verifyEmpty
121+
122+
[<Fact>]
123+
let ``TaskSeq-choose can change the element type`` () = task {
124+
// choose maps int -> string option, verifying type-changing behavior
125+
let chooser n = if n % 2 = 0 then Some(sprintf "even-%d" n) else None
126+
127+
let! xs =
128+
taskSeq { yield! [ 1..6 ] }
129+
|> TaskSeq.choose chooser
130+
|> TaskSeq.toListAsync
131+
132+
xs |> should equal [ "even-2"; "even-4"; "even-6" ]
133+
}
134+
135+
[<Fact>]
136+
let ``TaskSeq-chooseAsync can change the element type`` () = task {
137+
let chooser n = task { return if n % 2 = 0 then Some(sprintf "even-%d" n) else None }
138+
139+
let! xs =
140+
taskSeq { yield! [ 1..6 ] }
141+
|> TaskSeq.chooseAsync chooser
142+
|> TaskSeq.toListAsync
143+
144+
xs |> should equal [ "even-2"; "even-4"; "even-6" ]
145+
}
146+
69147
module SideEffects =
70148
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
71149
let ``TaskSeq-choose applied multiple times`` variant = task {
@@ -94,3 +172,41 @@ module SideEffects =
94172
String lettersK |> should equal "KLMNO"
95173
String lettersU |> should equal "UVWXY"
96174
}
175+
176+
[<Fact>]
177+
let ``TaskSeq-choose evaluates each source element exactly once`` () = task {
178+
let mutable count = 0
179+
180+
let ts = taskSeq {
181+
for i in 1..5 do
182+
count <- count + 1
183+
yield i
184+
}
185+
186+
let! xs =
187+
ts
188+
|> TaskSeq.choose (fun x -> if x < 3 then Some x else None)
189+
|> TaskSeq.toListAsync
190+
191+
count |> should equal 5 // all 5 elements were visited
192+
xs |> should equal [ 1; 2 ]
193+
}
194+
195+
[<Fact>]
196+
let ``TaskSeq-chooseAsync evaluates each source element exactly once`` () = task {
197+
let mutable count = 0
198+
199+
let ts = taskSeq {
200+
for i in 1..5 do
201+
count <- count + 1
202+
yield i
203+
}
204+
205+
let! xs =
206+
ts
207+
|> TaskSeq.chooseAsync (fun x -> task { return if x < 3 then Some x else None })
208+
|> TaskSeq.toListAsync
209+
210+
count |> should equal 5
211+
xs |> should equal [ 1; 2 ]
212+
}

src/FSharp.Control.TaskSeq.Test/TaskSeq.DistinctUntilChanged.Tests.fs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,98 @@ module Functionality =
3737
|> String.concat ""
3838
|> should equal "ABZCZCDZ"
3939
}
40+
41+
[<Fact>]
42+
let ``TaskSeq-distinctUntilChanged with single element returns singleton`` () = task {
43+
let! xs =
44+
taskSeq { yield 42 }
45+
|> TaskSeq.distinctUntilChanged
46+
|> TaskSeq.toListAsync
47+
48+
xs |> should equal [ 42 ]
49+
}
50+
51+
[<Fact>]
52+
let ``TaskSeq-distinctUntilChanged with all identical elements returns one element`` () = task {
53+
let! xs =
54+
taskSeq { yield! [ 7; 7; 7; 7; 7 ] }
55+
|> TaskSeq.distinctUntilChanged
56+
|> TaskSeq.toListAsync
57+
58+
xs |> should equal [ 7 ]
59+
}
60+
61+
[<Fact>]
62+
let ``TaskSeq-distinctUntilChanged with all distinct elements returns all`` () = task {
63+
let! xs =
64+
taskSeq { yield! [ 1; 2; 3; 4; 5 ] }
65+
|> TaskSeq.distinctUntilChanged
66+
|> TaskSeq.toListAsync
67+
68+
xs |> should equal [ 1; 2; 3; 4; 5 ]
69+
}
70+
71+
[<Fact>]
72+
let ``TaskSeq-distinctUntilChanged with alternating pairs`` () = task {
73+
// [A;A;B;B;A;A] -> [A;B;A]
74+
let! xs =
75+
taskSeq { yield! [ 'A'; 'A'; 'B'; 'B'; 'A'; 'A' ] }
76+
|> TaskSeq.distinctUntilChanged
77+
|> TaskSeq.toListAsync
78+
79+
xs |> should equal [ 'A'; 'B'; 'A' ]
80+
}
81+
82+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
83+
let ``TaskSeq-distinctUntilChanged on immutable all-unique seq preserves all elements`` variant = task {
84+
// getSeqImmutable yields 1..10, all unique, so all are returned
85+
let! xs =
86+
Gen.getSeqImmutable variant
87+
|> TaskSeq.distinctUntilChanged
88+
|> TaskSeq.toListAsync
89+
90+
xs |> should equal [ 1..10 ]
91+
}
92+
93+
module SideEffects =
94+
[<Fact>]
95+
let ``TaskSeq-distinctUntilChanged consumes every element exactly once`` () = task {
96+
let mutable count = 0
97+
98+
let ts = taskSeq {
99+
for i in 1..6 do
100+
count <- count + 1
101+
yield i % 3 // yields 1,2,0,1,2,0 — no consecutive duplicates
102+
}
103+
104+
let! xs = ts |> TaskSeq.distinctUntilChanged |> TaskSeq.toListAsync
105+
count |> should equal 6
106+
xs |> should equal [ 1; 2; 0; 1; 2; 0 ]
107+
}
108+
109+
[<Fact>]
110+
let ``TaskSeq-distinctUntilChanged skips duplicates without extra evaluation`` () = task {
111+
let mutable count = 0
112+
113+
let ts = taskSeq {
114+
for i in [ 1; 1; 2; 2; 3 ] do
115+
count <- count + 1
116+
yield i
117+
}
118+
119+
let! xs = ts |> TaskSeq.distinctUntilChanged |> TaskSeq.toListAsync
120+
// All 5 source elements must still be consumed
121+
count |> should equal 5
122+
xs |> should equal [ 1; 2; 3 ]
123+
}
124+
125+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
126+
let ``TaskSeq-distinctUntilChanged on side-effect seq preserves all unique elements`` variant = task {
127+
// getSeqWithSideEffect yields 1..10 (all unique on first iteration)
128+
let! xs =
129+
Gen.getSeqWithSideEffect variant
130+
|> TaskSeq.distinctUntilChanged
131+
|> TaskSeq.toListAsync
132+
133+
xs |> should equal [ 1..10 ]
134+
}

src/FSharp.Control.TaskSeq.Test/TaskSeq.Filter.Tests.fs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,50 @@ module Immutable =
8787

8888
}
8989

90+
module Immutable2 =
91+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
92+
let ``TaskSeq-filter keeps all when predicate is always true`` variant =
93+
Gen.getSeqImmutable variant
94+
|> TaskSeq.filter (fun _ -> true)
95+
|> verify1To10
96+
97+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
98+
let ``TaskSeq-filterAsync keeps all when predicate is always true`` variant =
99+
Gen.getSeqImmutable variant
100+
|> TaskSeq.filterAsync (fun _ -> Task.fromResult true)
101+
|> verify1To10
102+
103+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
104+
let ``TaskSeq-filter returns empty when predicate is always false`` variant =
105+
Gen.getSeqImmutable variant
106+
|> TaskSeq.filter (fun _ -> false)
107+
|> verifyEmpty
108+
109+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
110+
let ``TaskSeq-filterAsync returns empty when predicate is always false`` variant =
111+
Gen.getSeqImmutable variant
112+
|> TaskSeq.filterAsync (fun _ -> Task.fromResult false)
113+
|> verifyEmpty
114+
115+
[<Fact>]
116+
let ``TaskSeq-filter evaluates each element exactly once`` () = task {
117+
let mutable count = 0
118+
119+
let ts = taskSeq {
120+
for i in 1..5 do
121+
count <- count + 1
122+
yield i
123+
}
124+
125+
let! xs =
126+
ts
127+
|> TaskSeq.filter (fun x -> x % 2 = 0)
128+
|> TaskSeq.toListAsync
129+
130+
count |> should equal 5
131+
xs |> should equal [ 2; 4 ]
132+
}
133+
90134
module SideEffects =
91135
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
92136
let ``TaskSeq-filter filters correctly`` variant = task {

0 commit comments

Comments
 (0)