Skip to content

Commit 179386e

Browse files
authored
Merge branch 'main' into repo-assist/perf-while-bang-cleanup-20260409-5ddbcb4cb509276b
2 parents e378079 + 53ee24c commit 179386e

File tree

10 files changed

+380
-71
lines changed

10 files changed

+380
-71
lines changed

.github/workflows/test-report.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,13 @@ jobs:
2929
name: Report debug tests # Name of the check run which will be created
3030
path: '*.trx' # Path to test results (inside artifact .zip)
3131
reporter: dotnet-trx # Format of test results
32+
33+
test-report-release-linux:
34+
runs-on: ubuntu-latest
35+
steps:
36+
- uses: dorny/test-reporter@v2
37+
with:
38+
artifact: test-results-release-linux # artifact name
39+
name: Report release tests (Linux) # Name of the check run which will be created
40+
path: '*.trx' # Path to test results (inside artifact .zip)
41+
reporter: dotnet-trx # Format of test results

.github/workflows/test.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,34 @@ jobs:
7777
name: test-results-debug
7878
# this path glob pattern requires forward slashes!
7979
path: ./src/FSharp.Control.TaskSeq.Test/TestResults/test-results-debug.trx
80+
81+
82+
test-release-linux:
83+
name: Test Release Build (Linux)
84+
runs-on: ubuntu-latest
85+
steps:
86+
- name: checkout-code
87+
uses: actions/checkout@v6
88+
with:
89+
fetch-depth: 0
90+
91+
- name: setup-dotnet
92+
uses: actions/setup-dotnet@v4
93+
94+
- name: Cache NuGet packages
95+
uses: actions/cache@v5
96+
with:
97+
path: ~/.nuget/packages
98+
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.fsproj', '**/*.csproj', 'global.json') }}
99+
restore-keys: nuget-${{ runner.os }}-
100+
101+
# run tests directly (build.cmd is Windows-only)
102+
- name: Run dotnet test - release (Linux)
103+
run: dotnet test src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj -c Release --blame-hang-timeout 60000ms --logger "console;verbosity=detailed" --logger "trx;LogFileName=test-results-release-linux.trx"
104+
105+
- uses: actions/upload-artifact@v4
106+
if: success() || failure()
107+
with:
108+
name: test-results-release-linux
109+
# this path glob pattern requires forward slashes!
110+
path: ./src/FSharp.Control.TaskSeq.Test/TestResults/test-results-release-linux.trx

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ FSharp.Control.TaskSeq is an F# library providing a `taskSeq` computation expres
77
## Repository Layout
88

99
- `src/FSharp.Control.TaskSeq/` — Main library (netstandard2.1)
10-
- `src/FSharp.Control.TaskSeq.Test/` — xUnit test project (net9.0)
10+
- `src/FSharp.Control.TaskSeq.Test/` — xUnit test project (net10.0)
1111
- `src/FSharp.Control.TaskSeq.SmokeTests/` — Smoke/integration tests
1212
- `src/FSharp.Control.TaskSeq.sln` — Solution file
1313
- `Version.props` — Package version (derived automatically from `release-notes.txt`)

release-notes.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@ Release notes:
33

44
Unreleased
55

6+
1.1.1
7+
- perf: use while! in groupBy, countBy, partition, except, exceptOfSeq to eliminate redundant mutable 'go' variables and initial MoveNextAsync calls
8+
69
1.1.0
10+
- adds TaskSeq.chooseV, TaskSeq.chooseVAsync, #385
11+
12+
1.0.0
713
- adds taskSeqDynamic computation expression and TaskSeqDynamic/TaskSeqDynamicInfo types for dynamic (FSI-compatible) resumable code, fixing issue where taskSeq would raise NotImplementedException in F# Interactive, #246
814
- perf: simplify iter, fold, reduce, mapFold, tryLast, skipOrTake (Drop/Truncate) to use while! and remove manual go-flag and initial MoveNextAsync pre-advance, matching the pattern already used by sum/sumBy/average
915
- perf: toResizeArrayAsync (and therefore toArrayAsync, toListAsync, toResizeArrayAsync, toIListAsync) uses a direct loop instead of going through iter, avoiding a lambda and DU allocation per call
1016
- perf: tryItem uses a simpler loop that skips the redundant inner index check on every iteration
1117
- perf: TaskSeq.chunkBy and chunkByAsync reuse the ResizeArray buffer between chunks, reducing allocations on sequences with many chunk boundaries
1218
- fixes: TaskSeq.insertAt, insertManyAt, removeAt, removeManyAt, updateAt now raise ArgumentNullException (not NullReferenceException) when given a null source; insertManyAt also validates the values argument
1319
- refactor: simplify lengthBy and lengthBeforeMax to use while! and remove the redundant mutable 'go' and initial MoveNextAsync
20+
- refactor: simplify tryTail inner loop to use while!, removing redundant mutable 'go' flag and initial MoveNextAsync
1421
- adds TaskSeq.distinctUntilChangedWith and TaskSeq.distinctUntilChangedWithAsync, #345
1522
- adds TaskSeq.replicateInfinite, replicateInfiniteAsync, replicateUntilNoneAsync, #345
1623
- adds TaskSeq.firstOrDefault, lastOrDefault, #345

src/FSharp.Control.TaskSeq.SmokeTests/FSharp.Control.TaskSeq.SmokeTests.fsproj

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net9.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
66
</PropertyGroup>
77

8-
<ItemGroup>
9-
<Content Remove="C:\Users\Abel\.nuget\packages\fsharp.control.taskseq\0.3.0\contentFiles\any\netstandard2.1\release-notes.txt" />
10-
</ItemGroup>
11-
128
<ItemGroup>
139
<Compile Include="TestUtils.fs" />
1410
<Compile Include="SmokeTests.fs" />
@@ -25,14 +21,14 @@
2521
-->
2622
<PackageReference Include="FSharp.Control.TaskSeq" Version="0.4.0" />
2723
<PackageReference Include="FsToolkit.ErrorHandling.TaskResult" Version="4.15.1" />
28-
<PackageReference Include="FsUnit.xUnit" Version="6.0.0" />
29-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
30-
<PackageReference Include="xunit" Version="2.8.0" />
31-
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
24+
<PackageReference Include="FsUnit.xUnit" Version="6.0.1" />
25+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
26+
<PackageReference Include="xunit" Version="2.9.3" />
27+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
3228
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
3329
<PrivateAssets>all</PrivateAssets>
3430
</PackageReference>
35-
<PackageReference Include="coverlet.collector" Version="6.0.2">
31+
<PackageReference Include="coverlet.collector" Version="6.0.4">
3632
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
3733
<PrivateAssets>all</PrivateAssets>
3834
</PackageReference>

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<Compile Include="TaskSeq.Append.Tests.fs" />
1313
<Compile Include="TaskSeq.Cast.Tests.fs" />
1414
<Compile Include="TaskSeq.Choose.Tests.fs" />
15+
<Compile Include="TaskSeq.ChooseV.Tests.fs" />
1516
<Compile Include="TaskSeq.Collect.Tests.fs" />
1617
<Compile Include="TaskSeq.Concat.Tests.fs" />
1718
<Compile Include="TaskSeq.Contains.Tests.fs" />
@@ -88,14 +89,14 @@
8889
still compatible version of the TaskSeq library.
8990
-->
9091
<PackageReference Update="FSharp.Core" Version="6.0.1" />
91-
<PackageReference Include="FsUnit.xUnit" Version="6.0.0" />
92-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
92+
<PackageReference Include="FsUnit.xUnit" Version="6.0.1" />
93+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
9394
<PackageReference Include="xunit" Version="2.9.3" />
9495
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
9596
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
9697
<PrivateAssets>all</PrivateAssets>
9798
</PackageReference>
98-
<PackageReference Include="coverlet.collector" Version="6.0.2">
99+
<PackageReference Include="coverlet.collector" Version="6.0.4">
99100
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
100101
<PrivateAssets>all</PrivateAssets>
101102
</PackageReference>
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
module TaskSeq.Tests.ChooseV
2+
3+
open System
4+
5+
open Xunit
6+
open FsUnit.Xunit
7+
8+
open FSharp.Control
9+
10+
//
11+
// TaskSeq.chooseV
12+
// TaskSeq.chooseVAsync
13+
//
14+
15+
module EmptySeq =
16+
[<Fact>]
17+
let ``Null source is invalid`` () =
18+
assertNullArg
19+
<| fun () -> TaskSeq.chooseV (fun _ -> ValueNone) null
20+
21+
assertNullArg
22+
<| fun () -> TaskSeq.chooseVAsync (fun _ -> Task.fromResult ValueNone) null
23+
24+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
25+
let ``TaskSeq-chooseV`` variant = task {
26+
let! empty =
27+
Gen.getEmptyVariant variant
28+
|> TaskSeq.chooseV (fun _ -> ValueSome 42)
29+
|> TaskSeq.toListAsync
30+
31+
List.isEmpty empty |> should be True
32+
}
33+
34+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
35+
let ``TaskSeq-chooseVAsync`` variant = task {
36+
let! empty =
37+
Gen.getEmptyVariant variant
38+
|> TaskSeq.chooseVAsync (fun _ -> task { return ValueSome 42 })
39+
|> TaskSeq.toListAsync
40+
41+
List.isEmpty empty |> should be True
42+
}
43+
44+
module Immutable =
45+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
46+
let ``TaskSeq-chooseV can convert and filter`` variant = task {
47+
let chooser number =
48+
if number <= 5 then
49+
ValueSome(char number + '@')
50+
else
51+
ValueNone
52+
53+
let ts = Gen.getSeqImmutable variant
54+
55+
let! letters1 = TaskSeq.chooseV chooser ts |> TaskSeq.toArrayAsync
56+
let! letters2 = TaskSeq.chooseV chooser ts |> TaskSeq.toArrayAsync
57+
58+
String letters1 |> should equal "ABCDE"
59+
String letters2 |> should equal "ABCDE"
60+
}
61+
62+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
63+
let ``TaskSeq-chooseVAsync can convert and filter`` variant = task {
64+
let chooser number = task {
65+
return
66+
if number <= 5 then
67+
ValueSome(char number + '@')
68+
else
69+
ValueNone
70+
}
71+
72+
let ts = Gen.getSeqImmutable variant
73+
74+
let! letters1 = TaskSeq.chooseVAsync chooser ts |> TaskSeq.toArrayAsync
75+
let! letters2 = TaskSeq.chooseVAsync chooser ts |> TaskSeq.toArrayAsync
76+
77+
String letters1 |> should equal "ABCDE"
78+
String letters2 |> should equal "ABCDE"
79+
}
80+
81+
module Immutable2 =
82+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
83+
let ``TaskSeq-chooseV returns all when chooser always returns ValueSome`` variant = task {
84+
let ts = Gen.getSeqImmutable variant
85+
let! xs = ts |> TaskSeq.chooseV ValueSome |> TaskSeq.toArrayAsync
86+
xs |> should equal [| 1..10 |]
87+
}
88+
89+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
90+
let ``TaskSeq-chooseVAsync returns all when chooser always returns ValueSome`` variant = task {
91+
let ts = Gen.getSeqImmutable variant
92+
93+
let! xs =
94+
ts
95+
|> TaskSeq.chooseVAsync (fun x -> task { return ValueSome x })
96+
|> TaskSeq.toArrayAsync
97+
98+
xs |> should equal [| 1..10 |]
99+
}
100+
101+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
102+
let ``TaskSeq-chooseV returns empty when chooser always returns ValueNone`` variant = task {
103+
let ts = Gen.getSeqImmutable variant
104+
105+
do! ts |> TaskSeq.chooseV (fun _ -> ValueNone) |> verifyEmpty
106+
}
107+
108+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
109+
let ``TaskSeq-chooseVAsync returns empty when chooser always returns ValueNone`` variant = task {
110+
let ts = Gen.getSeqImmutable variant
111+
112+
do!
113+
ts
114+
|> TaskSeq.chooseVAsync (fun _ -> task { return ValueNone })
115+
|> verifyEmpty
116+
}
117+
118+
[<Fact>]
119+
let ``TaskSeq-chooseV with singleton sequence and ValueSome chooser returns singleton`` () = task {
120+
let! xs =
121+
taskSeq { yield 42 }
122+
|> TaskSeq.chooseV (fun x -> ValueSome(x * 2))
123+
|> TaskSeq.toListAsync
124+
125+
xs |> should equal [ 84 ]
126+
}
127+
128+
[<Fact>]
129+
let ``TaskSeq-chooseV with singleton sequence and ValueNone chooser returns empty`` () =
130+
taskSeq { yield 42 }
131+
|> TaskSeq.chooseV (fun _ -> ValueNone)
132+
|> verifyEmpty
133+
134+
[<Fact>]
135+
let ``TaskSeq-chooseV can change the element type`` () = task {
136+
// choose maps int -> string voption, verifying type-changing behavior
137+
let chooser n =
138+
if n % 2 = 0 then
139+
ValueSome(sprintf "even-%d" n)
140+
else
141+
ValueNone
142+
143+
let! xs =
144+
taskSeq { yield! [ 1..6 ] }
145+
|> TaskSeq.chooseV chooser
146+
|> TaskSeq.toListAsync
147+
148+
xs |> should equal [ "even-2"; "even-4"; "even-6" ]
149+
}
150+
151+
[<Fact>]
152+
let ``TaskSeq-chooseVAsync can change the element type`` () = task {
153+
let chooser n = task {
154+
return
155+
if n % 2 = 0 then
156+
ValueSome(sprintf "even-%d" n)
157+
else
158+
ValueNone
159+
}
160+
161+
let! xs =
162+
taskSeq { yield! [ 1..6 ] }
163+
|> TaskSeq.chooseVAsync chooser
164+
|> TaskSeq.toListAsync
165+
166+
xs |> should equal [ "even-2"; "even-4"; "even-6" ]
167+
}
168+
169+
module SideEffects =
170+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
171+
let ``TaskSeq-chooseV applied multiple times`` variant = task {
172+
let ts = Gen.getSeqWithSideEffect variant
173+
174+
let chooser x number =
175+
if number <= x then
176+
ValueSome(char number + '@')
177+
else
178+
ValueNone
179+
180+
let! lettersA = ts |> TaskSeq.chooseV (chooser 5) |> TaskSeq.toArrayAsync
181+
let! lettersK = ts |> TaskSeq.chooseV (chooser 15) |> TaskSeq.toArrayAsync
182+
let! lettersU = ts |> TaskSeq.chooseV (chooser 25) |> TaskSeq.toArrayAsync
183+
184+
String lettersA |> should equal "ABCDE"
185+
String lettersK |> should equal "KLMNO"
186+
String lettersU |> should equal "UVWXY"
187+
}
188+
189+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
190+
let ``TaskSeq-chooseVAsync applied multiple times`` variant = task {
191+
let ts = Gen.getSeqWithSideEffect variant
192+
193+
let chooser x number = task {
194+
return
195+
if number <= x then
196+
ValueSome(char number + '@')
197+
else
198+
ValueNone
199+
}
200+
201+
let! lettersA = TaskSeq.chooseVAsync (chooser 5) ts |> TaskSeq.toArrayAsync
202+
let! lettersK = TaskSeq.chooseVAsync (chooser 15) ts |> TaskSeq.toArrayAsync
203+
let! lettersU = TaskSeq.chooseVAsync (chooser 25) ts |> TaskSeq.toArrayAsync
204+
205+
String lettersA |> should equal "ABCDE"
206+
String lettersK |> should equal "KLMNO"
207+
String lettersU |> should equal "UVWXY"
208+
}
209+
210+
[<Fact>]
211+
let ``TaskSeq-chooseV evaluates each source element exactly once`` () = task {
212+
let mutable count = 0
213+
214+
let ts = taskSeq {
215+
for i in 1..5 do
216+
count <- count + 1
217+
yield i
218+
}
219+
220+
let! xs =
221+
ts
222+
|> TaskSeq.chooseV (fun x -> if x < 3 then ValueSome x else ValueNone)
223+
|> TaskSeq.toListAsync
224+
225+
count |> should equal 5 // all 5 elements were visited
226+
xs |> should equal [ 1; 2 ]
227+
}
228+
229+
[<Fact>]
230+
let ``TaskSeq-chooseVAsync evaluates each source element exactly once`` () = task {
231+
let mutable count = 0
232+
233+
let ts = taskSeq {
234+
for i in 1..5 do
235+
count <- count + 1
236+
yield i
237+
}
238+
239+
let! xs =
240+
ts
241+
|> TaskSeq.chooseVAsync (fun x -> task { return if x < 3 then ValueSome x else ValueNone })
242+
|> TaskSeq.toListAsync
243+
244+
count |> should equal 5
245+
xs |> should equal [ 1; 2 ]
246+
}

src/FSharp.Control.TaskSeq/TaskSeq.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,9 @@ type TaskSeq private () =
441441
}
442442

443443
static member choose chooser source = Internal.choose (TryPick chooser) source
444+
static member chooseV chooser source = Internal.chooseV (TryPickV chooser) source
444445
static member chooseAsync chooser source = Internal.choose (TryPickAsync chooser) source
446+
static member chooseVAsync chooser source = Internal.chooseV (TryPickVAsync chooser) source
445447

446448
static member filter predicate source = Internal.filter (Predicate predicate) source
447449
static member filterAsync predicate source = Internal.filter (PredicateAsync predicate) source

0 commit comments

Comments
 (0)