Skip to content

Commit 6a8cf8b

Browse files
github-actions[bot]Copilotdsyme
authored
[Repo Assist] Add AsyncSeq.mapFold, mapFoldAsync, allPairs, rev (#266)
* Add AsyncSeq.mapFold, mapFoldAsync, allPairs, rev Four new Seq-mirroring combinators: - mapFoldAsync: maps elements with an async folder that threads state, returns (results array, final state); mirrors Seq.mapFold - mapFold: synchronous variant of mapFoldAsync - allPairs: cartesian product of two async sequences; buffers second source before iteration; mirrors Seq.allPairs - rev: reverses an async sequence; buffers entire source; mirrors Seq.rev All 297 tests pass (10 new tests added). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger checks * Update RELEASE_NOTES for version 4.8.0 Updated release notes for version 4.8.0, adding new features and improvements. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Don Syme <dsyme@users.noreply.github.com>
1 parent ced9d2e commit 6a8cf8b

File tree

4 files changed

+145
-0
lines changed

4 files changed

+145
-0
lines changed

RELEASE_NOTES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
### 4.8.0
2+
3+
* Added `AsyncSeq.mapFoldAsync` — maps each element using an asynchronous folder that also threads an accumulator state, returning both the array of results and the final state; mirrors `Seq.mapFold`.
4+
* Added `AsyncSeq.mapFold` — synchronous variant of `AsyncSeq.mapFoldAsync`, mirroring `Seq.mapFold`.
5+
* Added `AsyncSeq.allPairs` — returns an async sequence of all pairs from two input sequences (cartesian product); the second source is fully buffered before iteration, mirroring `Seq.allPairs`.
6+
* Added `AsyncSeq.rev` — returns a new async sequence with all elements in reverse order; the entire source sequence is buffered before yielding, mirroring `Seq.rev`.
7+
18
### 4.7.0
29

310
* Added `AsyncSeq.splitAt` — splits a sequence at the given index, returning the first `count` elements as an array and the remaining elements as a new `AsyncSeq`. Mirrors `Seq.splitAt`. The source is enumerated once.

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,6 +1295,23 @@ module AsyncSeq =
12951295
let reduce (f: 'T -> 'T -> 'T) (source: AsyncSeq<'T>) : Async<'T> =
12961296
reduceAsync (fun a b -> f a b |> async.Return) source
12971297

1298+
let mapFoldAsync (folder: 'State -> 'T -> Async<'Result * 'State>) (state: 'State) (source: AsyncSeq<'T>) : Async<'Result array * 'State> = async {
1299+
let results = ResizeArray<'Result>()
1300+
let mutable st = state
1301+
use ie = source.GetEnumerator()
1302+
let! move = ie.MoveNext()
1303+
let b = ref move
1304+
while b.Value.IsSome do
1305+
let! (r, st') = folder st b.Value.Value
1306+
results.Add(r)
1307+
st <- st'
1308+
let! next = ie.MoveNext()
1309+
b := next
1310+
return (results.ToArray(), st) }
1311+
1312+
let mapFold (folder: 'State -> 'T -> 'Result * 'State) (state: 'State) (source: AsyncSeq<'T>) : Async<'Result array * 'State> =
1313+
mapFoldAsync (fun st x -> folder st x |> async.Return) state source
1314+
12981315
let length (source : AsyncSeq<'T>) =
12991316
fold (fun st _ -> st + 1L) 0L source
13001317

@@ -1702,6 +1719,25 @@ module AsyncSeq =
17021719
let zipWith3 (f:'T1 -> 'T2 -> 'T3 -> 'U) (source1:AsyncSeq<'T1>) (source2:AsyncSeq<'T2>) (source3:AsyncSeq<'T3>) : AsyncSeq<'U> =
17031720
zipWithAsync3 (fun a b c -> f a b c |> async.Return) source1 source2 source3
17041721

1722+
let allPairs (source1: AsyncSeq<'T1>) (source2: AsyncSeq<'T2>) : AsyncSeq<'T1 * 'T2> = asyncSeq {
1723+
let buf = System.Collections.Generic.List<'T2>()
1724+
use ie2 = source2.GetEnumerator()
1725+
let! move2 = ie2.MoveNext()
1726+
let b2 = ref move2
1727+
while b2.Value.IsSome do
1728+
buf.Add(b2.Value.Value)
1729+
let! next2 = ie2.MoveNext()
1730+
b2 := next2
1731+
use ie1 = source1.GetEnumerator()
1732+
let! move1 = ie1.MoveNext()
1733+
let b1 = ref move1
1734+
while b1.Value.IsSome do
1735+
let x = b1.Value.Value
1736+
for y in buf do
1737+
yield (x, y)
1738+
let! next1 = ie1.MoveNext()
1739+
b1 := next1 }
1740+
17051741
let zappAsync (fs:AsyncSeq<'T -> Async<'U>>) (s:AsyncSeq<'T>) : AsyncSeq<'U> =
17061742
zipWithAsync (|>) s fs
17071743

@@ -1994,6 +2030,11 @@ module AsyncSeq =
19942030

19952031
let sortWith (comparer:'T -> 'T -> int) (source:AsyncSeq<'T>) : array<'T> =
19962032
toSortedSeq (Array.sortWith comparer) source
2033+
2034+
let rev (source: AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq {
2035+
let! arr = toArrayAsync source
2036+
for i in arr.Length - 1 .. -1 .. 0 do
2037+
yield arr.[i] }
19972038
#endif
19982039

19992040
#if !FABLE_COMPILER

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,16 @@ module AsyncSeq =
258258
/// specified 'reduction' function. Raises InvalidOperationException if the sequence is empty.
259259
val reduce : reduction:('T -> 'T -> 'T) -> source:AsyncSeq<'T> -> Async<'T>
260260

261+
/// Asynchronously maps each element of the async sequence with an asynchronous folder function that
262+
/// also threads an accumulator state through the computation. Returns the array of results and the
263+
/// final state, mirroring Seq.mapFold.
264+
val mapFoldAsync : folder:('State -> 'T -> Async<'Result * 'State>) -> state:'State -> source:AsyncSeq<'T> -> Async<'Result array * 'State>
265+
266+
/// Maps each element of the async sequence with a folder function that also threads an accumulator
267+
/// state through the computation. Returns the array of results and the final state,
268+
/// mirroring Seq.mapFold.
269+
val mapFold : folder:('State -> 'T -> 'Result * 'State) -> state:'State -> source:AsyncSeq<'T> -> Async<'Result array * 'State>
270+
261271
/// Asynchronously sum the elements of the input asynchronous sequence using the specified function.
262272
val inline sum : source:AsyncSeq< ^T > -> Async< ^T>
263273
when ^T : (static member ( + ) : ^T * ^T -> ^T)
@@ -496,6 +506,10 @@ module AsyncSeq =
496506
/// The resulting sequence stops when any of the argument sequences stop.
497507
val zipWith3 : mapping:('T1 -> 'T2 -> 'T3 -> 'U) -> source1:AsyncSeq<'T1> -> source2:AsyncSeq<'T2> -> source3:AsyncSeq<'T3> -> AsyncSeq<'U>
498508

509+
/// Returns an async sequence of all pairs of elements from the two input sequences.
510+
/// The second sequence is fully buffered before iteration begins, mirroring Seq.allPairs.
511+
val allPairs : source1:AsyncSeq<'T1> -> source2:AsyncSeq<'T2> -> AsyncSeq<'T1 * 'T2>
512+
499513
/// Builds a new asynchronous sequence whose elements are generated by
500514
/// applying the specified function to all elements of the input sequence.
501515
///
@@ -654,6 +668,11 @@ module AsyncSeq =
654668
/// that sequence is iterated. As a result this function should not be used with
655669
/// large or infinite sequences.
656670
val sortWith : comparer:('T -> 'T -> int) -> source:AsyncSeq<'T> -> array<'T>
671+
672+
/// Returns a new async sequence with the elements in reverse order. The entire source
673+
/// sequence is buffered before yielding any elements, mirroring Seq.rev.
674+
/// This function should not be used with large or infinite sequences.
675+
val rev : source:AsyncSeq<'T> -> AsyncSeq<'T>
657676
#endif
658677

659678
/// Interleaves two async sequences of the same type into a resulting sequence. The provided

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

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3409,6 +3409,84 @@ 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.mapFold ──────────────────────────────────────────────────────────
3413+
3414+
[<Test>]
3415+
let ``AsyncSeq.mapFold maps elements and accumulates state`` () =
3416+
let source = asyncSeq { yield 1; yield 2; yield 3 }
3417+
let results, finalState =
3418+
AsyncSeq.mapFold (fun acc x -> (x * 2, acc + x)) 0 source |> Async.RunSynchronously
3419+
Assert.AreEqual([| 2; 4; 6 |], results)
3420+
Assert.AreEqual(6, finalState)
3421+
3422+
[<Test>]
3423+
let ``AsyncSeq.mapFold returns empty array and initial state for empty sequence`` () =
3424+
let results, finalState =
3425+
AsyncSeq.mapFold (fun acc x -> (x, acc + x)) 99 AsyncSeq.empty<int> |> Async.RunSynchronously
3426+
Assert.AreEqual([||], results)
3427+
Assert.AreEqual(99, finalState)
3428+
3429+
[<Test>]
3430+
let ``AsyncSeq.mapFoldAsync maps elements asynchronously and accumulates state`` () =
3431+
let source = asyncSeq { yield 10; yield 20; yield 30 }
3432+
let results, finalState =
3433+
AsyncSeq.mapFoldAsync (fun acc x -> async { return (x + 1, acc + x) }) 0 source
3434+
|> Async.RunSynchronously
3435+
Assert.AreEqual([| 11; 21; 31 |], results)
3436+
Assert.AreEqual(60, finalState)
3437+
3438+
[<Test>]
3439+
let ``AsyncSeq.mapFoldAsync returns empty array and initial state for empty sequence`` () =
3440+
let results, finalState =
3441+
AsyncSeq.mapFoldAsync (fun acc x -> async { return (x, acc + 1) }) 5 AsyncSeq.empty<int>
3442+
|> Async.RunSynchronously
3443+
Assert.AreEqual([||], results)
3444+
Assert.AreEqual(5, finalState)
3445+
3446+
// ── AsyncSeq.allPairs ────────────────────────────────────────────────────────
3447+
3448+
[<Test>]
3449+
let ``AsyncSeq.allPairs returns cartesian product`` () =
3450+
let s1 = asyncSeq { yield 1; yield 2 }
3451+
let s2 = asyncSeq { yield 'a'; yield 'b'; yield 'c' }
3452+
let result =
3453+
AsyncSeq.allPairs s1 s2 |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3454+
Assert.AreEqual(
3455+
[| (1,'a'); (1,'b'); (1,'c'); (2,'a'); (2,'b'); (2,'c') |],
3456+
result)
3457+
3458+
[<Test>]
3459+
let ``AsyncSeq.allPairs returns empty when first source is empty`` () =
3460+
let result =
3461+
AsyncSeq.allPairs AsyncSeq.empty<int> (asyncSeq { yield 1; yield 2 })
3462+
|> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3463+
Assert.AreEqual([||], result)
3464+
3465+
[<Test>]
3466+
let ``AsyncSeq.allPairs returns empty when second source is empty`` () =
3467+
let result =
3468+
AsyncSeq.allPairs (asyncSeq { yield 1; yield 2 }) AsyncSeq.empty<int>
3469+
|> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3470+
Assert.AreEqual([||], result)
3471+
3472+
// ── AsyncSeq.rev ─────────────────────────────────────────────────────────────
3473+
3474+
[<Test>]
3475+
let ``AsyncSeq.rev reverses a sequence`` () =
3476+
let source = asyncSeq { yield 1; yield 2; yield 3; yield 4; yield 5 }
3477+
let result = AsyncSeq.rev source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3478+
Assert.AreEqual([| 5; 4; 3; 2; 1 |], result)
3479+
3480+
[<Test>]
3481+
let ``AsyncSeq.rev returns empty sequence for empty input`` () =
3482+
let result = AsyncSeq.rev AsyncSeq.empty<int> |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3483+
Assert.AreEqual([||], result)
3484+
3485+
[<Test>]
3486+
let ``AsyncSeq.rev returns singleton for single element`` () =
3487+
let result = AsyncSeq.rev (asyncSeq { yield 42 }) |> AsyncSeq.toArrayAsync |> Async.RunSynchronously
3488+
Assert.AreEqual([| 42 |], result)
3489+
34123490
[<Test>]
34133491
let ``AsyncSeq.splitAt splits a sequence at the given index`` () =
34143492
let source = asyncSeq { yield 1; yield 2; yield 3; yield 4; yield 5 }

0 commit comments

Comments
 (0)