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..367c0bfe 100644
--- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
+++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
@@ -29,6 +29,7 @@
+
diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Unfold.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Unfold.Tests.fs
new file mode 100644
index 00000000..6cbec400
--- /dev/null
+++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Unfold.Tests.fs
@@ -0,0 +1,160 @@
+module TaskSeq.Tests.Unfold
+
+open Xunit
+open FsUnit.Xunit
+
+open FSharp.Control
+
+//
+// TaskSeq.unfold
+// TaskSeq.unfoldAsync
+//
+
+module EmptySeq =
+ []
+ let ``TaskSeq-unfold generator returning None immediately yields empty sequence`` () = task {
+ let! result = TaskSeq.unfold (fun _ -> None) 0 |> TaskSeq.toArrayAsync
+
+ result |> should be Empty
+ }
+
+ []
+ let ``TaskSeq-unfoldAsync generator returning None immediately yields empty sequence`` () = task {
+ let! result =
+ TaskSeq.unfoldAsync (fun _ -> task { return None }) 0
+ |> TaskSeq.toArrayAsync
+
+ result |> should be Empty
+ }
+
+module Functionality =
+ []
+ let ``TaskSeq-unfold generates a finite sequence`` () = task {
+ // unfold 0..9
+ let! result =
+ TaskSeq.unfold (fun n -> if n < 10 then Some(n, n + 1) else None) 0
+ |> TaskSeq.toArrayAsync
+
+ result |> should equal [| 0..9 |]
+ }
+
+ []
+ let ``TaskSeq-unfoldAsync generates a finite sequence`` () = task {
+ let! result =
+ TaskSeq.unfoldAsync (fun n -> task { return if n < 10 then Some(n, n + 1) else None }) 0
+ |> TaskSeq.toArrayAsync
+
+ result |> should equal [| 0..9 |]
+ }
+
+ []
+ let ``TaskSeq-unfold generates a singleton sequence`` () = task {
+ let! result =
+ TaskSeq.unfold (fun s -> if s = 0 then Some(42, 1) else None) 0
+ |> TaskSeq.toArrayAsync
+
+ result |> should equal [| 42 |]
+ }
+
+ []
+ let ``TaskSeq-unfoldAsync generates a singleton sequence`` () = task {
+ let! result =
+ TaskSeq.unfoldAsync (fun s -> task { return if s = 0 then Some(42, 1) else None }) 0
+ |> TaskSeq.toArrayAsync
+
+ result |> should equal [| 42 |]
+ }
+
+ []
+ let ``TaskSeq-unfold uses state correctly to thread accumulator`` () = task {
+ // Fibonacci: state = (a, b), yield a, new state = (b, a+b)
+ let! fibs =
+ TaskSeq.unfold (fun (a, b) -> if a > 100 then None else Some(a, (b, a + b))) (1, 1)
+ |> TaskSeq.toArrayAsync
+
+ fibs
+ |> should equal [| 1; 1; 2; 3; 5; 8; 13; 21; 34; 55; 89 |]
+ }
+
+ []
+ let ``TaskSeq-unfoldAsync uses state correctly to thread accumulator`` () = task {
+ let! fibs =
+ TaskSeq.unfoldAsync (fun (a, b) -> task { return if a > 100 then None else Some(a, (b, a + b)) }) (1, 1)
+ |> TaskSeq.toArrayAsync
+
+ fibs
+ |> should equal [| 1; 1; 2; 3; 5; 8; 13; 21; 34; 55; 89 |]
+ }
+
+ []
+ let ``TaskSeq-unfold can be truncated to limit infinite-like sequences`` () = task {
+ // counters counting from 1 upward, take first 100
+ let! result =
+ TaskSeq.unfold (fun n -> Some(n, n + 1)) 1
+ |> TaskSeq.take 100
+ |> TaskSeq.toArrayAsync
+
+ result |> should equal [| 1..100 |]
+ result |> Array.length |> should equal 100
+ }
+
+ []
+ let ``TaskSeq-unfoldAsync can be truncated to limit infinite-like sequences`` () = task {
+ let! result =
+ TaskSeq.unfoldAsync (fun n -> task { return Some(n, n + 1) }) 1
+ |> TaskSeq.take 100
+ |> TaskSeq.toArrayAsync
+
+ result |> should equal [| 1..100 |]
+ result |> Array.length |> should equal 100
+ }
+
+ []
+ let ``TaskSeq-unfold generates string sequences from state`` () = task {
+ // build "A", "B", ..., "Z"
+ let! letters =
+ TaskSeq.unfold (fun c -> if c > int 'Z' then None else Some(string (char c), c + 1)) (int 'A')
+ |> TaskSeq.toArrayAsync
+
+ letters
+ |> should equal [| for c in 'A' .. 'Z' -> string c |]
+ }
+
+ []
+ let ``TaskSeq-unfold calls generator exactly once per element plus one final None call`` () = task {
+ let mutable callCount = 0
+
+ let! result =
+ TaskSeq.unfold
+ (fun n ->
+ callCount <- callCount + 1
+
+ if n < 5 then Some(n, n + 1) else None)
+ 0
+ |> TaskSeq.toArrayAsync
+
+ result |> should equal [| 0..4 |]
+ callCount |> should equal 6 // 5 Some + 1 None
+ }
+
+ []
+ let ``TaskSeq-unfold re-iterating restarts from initial state`` () = task {
+ let ts = TaskSeq.unfold (fun n -> if n < 5 then Some(n, n + 1) else None) 0
+
+ let! first = ts |> TaskSeq.toArrayAsync
+ let! second = ts |> TaskSeq.toArrayAsync
+
+ first |> should equal second
+ first |> should equal [| 0..4 |]
+ }
+
+ []
+ let ``TaskSeq-unfoldAsync re-iterating restarts from initial state`` () = task {
+ let ts = TaskSeq.unfoldAsync (fun n -> task { return if n < 5 then Some(n, n + 1) else None }) 0
+
+ let! first = ts |> TaskSeq.toArrayAsync
+ let! second = ts |> TaskSeq.toArrayAsync
+
+ first |> should equal second
+ first |> should equal [| 0..4 |]
+ }
diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs
index 32c159fb..68707544 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeq.fs
+++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs
@@ -175,6 +175,9 @@ type TaskSeq private () =
static member initAsync count initializer = Internal.init (Some count) (InitActionAsync initializer)
static member initInfiniteAsync initializer = Internal.init None (InitActionAsync initializer)
+ static member unfold generator state = Internal.unfold generator state
+ static member unfoldAsync generator state = Internal.unfoldAsync generator state
+
static member delay(generator: unit -> TaskSeq<'T>) =
{ new IAsyncEnumerable<'T> with
member _.GetAsyncEnumerator(ct) = generator().GetAsyncEnumerator(ct)
diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi
index 76e90c2f..ab4304b4 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi
+++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi
@@ -211,6 +211,34 @@ type TaskSeq =
/// The resulting task sequence.
static member initInfiniteAsync: initializer: (int -> #Task<'T>) -> TaskSeq<'T>
+ ///
+ /// Returns a task sequence generated by applying the generator function to a state value, until it returns None.
+ /// Each call to returns either None, which terminates the sequence, or
+ /// Some(element, newState), which yields and updates the state for the next call.
+ /// Unlike , the number of elements need not be known in advance.
+ /// If the generator function is asynchronous, consider using .
+ ///
+ ///
+ /// A function that takes the current state and returns either None to terminate,
+ /// or Some(element, newState) to yield an element and continue with a new state.
+ /// The initial state value.
+ /// The resulting task sequence.
+ static member unfold: generator: ('State -> ('T * 'State) option) -> state: 'State -> TaskSeq<'T>
+
+ ///
+ /// Returns a task sequence generated by applying the asynchronous generator function to a state value, until it
+ /// returns None. Each call to returns either None, which terminates the
+ /// sequence, or Some(element, newState), which yields and updates the state.
+ /// Unlike , the number of elements need not be known in advance.
+ /// If the generator function is synchronous, consider using .
+ ///
+ ///
+ /// An async function that takes the current state and returns either None to terminate,
+ /// or Some(element, newState) to yield an element and continue with a new state.
+ /// The initial state value.
+ /// The resulting task sequence.
+ static member unfoldAsync: generator: ('State -> Task<('T * 'State) option>) -> state: 'State -> TaskSeq<'T>
+
///
/// Combines the given task sequence of task sequences and concatenates them end-to-end, to form a
/// new flattened, single task sequence, like . Each task sequence is
diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
index a8f2a4bf..344d65ef 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
+++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
@@ -300,6 +300,32 @@ module internal TaskSeqInternal =
}
+ let unfold generator state = taskSeq {
+ let mutable go = true
+ let mutable currentState = state
+
+ while go do
+ match generator currentState with
+ | None -> go <- false
+ | Some(value, nextState) ->
+ yield value
+ currentState <- nextState
+ }
+
+ let unfoldAsync generator state = taskSeq {
+ let mutable go = true
+ let mutable currentState = state
+
+ while go do
+ let! result = (generator currentState: Task<_>)
+
+ match result with
+ | None -> go <- false
+ | Some(value, nextState) ->
+ yield value
+ currentState <- nextState
+ }
+
let iter action (source: TaskSeq<_>) =
checkNonNull (nameof source) source