diff --git a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
index 118a9417..e0e6df08 100644
--- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
+++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
@@ -25,6 +25,7 @@
+
diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Reduce.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Reduce.Tests.fs
new file mode 100644
index 00000000..11429a37
--- /dev/null
+++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Reduce.Tests.fs
@@ -0,0 +1,156 @@
+module TaskSeq.Tests.Reduce
+
+open Xunit
+open FsUnit.Xunit
+
+open FSharp.Control
+
+//
+// TaskSeq.reduce
+// TaskSeq.reduceAsync
+//
+
+module EmptySeq =
+ []
+ let ``Null source is invalid`` () =
+ assertNullArg
+ <| fun () -> TaskSeq.reduce (fun a _ -> a) null
+
+ assertNullArg
+ <| fun () -> TaskSeq.reduceAsync (fun a _ -> Task.fromResult a) null
+
+ [)>]
+ let ``TaskSeq-reduce raises on empty`` variant =
+ fun () ->
+ Gen.getEmptyVariant variant
+ |> TaskSeq.reduce (fun a b -> a + b)
+ |> Task.ignore
+
+ |> should throwAsyncExact typeof
+
+ [)>]
+ let ``TaskSeq-reduceAsync raises on empty`` variant =
+ fun () ->
+ Gen.getEmptyVariant variant
+ |> TaskSeq.reduceAsync (fun a b -> task { return a + b })
+ |> Task.ignore
+
+ |> should throwAsyncExact typeof
+
+module Immutable =
+ [)>]
+ let ``TaskSeq-reduce folds from first element`` variant = task {
+ // items are 1..10; sum = 55
+ let! sum =
+ Gen.getSeqImmutable variant
+ |> TaskSeq.reduce (fun acc item -> acc + item)
+
+ sum |> should equal 55
+ }
+
+ [)>]
+ let ``TaskSeq-reduceAsync folds from first element`` variant = task {
+ let! sum =
+ Gen.getSeqImmutable variant
+ |> TaskSeq.reduceAsync (fun acc item -> task { return acc + item })
+
+ sum |> should equal 55
+ }
+
+ []
+ let ``TaskSeq-reduce returns single element without calling folder`` () = task {
+ let mutable called = false
+
+ let! result =
+ TaskSeq.singleton 42
+ |> TaskSeq.reduce (fun _ _ ->
+ called <- true
+ failwith "should not be called")
+
+ result |> should equal 42
+ called |> should equal false
+ }
+
+ []
+ let ``TaskSeq-reduceAsync returns single element without calling folder`` () = task {
+ let mutable called = false
+
+ let! result =
+ TaskSeq.singleton 42
+ |> TaskSeq.reduceAsync (fun _ _ -> task {
+ called <- true
+ return failwith "should not be called"
+ })
+
+ result |> should equal 42
+ called |> should equal false
+ }
+
+ [)>]
+ let ``TaskSeq-reduce uses first element as initial accumulator`` variant = task {
+ // reduce must use element[0] as initial state; for 1..10 summing gives 55
+ // if it used 0 as initial, sum would also be 55 — but we verify the folder is called n-1 times
+ let mutable callCount = 0
+
+ let! sum =
+ Gen.getSeqImmutable variant
+ |> TaskSeq.reduce (fun acc item ->
+ callCount <- callCount + 1
+ acc + item)
+
+ sum |> should equal 55
+ callCount |> should equal 9 // 10 elements => 9 reduce calls
+ }
+
+ [)>]
+ let ``TaskSeq-reduce can concatenate strings`` variant = task {
+ // items 1..10 as chars: ABCDEFGHIJ
+ let! letters =
+ Gen.getSeqImmutable variant
+ |> TaskSeq.map (fun i -> string (char (i + 64)))
+ |> TaskSeq.reduce (fun acc item -> acc + item)
+
+ letters |> should equal "ABCDEFGHIJ"
+ }
+
+ [)>]
+ let ``TaskSeq-reduceAsync can concatenate strings`` variant = task {
+ let! letters =
+ Gen.getSeqImmutable variant
+ |> TaskSeq.map (fun i -> string (char (i + 64)))
+ |> TaskSeq.reduceAsync (fun acc item -> task { return acc + item })
+
+ letters |> should equal "ABCDEFGHIJ"
+ }
+
+module SideEffects =
+ [)>]
+ let ``TaskSeq-reduce folds correctly with side-effecting sequences`` variant = task {
+ let ts = Gen.getSeqWithSideEffect variant
+
+ let! sum = ts |> TaskSeq.reduce (fun acc item -> acc + item)
+
+ sum |> should equal 55
+
+ // second enumeration produces next 10 elements: 11..20, sum = 155
+ let! sum2 = ts |> TaskSeq.reduce (fun acc item -> acc + item)
+
+ sum2 |> should equal 155
+ }
+
+ [)>]
+ let ``TaskSeq-reduceAsync folds correctly with side-effecting sequences`` variant = task {
+ let ts = Gen.getSeqWithSideEffect variant
+
+ let! sum =
+ ts
+ |> TaskSeq.reduceAsync (fun acc item -> task { return acc + item })
+
+ sum |> should equal 55
+
+ let! sum2 =
+ ts
+ |> TaskSeq.reduceAsync (fun acc item -> task { return acc + item })
+
+ sum2 |> should equal 155
+ }
diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs
index 32c159fb..4406b713 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeq.fs
+++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs
@@ -406,3 +406,5 @@ type TaskSeq private () =
static member zip source1 source2 = Internal.zip source1 source2
static member fold folder state source = Internal.fold (FolderAction folder) state source
static member foldAsync folder state source = Internal.fold (AsyncFolderAction folder) state source
+ static member reduce folder source = Internal.reduce (FolderAction folder) source
+ static member reduceAsync folder source = Internal.reduce (AsyncFolderAction folder) source
diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi
index 76e90c2f..f552d83e 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi
+++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi
@@ -1350,6 +1350,38 @@ type TaskSeq =
static member foldAsync:
folder: ('State -> 'T -> #Task<'State>) -> state: 'State -> source: TaskSeq<'T> -> Task<'State>
+ ///
+ /// Applies the function 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
+ /// and the elements are , then computes
+ /// . Raises when the
+ /// sequence is empty.
+ /// If the accumulator function is asynchronous, consider using .
+ ///
+ ///
+ /// A function that updates the state with each element from the sequence.
+ /// The input sequence.
+ /// The final state value after applying the reduction function to all elements.
+ /// Thrown when the input task sequence is null.
+ /// Thrown when the input task sequence is empty.
+ static member reduce: folder: ('T -> 'T -> 'T) -> source: TaskSeq<'T> -> Task<'T>
+
+ ///
+ /// Applies the asynchronous function 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 and the elements are , then computes
+ /// . Raises when the
+ /// sequence is empty.
+ /// If the accumulator function is synchronous, consider using .
+ ///
+ ///
+ /// A function that updates the state with each element from the sequence.
+ /// The input sequence.
+ /// The final state value after applying the reduction function to all elements.
+ /// Thrown when the input task sequence is null.
+ /// Thrown when the input task sequence is empty.
+ static member reduceAsync: folder: ('T -> 'T -> #Task<'T>) -> source: TaskSeq<'T> -> Task<'T>
+
///
/// Return a new task sequence with a new item inserted before the given index.
///
diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
index a8f2a4bf..c2c16373 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
+++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
@@ -371,6 +371,37 @@ module internal TaskSeqInternal =
return result
}
+ let reduce folder (source: TaskSeq<_>) =
+ checkNonNull (nameof source) source
+
+ task {
+ use e = source.GetAsyncEnumerator CancellationToken.None
+ let! hasFirst = e.MoveNextAsync()
+
+ if not hasFirst then
+ raiseEmptySeq ()
+
+ let mutable result = e.Current
+ let! step = e.MoveNextAsync()
+ let mutable go = step
+
+ match folder with
+ | FolderAction folder ->
+ while go do
+ result <- folder result e.Current
+ let! step = e.MoveNextAsync()
+ go <- step
+
+ | AsyncFolderAction folder ->
+ while go do
+ let! tempResult = folder result e.Current
+ result <- tempResult
+ let! step = e.MoveNextAsync()
+ go <- step
+
+ return result
+ }
+
let toResizeArrayAsync source =
checkNonNull (nameof source) source