Skip to content

Commit 202f992

Browse files
Add async sort variants: sortAsync, sortByAsync, sortDescendingAsync, sortByDescendingAsync, sortWithAsync
The existing sort functions (sort, sortBy, sortDescending, sortByDescending, sortWith) use Async.RunSynchronously internally, which blocks a thread and cannot be composed in async workflows without escaping to a thread-pool. This PR adds five Async<'T[]>-returning counterparts for composing sort operations inside async { ... } blocks: AsyncSeq.sortAsync : AsyncSeq<'T> -> Async<'T[]> AsyncSeq.sortByAsync : ('T -> 'Key) -> AsyncSeq<'T> -> Async<'T[]> AsyncSeq.sortDescendingAsync : AsyncSeq<'T> -> Async<'T[]> AsyncSeq.sortByDescendingAsync: ('T -> 'Key) -> AsyncSeq<'T> -> Async<'T[]> AsyncSeq.sortWithAsync : ('T -> 'T -> int) -> AsyncSeq<'T> -> Async<'T[]> Each is a thin wrapper over toArrayAsync + Async.map, identical semantics to the sync counterparts but composable without blocking. 6 new tests added; all 378 existing tests continue to pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6be414a commit 202f992

File tree

4 files changed

+84
-0
lines changed

4 files changed

+84
-0
lines changed

RELEASE_NOTES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
### 4.12.0
2+
3+
* Added `AsyncSeq.sortAsync` — asynchronous variant of `sort` returning `Async<'T[]>`, avoiding `Async.RunSynchronously` in async workflows.
4+
* Added `AsyncSeq.sortByAsync` — asynchronous variant of `sortBy` returning `Async<'T[]>`.
5+
* Added `AsyncSeq.sortDescendingAsync` — asynchronous variant of `sortDescending` returning `Async<'T[]>`.
6+
* Added `AsyncSeq.sortByDescendingAsync` — asynchronous variant of `sortByDescending` returning `Async<'T[]>`.
7+
* Added `AsyncSeq.sortWithAsync` — asynchronous variant of `sortWith` returning `Async<'T[]>`.
8+
19
### 4.11.0
210

311
* Code/Performance: Modernised ~30 API functions to use `mutable` local variables instead of `ref` cells (`!`/`:=` operators). Affected: `tryLast`, `tryFirst`, `tryItem`, `compareWithAsync`, `reduceAsync`, `scanAsync`, `pairwise`, `windowed`, `pickAsync`, `tryPickAsync`, `tryFindIndex`, `tryFindIndexAsync`, `threadStateAsync`, `zipWithAsync`, `zipWithAsyncParallel`, `zipWithAsync3`, `allPairs`, `takeWhileAsync`, `takeUntilSignal`, `skipWhileAsync`, `skipWhileInclusiveAsync`, `skipUntilSignal`, `tryTail`, `splitAt`, `toArrayAsync`, `concatSeq`, `interleaveChoice`, `chunkBySize`, `chunkByAsync`, `mergeChoiceEnum`, `distinctUntilChangedWithAsync`, `emitEnumerator`, `removeAt`, `updateAt`, `insertAt`. This eliminates heap-allocated `ref`-cell objects for these variables, reducing GC pressure in hot paths, and modernises the code style to idiomatic F#.

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2244,6 +2244,21 @@ module AsyncSeq =
22442244
let sortWith (comparer:'T -> 'T -> int) (source:AsyncSeq<'T>) : array<'T> =
22452245
toSortedSeq (Array.sortWith comparer) source
22462246

2247+
let sortAsync (source:AsyncSeq<'T>) : Async<array<'T>> when 'T : comparison =
2248+
toArrayAsync source |> Async.map Array.sort
2249+
2250+
let sortByAsync (projection:'T -> 'Key) (source:AsyncSeq<'T>) : Async<array<'T>> when 'Key : comparison =
2251+
toArrayAsync source |> Async.map (Array.sortBy projection)
2252+
2253+
let sortDescendingAsync (source:AsyncSeq<'T>) : Async<array<'T>> when 'T : comparison =
2254+
toArrayAsync source |> Async.map Array.sortDescending
2255+
2256+
let sortByDescendingAsync (projection:'T -> 'Key) (source:AsyncSeq<'T>) : Async<array<'T>> when 'Key : comparison =
2257+
toArrayAsync source |> Async.map (Array.sortByDescending projection)
2258+
2259+
let sortWithAsync (comparer:'T -> 'T -> int) (source:AsyncSeq<'T>) : Async<array<'T>> =
2260+
toArrayAsync source |> Async.map (Array.sortWith comparer)
2261+
22472262
let rev (source: AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq {
22482263
let! arr = toArrayAsync source
22492264
for i in arr.Length - 1 .. -1 .. 0 do

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,26 @@ module AsyncSeq =
730730
/// large or infinite sequences.
731731
val sortWith : comparer:('T -> 'T -> int) -> source:AsyncSeq<'T> -> array<'T>
732732

733+
/// Asynchronously sorts the given async sequence and returns an Async<array<'T>>.
734+
/// Prefer this over sortAsync when composing async workflows.
735+
val sortAsync : source:AsyncSeq<'T> -> Async<array<'T>> when 'T : comparison
736+
737+
/// Asynchronously applies a key-generating function to each element of an AsyncSeq and returns
738+
/// an Async<array<'T>> ordered by keys. Prefer this over sortBy when composing async workflows.
739+
val sortByAsync : projection:('T -> 'Key) -> source:AsyncSeq<'T> -> Async<array<'T>> when 'Key : comparison
740+
741+
/// Asynchronously sorts the given async sequence in descending order and returns an Async<array<'T>>.
742+
/// Prefer this over sortDescending when composing async workflows.
743+
val sortDescendingAsync : source:AsyncSeq<'T> -> Async<array<'T>> when 'T : comparison
744+
745+
/// Asynchronously applies a key-generating function to each element of an AsyncSeq and returns
746+
/// an Async<array<'T>> ordered descending by keys. Prefer this over sortByDescending when composing async workflows.
747+
val sortByDescendingAsync : projection:('T -> 'Key) -> source:AsyncSeq<'T> -> Async<array<'T>> when 'Key : comparison
748+
749+
/// Asynchronously sorts the given async sequence using the given comparison function and returns
750+
/// an Async<array<'T>>. Prefer this over sortWith when composing async workflows.
751+
val sortWithAsync : comparer:('T -> 'T -> int) -> source:AsyncSeq<'T> -> Async<array<'T>>
752+
733753
/// Returns a new async sequence with the elements in reverse order. The entire source
734754
/// sequence is buffered before yielding any elements, mirroring Seq.rev.
735755
/// This function should not be used with large or infinite sequences.

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3456,6 +3456,47 @@ let ``AsyncSeq.sortWith returns empty array for empty sequence`` () =
34563456
let result = AsyncSeq.sortWith compare AsyncSeq.empty<int>
34573457
Assert.AreEqual([||], result)
34583458

3459+
// ===== sortAsync / sortByAsync / sortDescendingAsync / sortByDescendingAsync / sortWithAsync =====
3460+
3461+
[<Test>]
3462+
let ``AsyncSeq.sortAsync sorts in ascending order`` () =
3463+
let result = AsyncSeq.ofSeq [ 3; 1; 4; 1; 5; 9 ] |> AsyncSeq.sortAsync |> Async.RunSynchronously
3464+
Assert.AreEqual([| 1; 1; 3; 4; 5; 9 |], result)
3465+
3466+
[<Test>]
3467+
let ``AsyncSeq.sortAsync returns empty array for empty sequence`` () =
3468+
let result = AsyncSeq.empty<int> |> AsyncSeq.sortAsync |> Async.RunSynchronously
3469+
Assert.AreEqual([||], result)
3470+
3471+
[<Test>]
3472+
let ``AsyncSeq.sortByAsync sorts by projected key`` () =
3473+
let result =
3474+
AsyncSeq.ofSeq [ "banana"; "apple"; "cherry" ]
3475+
|> AsyncSeq.sortByAsync (fun s -> s.Length)
3476+
|> Async.RunSynchronously
3477+
Assert.AreEqual([| "apple"; "banana"; "cherry" |], result)
3478+
3479+
[<Test>]
3480+
let ``AsyncSeq.sortDescendingAsync sorts in descending order`` () =
3481+
let result = AsyncSeq.ofSeq [ 3; 1; 4; 1; 5 ] |> AsyncSeq.sortDescendingAsync |> Async.RunSynchronously
3482+
Assert.AreEqual([| 5; 4; 3; 1; 1 |], result)
3483+
3484+
[<Test>]
3485+
let ``AsyncSeq.sortByDescendingAsync sorts by projected key descending`` () =
3486+
let result =
3487+
AsyncSeq.ofSeq [ "apple"; "banana"; "fig" ]
3488+
|> AsyncSeq.sortByDescendingAsync (fun s -> s.Length)
3489+
|> Async.RunSynchronously
3490+
Assert.AreEqual([| "banana"; "apple"; "fig" |], result)
3491+
3492+
[<Test>]
3493+
let ``AsyncSeq.sortWithAsync sorts using custom comparer`` () =
3494+
let result =
3495+
AsyncSeq.ofSeq [ 3; 1; 4; 1; 5 ]
3496+
|> AsyncSeq.sortWithAsync (fun a b -> compare b a)
3497+
|> Async.RunSynchronously
3498+
Assert.AreEqual([| 5; 4; 3; 1; 1 |], result)
3499+
34593500
// ── AsyncSeq.mapFold ──────────────────────────────────────────────────────────
34603501

34613502
[<Test>]

0 commit comments

Comments
 (0)