Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"version": "10.0.102",
"rollForward": "minor"
}
}
1 change: 1 addition & 0 deletions release-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Release notes:
- update engineering to .NET 9/10
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
- adds TaskSeq.pairwise, #289
- adds TaskSeq.mapFold and TaskSeq.mapFoldAsync

0.4.0
- overhaul all doc comments, add exceptions, improve IDE quick-info experience, #136, #220, #234
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<Compile Include="TaskSeq.Find.Tests.fs" />
<Compile Include="TaskSeq.Fold.Tests.fs" />
<Compile Include="TaskSeq.Scan.Tests.fs" />
<Compile Include="TaskSeq.MapFold.Tests.fs" />
<Compile Include="TaskSeq.Reduce.Tests.fs" />
<Compile Include="TaskSeq.Forall.Tests.fs" />
<Compile Include="TaskSeq.Head.Tests.fs" />
Expand Down
189 changes: 189 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.MapFold.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
module TaskSeq.Tests.MapFold

open Xunit
open FsUnit.Xunit

open FSharp.Control

//
// TaskSeq.mapFold
// TaskSeq.mapFoldAsync
//

module EmptySeq =
[<Fact>]
let ``Null source is invalid`` () =
assertNullArg
<| fun () -> TaskSeq.mapFold (fun _ item -> string item, 0) 0 null

assertNullArg
<| fun () -> TaskSeq.mapFoldAsync (fun _ item -> Task.fromResult (string item, 0)) 0 null

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-mapFold on empty returns empty array with initial state`` variant = task {
let! results, finalState =
Gen.getEmptyVariant variant
|> TaskSeq.mapFold (fun state item -> item * 2, state + item) 0

results |> should equal [||]
finalState |> should equal 0
}

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-mapFoldAsync on empty returns empty array with initial state`` variant = task {
let! results, finalState =
Gen.getEmptyVariant variant
|> TaskSeq.mapFoldAsync (fun state item -> task { return item * 2, state + item }) 0

results |> should equal [||]
finalState |> should equal 0
}

module Functionality =
[<Fact>]
let ``TaskSeq-mapFold maps elements while threading state`` () = task {
// mapFold: map each element to its double, sum all originals as state
let! results, finalState =
TaskSeq.ofList [ 1; 2; 3; 4; 5 ]
|> TaskSeq.mapFold (fun state item -> item * 2, state + item) 0

results |> should equal [| 2; 4; 6; 8; 10 |]
finalState |> should equal 15 // 1+2+3+4+5
}

[<Fact>]
let ``TaskSeq-mapFoldAsync maps elements while threading state`` () = task {
let! results, finalState =
TaskSeq.ofList [ 1; 2; 3; 4; 5 ]
|> TaskSeq.mapFoldAsync (fun state item -> task { return item * 2, state + item }) 0

results |> should equal [| 2; 4; 6; 8; 10 |]
finalState |> should equal 15
}

[<Fact>]
let ``TaskSeq-mapFold returns array of same length as source`` () = task {
let! results, _ =
TaskSeq.ofList [ 'a'; 'b'; 'c' ]
|> TaskSeq.mapFold (fun idx c -> string c, idx + 1) 0

results |> should equal [| "a"; "b"; "c" |]
}

[<Fact>]
let ``TaskSeq-mapFold single element returns singleton array and updated state`` () = task {
let! results, finalState =
TaskSeq.singleton 42
|> TaskSeq.mapFold (fun state item -> item + 1, state + item) 10

results |> should equal [| 43 |]
finalState |> should equal 52
}

[<Fact>]
let ``TaskSeq-mapFold state threads through in order`` () = task {
// Build running index as state; mapped element is (index, item) pair
let! results, finalState =
TaskSeq.ofList [ 10; 20; 30 ]
|> TaskSeq.mapFold (fun idx item -> (idx, item), idx + 1) 0

results |> should equal [| (0, 10); (1, 20); (2, 30) |]
finalState |> should equal 3
}

[<Fact>]
let ``TaskSeq-mapFoldAsync state threads through in order`` () = task {
let! results, finalState =
TaskSeq.ofList [ 10; 20; 30 ]
|> TaskSeq.mapFoldAsync (fun idx item -> task { return (idx, item), idx + 1 }) 0

results |> should equal [| (0, 10); (1, 20); (2, 30) |]
finalState |> should equal 3
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-mapFold accumulates correctly across variants`` variant = task {
// Input 1..10; mapped = item*item; state = running sum
let! results, finalState =
Gen.getSeqImmutable variant
|> TaskSeq.mapFold (fun acc item -> item * item, acc + item) 0

results
|> should equal [| 1; 4; 9; 16; 25; 36; 49; 64; 81; 100 |]

finalState |> should equal 55 // 1+2+...+10
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-mapFoldAsync accumulates correctly across variants`` variant = task {
let! results, finalState =
Gen.getSeqImmutable variant
|> TaskSeq.mapFoldAsync (fun acc item -> task { return item * item, acc + item }) 0

results
|> should equal [| 1; 4; 9; 16; 25; 36; 49; 64; 81; 100 |]

finalState |> should equal 55
}

[<Fact>]
let ``TaskSeq-mapFold result matches equivalent List.mapFold`` () = task {
let items = [ 1; 2; 3; 4; 5 ]

let listResults, listState = List.mapFold (fun state item -> item + state, state + item) 0 items

let! taskResults, taskState =
TaskSeq.ofList items
|> TaskSeq.mapFold (fun state item -> item + state, state + item) 0

taskResults |> should equal (Array.ofList listResults)
taskState |> should equal listState
}

module SideEffects =
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
let ``TaskSeq-mapFold second iteration sees next batch of side-effect values`` variant = task {
let ts = Gen.getSeqWithSideEffect variant

let! first, firstState =
ts
|> TaskSeq.mapFold (fun acc item -> item * 2, acc + item) 0

first
|> should equal [| 2; 4; 6; 8; 10; 12; 14; 16; 18; 20 |]

firstState |> should equal 55

// side-effect sequences yield next 10 items (11..20) on second consumption
let! second, secondState =
ts
|> TaskSeq.mapFold (fun acc item -> item * 2, acc + item) 0

second
|> should equal [| 22; 24; 26; 28; 30; 32; 34; 36; 38; 40 |]

secondState |> should equal 155 // 11+12+...+20
}

[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
let ``TaskSeq-mapFoldAsync second iteration sees next batch of side-effect values`` variant = task {
let ts = Gen.getSeqWithSideEffect variant

let! first, firstState =
ts
|> TaskSeq.mapFoldAsync (fun acc item -> task { return item * 2, acc + item }) 0

first
|> should equal [| 2; 4; 6; 8; 10; 12; 14; 16; 18; 20 |]

firstState |> should equal 55

let! second, secondState =
ts
|> TaskSeq.mapFoldAsync (fun acc item -> task { return item * 2, acc + item }) 0

second
|> should equal [| 22; 24; 26; 28; 30; 32; 34; 36; 38; 40 |]

secondState |> should equal 155
}
2 changes: 2 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -414,3 +414,5 @@ type TaskSeq private () =
static member scanAsync folder state source = Internal.scan (AsyncFolderAction folder) state source
static member reduce folder source = Internal.reduce (FolderAction folder) source
static member reduceAsync folder source = Internal.reduce (AsyncFolderAction folder) source
static member mapFold mapping state source = Internal.mapFold (MapFolderAction mapping) state source
static member mapFoldAsync mapping state source = Internal.mapFold (AsyncMapFolderAction mapping) state source
39 changes: 37 additions & 2 deletions src/FSharp.Control.TaskSeq/TaskSeq.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -1419,8 +1419,43 @@ type TaskSeq =
folder: ('State -> 'T -> #Task<'State>) -> state: 'State -> source: TaskSeq<'T> -> TaskSeq<'State>

/// <summary>
/// Applies the function <paramref name="folder" /> to each element of the task sequence, threading an accumulator
/// argument through the computation. The first element is used as the initial state. If the input function is
/// Applies the function <paramref name="mapping" /> to each element of the task sequence, threading an accumulator
/// argument through the computation, while also generating a new mapped element for each input element.
/// If the input function is <paramref name="f" /> and the elements are <paramref name="i0...iN" />, then
/// computes both the mapped results <paramref name="r0...rN" /> and the final state in a single pass.
/// The result is a pair of an array of mapped values and the final state.
/// If the mapping function <paramref name="mapping" /> is asynchronous, consider using <see cref="TaskSeq.mapFoldAsync" />.
/// </summary>
///
/// <param name="mapping">A function that maps each element to a result while also updating the state.</param>
/// <param name="state">The initial state.</param>
/// <param name="source">The input task sequence.</param>
/// <returns>A task returning a pair of the array of mapped results and the final state.</returns>
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
static member mapFold:
mapping: ('State -> 'T -> 'Result * 'State) -> state: 'State -> source: TaskSeq<'T> -> Task<'Result[] * 'State>

/// <summary>
/// Applies the asynchronous function <paramref name="mapping" /> to each element of the task sequence,
/// threading an accumulator argument through the computation, while also generating a new mapped element for each input element.
/// If the input function is <paramref name="f" /> and the elements are <paramref name="i0...iN" />, then
/// computes both the mapped results <paramref name="r0...rN" /> and the final state in a single pass.
/// The result is a pair of an array of mapped values and the final state.
/// If the mapping function <paramref name="mapping" /> is synchronous, consider using <see cref="TaskSeq.mapFold" />.
/// </summary>
///
/// <param name="mapping">An asynchronous function that maps each element to a result while also updating the state.</param>
/// <param name="state">The initial state.</param>
/// <param name="source">The input task sequence.</param>
/// <returns>A task returning a pair of the array of mapped results and the final state.</returns>
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
static member mapFoldAsync:
mapping: ('State -> 'T -> #Task<'Result * 'State>) ->
state: 'State ->
source: TaskSeq<'T> ->
Task<'Result[] * 'State>


/// <paramref name="f" /> and the elements are <paramref name="i0...iN" />, then computes
/// <paramref name="f (... (f i0 i1)...) iN" />. Raises <see cref="T:System.ArgumentException" /> when the
/// sequence is empty.
Expand Down
36 changes: 36 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ type internal InitAction<'T, 'TaskT when 'TaskT :> Task<'T>> =
| InitAction of init_item: (int -> 'T)
| InitActionAsync of async_init_item: (int -> 'TaskT)

[<Struct>]
type internal MapFolderAction<'T, 'State, 'Result, 'TaskResultState when 'TaskResultState :> Task<'Result * 'State>> =
| MapFolderAction of map_folder_action: ('State -> 'T -> 'Result * 'State)
| AsyncMapFolderAction of async_map_folder_action: ('State -> 'T -> 'TaskResultState)

[<Struct>]
type internal ManyOrOne<'T> =
| Many of source_seq: TaskSeq<'T>
Expand Down Expand Up @@ -451,6 +456,37 @@ module internal TaskSeqInternal =
return result
}

let mapFold (folder: MapFolderAction<_, _, _, _>) initial (source: TaskSeq<_>) =
checkNonNull (nameof source) source

task {
use e = source.GetAsyncEnumerator CancellationToken.None
let mutable go = true
let mutable state = initial
let results = ResizeArray()
let! step = e.MoveNextAsync()
go <- step

match folder with
| MapFolderAction folder ->
while go do
let result, newState = folder state e.Current
results.Add result
state <- newState
let! step = e.MoveNextAsync()
go <- step

| AsyncMapFolderAction folder ->
while go do
let! (result, newState) = folder state e.Current
results.Add result
state <- newState
let! step = e.MoveNextAsync()
go <- step

return results.ToArray(), state
}

let toResizeArrayAsync source =
checkNonNull (nameof source) source

Expand Down