Skip to content

Commit a63fee5

Browse files
Add AsyncSeq.sortByAsync and sortByDescendingAsync
Adds two new async sorting functions that mirror the existing 'Async' variants pattern of minByAsync, maxByAsync, and countByAsync: - sortByAsync: sorts by an async key projection, returning Async<array<'T>> - sortByDescendingAsync: same but in descending order Both functions compute each key exactly once per element, then sort. 5 new tests added; all 326 tests pass. Co-authored-by: Copilot <[email protected]>
1 parent 3249128 commit a63fee5

4 files changed

Lines changed: 79 additions & 0 deletions

File tree

RELEASE_NOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
### 4.10.1
2+
3+
* Added `AsyncSeq.sortByAsync` — sorts an async sequence by an asynchronous key-generating function, computing each key exactly once and returning a sorted array. Mirrors the `*Async` pattern of `minByAsync`, `maxByAsync`, and `countByAsync`.
4+
* Added `AsyncSeq.sortByDescendingAsync` — same as `sortByAsync` but orders descending.
5+
16
### 4.10.0
27

38
* Added `AsyncSeq.withCancellation` — returns a new `AsyncSeq` that passes the given `CancellationToken` to `GetAsyncEnumerator`, overriding whatever token would otherwise be supplied. Mirrors `TaskSeq.withCancellation` and is useful when consuming sequences from libraries (e.g. Entity Framework) that accept a cancellation token through `GetAsyncEnumerator`. Part of ongoing design-parity work with FSharp.Control.TaskSeq (see #277).

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2084,6 +2084,20 @@ module AsyncSeq =
20842084
let sortWith (comparer:'T -> 'T -> int) (source:AsyncSeq<'T>) : array<'T> =
20852085
toSortedSeq (Array.sortWith comparer) source
20862086

2087+
let sortByAsync (projection:'T -> Async<'Key>) (source:AsyncSeq<'T>) : Async<array<'T>> when 'Key : comparison = async {
2088+
let! pairs =
2089+
source
2090+
|> mapAsync (fun x -> async { let! k = projection x in return (k, x) })
2091+
|> toArrayAsync
2092+
return pairs |> Array.sortBy fst |> Array.map snd }
2093+
2094+
let sortByDescendingAsync (projection:'T -> Async<'Key>) (source:AsyncSeq<'T>) : Async<array<'T>> when 'Key : comparison = async {
2095+
let! pairs =
2096+
source
2097+
|> mapAsync (fun x -> async { let! k = projection x in return (k, x) })
2098+
|> toArrayAsync
2099+
return pairs |> Array.sortByDescending fst |> Array.map snd }
2100+
20872101
let rev (source: AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq {
20882102
let! arr = toArrayAsync source
20892103
for i in arr.Length - 1 .. -1 .. 0 do

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,18 @@ module AsyncSeq =
663663
/// large or infinite sequences.
664664
val sortByDescending : projection:('T -> 'Key) -> source:AsyncSeq<'T> -> array<'T> when 'Key : comparison
665665

666+
/// Applies an asynchronous key-generating function to each element of an AsyncSeq and returns
667+
/// an array ordered by the computed keys. Each key is computed exactly once per element.
668+
/// This function digests the whole initial sequence before returning. As a result this
669+
/// function should not be used with large or infinite sequences.
670+
val sortByAsync : projection:('T -> Async<'Key>) -> source:AsyncSeq<'T> -> Async<array<'T>> when 'Key : comparison
671+
672+
/// Applies an asynchronous key-generating function to each element of an AsyncSeq and returns
673+
/// an array ordered descending by the computed keys. Each key is computed exactly once per element.
674+
/// This function digests the whole initial sequence before returning. As a result this
675+
/// function should not be used with large or infinite sequences.
676+
val sortByDescendingAsync : projection:('T -> Async<'Key>) -> source:AsyncSeq<'T> -> Async<array<'T>> when 'Key : comparison
677+
666678
/// Sorts the given async sequence using the given comparison function and returns an array.
667679
/// This function returns an array that digests the whole initial sequence as soon as
668680
/// that sequence is iterated. As a result this function should not be used with

tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3409,8 +3409,56 @@ let ``AsyncSeq.sortWith returns empty array for empty sequence`` () =
34093409
let result = AsyncSeq.sortWith compare AsyncSeq.empty<int>
34103410
Assert.AreEqual([||], result)
34113411

3412+
// ── AsyncSeq.sortByAsync / sortByDescendingAsync ──────────────────────────────
3413+
3414+
[<Test>]
3415+
let ``AsyncSeq.sortByAsync sorts ascending by async projection`` () =
3416+
let source = asyncSeq { yield "banana"; yield "apple"; yield "cherry" }
3417+
let result =
3418+
source
3419+
|> AsyncSeq.sortByAsync (fun s -> async { return s.Length })
3420+
|> Async.RunSynchronously
3421+
Assert.AreEqual([| "apple"; "banana"; "cherry" |], result)
3422+
3423+
[<Test>]
3424+
let ``AsyncSeq.sortByAsync returns empty array for empty sequence`` () =
3425+
let result =
3426+
AsyncSeq.empty<int>
3427+
|> AsyncSeq.sortByAsync (fun x -> async { return x })
3428+
|> Async.RunSynchronously
3429+
Assert.AreEqual([||], result)
3430+
3431+
[<Test>]
3432+
let ``AsyncSeq.sortByAsync computes projection exactly once per element`` () =
3433+
let callCount = ref 0
3434+
let source = asyncSeq { yield 3; yield 1; yield 2 }
3435+
let result =
3436+
source
3437+
|> AsyncSeq.sortByAsync (fun x -> async { incr callCount; return x })
3438+
|> Async.RunSynchronously
3439+
Assert.AreEqual([| 1; 2; 3 |], result)
3440+
Assert.AreEqual(3, !callCount)
3441+
3442+
[<Test>]
3443+
let ``AsyncSeq.sortByDescendingAsync sorts descending by async projection`` () =
3444+
let source = asyncSeq { yield "hi"; yield "apple"; yield "cherry" }
3445+
let result =
3446+
source
3447+
|> AsyncSeq.sortByDescendingAsync (fun s -> async { return s.Length })
3448+
|> Async.RunSynchronously
3449+
Assert.AreEqual([| "cherry"; "apple"; "hi" |], result)
3450+
3451+
[<Test>]
3452+
let ``AsyncSeq.sortByDescendingAsync returns empty array for empty sequence`` () =
3453+
let result =
3454+
AsyncSeq.empty<int>
3455+
|> AsyncSeq.sortByDescendingAsync (fun x -> async { return x })
3456+
|> Async.RunSynchronously
3457+
Assert.AreEqual([||], result)
3458+
34123459
// ── AsyncSeq.mapFold ──────────────────────────────────────────────────────────
34133460

3461+
34143462
[<Test>]
34153463
let ``AsyncSeq.mapFold maps elements and accumulates state`` () =
34163464
let source = asyncSeq { yield 1; yield 2; yield 3 }

0 commit comments

Comments
 (0)