Skip to content

Commit 4865e28

Browse files
authored
Merge pull request #419 from fsprojects/repo-assist/improve-trymax-trymin-20260508-f1717f1ca85019fb
[Repo Assist] feat: add TaskSeq.tryMax and TaskSeq.tryMin
2 parents 416c498 + 7422573 commit 4865e28

5 files changed

Lines changed: 127 additions & 0 deletions

File tree

release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Release notes:
33

44
Unreleased
5+
- adds TaskSeq.tryMax and TaskSeq.tryMin: safe variants of TaskSeq.max and TaskSeq.min that return None instead of raising ArgumentException when the input sequence is empty
56
- test: rename `SideEffect` module to `SideEffects` in TaskSeq.Concat.Tests.fs, TaskSeq.Delay.Tests.fs, and TaskSeq.Item.Tests.fs for consistency with the rest of the test suite (50+ files already use the plural form)
67
- test: add SideEffects module to TaskSeq.Using.Tests.fs; 7 new tests verify Dispose/DisposeAsync call counts, re-iteration semantics, and early-termination disposal for use and use! CE bindings
78
- perf: pairwise, distinctUntilChanged, distinctUntilChangedWith, distinctUntilChangedWithAsync now use explicit enumerator + while! instead of ValueOption tracking + for-in loop, eliminating per-element struct match overhead

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ open FSharp.Control
1010
//
1111
// TaskSeq.max
1212
// TaskSeq.min
13+
// TaskSeq.tryMax
14+
// TaskSeq.tryMin
1315
// TaskSeq.maxBy
1416
// TaskSeq.minBy
1517
// TaskSeq.maxByAsync
@@ -316,3 +318,87 @@ module SideEffects =
316318
do! test (MinMax.getByFunction minMax) 20
317319
do! test (MinMax.getByFunction minMax) 30
318320
}
321+
322+
323+
module TryMaxMin =
324+
[<Fact>]
325+
let ``TaskSeq-tryMax returns None for null source`` () =
326+
assertNullArg
327+
<| fun () -> TaskSeq.tryMax (null: TaskSeq<int>)
328+
329+
[<Fact>]
330+
let ``TaskSeq-tryMin returns None for null source`` () =
331+
assertNullArg
332+
<| fun () -> TaskSeq.tryMin (null: TaskSeq<int>)
333+
334+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
335+
let ``TaskSeq-tryMax returns None on empty`` variant = task {
336+
let! result = Gen.getEmptyVariant variant |> TaskSeq.tryMax
337+
result |> should equal None
338+
}
339+
340+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
341+
let ``TaskSeq-tryMin returns None on empty`` variant = task {
342+
let! result = Gen.getEmptyVariant variant |> TaskSeq.tryMin
343+
result |> should equal None
344+
}
345+
346+
[<Fact>]
347+
let ``TaskSeq-tryMax returns Some for singleton`` () = task {
348+
let! result = TaskSeq.singleton 42 |> TaskSeq.tryMax
349+
result |> should equal (Some 42)
350+
}
351+
352+
[<Fact>]
353+
let ``TaskSeq-tryMin returns Some for singleton`` () = task {
354+
let! result = TaskSeq.singleton 42 |> TaskSeq.tryMin
355+
result |> should equal (Some 42)
356+
}
357+
358+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
359+
let ``TaskSeq-tryMax returns Some max of sequence`` variant = task {
360+
let! result = Gen.getSeqImmutable variant |> TaskSeq.tryMax
361+
result |> should equal (Some 10)
362+
}
363+
364+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
365+
let ``TaskSeq-tryMin returns Some min of sequence`` variant = task {
366+
let! result = Gen.getSeqImmutable variant |> TaskSeq.tryMin
367+
result |> should equal (Some 1)
368+
}
369+
370+
[<Fact>]
371+
let ``TaskSeq-tryMax and max agree on non-empty sequence`` () = task {
372+
let ts = TaskSeq.ofList [ 3; 1; 4; 1; 5; 9; 2; 6 ]
373+
let! viaMax = ts |> TaskSeq.max
374+
let ts2 = TaskSeq.ofList [ 3; 1; 4; 1; 5; 9; 2; 6 ]
375+
let! viaTryMax = ts2 |> TaskSeq.tryMax
376+
viaTryMax |> should equal (Some viaMax)
377+
}
378+
379+
[<Fact>]
380+
let ``TaskSeq-tryMin and min agree on non-empty sequence`` () = task {
381+
let ts = TaskSeq.ofList [ 3; 1; 4; 1; 5; 9; 2; 6 ]
382+
let! viaMin = ts |> TaskSeq.min
383+
let ts2 = TaskSeq.ofList [ 3; 1; 4; 1; 5; 9; 2; 6 ]
384+
let! viaTryMin = ts2 |> TaskSeq.tryMin
385+
viaTryMin |> should equal (Some viaMin)
386+
}
387+
388+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
389+
let ``TaskSeq-tryMax re-iteration reflects side effects`` variant = task {
390+
let ts = Gen.getSeqWithSideEffect variant
391+
let! first = ts |> TaskSeq.tryMax
392+
let! second = ts |> TaskSeq.tryMax
393+
first |> should equal (Some 10)
394+
second |> should equal (Some 20)
395+
}
396+
397+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
398+
let ``TaskSeq-tryMin re-iteration reflects side effects`` variant = task {
399+
let ts = Gen.getSeqWithSideEffect variant
400+
let! first = ts |> TaskSeq.tryMin
401+
let! second = ts |> TaskSeq.tryMin
402+
first |> should equal (Some 1)
403+
second |> should equal (Some 11)
404+
}

src/FSharp.Control.TaskSeq/TaskSeq.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,8 @@ type TaskSeq private () =
303303

304304
static member max source = Internal.maxMin max source
305305
static member min source = Internal.maxMin min source
306+
static member tryMax source = Internal.tryMaxMin max source
307+
static member tryMin source = Internal.tryMaxMin min source
306308
static member maxBy projection source = Internal.maxMinBy (<) projection source // looks like 'less than', is 'greater than'
307309
static member minBy projection source = Internal.maxMinBy (>) projection source
308310
static member maxByAsync projection source = Internal.maxMinByAsync (<) projection source // looks like 'less than', is 'greater than'

src/FSharp.Control.TaskSeq/TaskSeq.fsi

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,26 @@ type TaskSeq =
224224
/// <exception cref="T:ArgumentException">Thrown when the input task sequence is empty.</exception>
225225
static member min: source: TaskSeq<'T> -> Task<'T> when 'T: comparison
226226

227+
/// <summary>
228+
/// Returns <c>Some</c> with the greatest of all elements of the task sequence, compared via <see cref="Operators.max" />,
229+
/// or <c>None</c> if the sequence is empty. For sequences that should never be empty, prefer <see cref="TaskSeq.max" />.
230+
/// </summary>
231+
///
232+
/// <param name="source">The input task sequence.</param>
233+
/// <returns>The largest element of the sequence wrapped in <c>Some</c>, or <c>None</c> if the sequence is empty.</returns>
234+
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
235+
static member tryMax: source: TaskSeq<'T> -> Task<'T option> when 'T: comparison
236+
237+
/// <summary>
238+
/// Returns <c>Some</c> with the smallest of all elements of the task sequence, compared via <see cref="Operators.min" />,
239+
/// or <c>None</c> if the sequence is empty. For sequences that should never be empty, prefer <see cref="TaskSeq.min" />.
240+
/// </summary>
241+
///
242+
/// <param name="source">The input task sequence.</param>
243+
/// <returns>The smallest element of the sequence wrapped in <c>Some</c>, or <c>None</c> if the sequence is empty.</returns>
244+
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
245+
static member tryMin: source: TaskSeq<'T> -> Task<'T option> when 'T: comparison
246+
227247
/// <summary>
228248
/// Returns the greatest of all elements of the task sequence, compared via <see cref="Operators.max" />
229249
/// on the result of applying the function <paramref name="projection" /> to each element.

src/FSharp.Control.TaskSeq/TaskSeqInternal.fs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,24 @@ module internal TaskSeqInternal =
238238
return acc
239239
}
240240

241+
let inline tryMaxMin ([<InlineIfLambda>] maxOrMin) (source: TaskSeq<_>) =
242+
checkNonNull (nameof source) source
243+
244+
task {
245+
use e = source.GetAsyncEnumerator CancellationToken.None
246+
let! hasFirst = e.MoveNextAsync()
247+
248+
if not hasFirst then
249+
return None
250+
else
251+
let mutable acc = e.Current
252+
253+
while! e.MoveNextAsync() do
254+
acc <- maxOrMin e.Current acc
255+
256+
return Some acc
257+
}
258+
241259
// 'compare' is either `<` or `>` (i.e, less-than, greater-than resp.)
242260
let inline maxMinBy ([<InlineIfLambda>] compare) ([<InlineIfLambda>] projection) (source: TaskSeq<_>) =
243261
checkNonNull (nameof source) source

0 commit comments

Comments
 (0)