Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<Compile Include="TaskSeq.Head.Tests.fs" />
<Compile Include="TaskSeq.Indexed.Tests.fs" />
<Compile Include="TaskSeq.Init.Tests.fs" />
<Compile Include="TaskSeq.Unfold.Tests.fs" />
<Compile Include="TaskSeq.InsertAt.Tests.fs" />
<Compile Include="TaskSeq.IsEmpty.fs" />
<Compile Include="TaskSeq.Item.Tests.fs" />
Expand Down
160 changes: 160 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Unfold.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
module TaskSeq.Tests.Unfold

open Xunit
open FsUnit.Xunit

open FSharp.Control

//
// TaskSeq.unfold
// TaskSeq.unfoldAsync
//

module EmptySeq =
[<Fact>]
let ``TaskSeq-unfold generator returning None immediately yields empty sequence`` () = task {
let! result = TaskSeq.unfold (fun _ -> None) 0 |> TaskSeq.toArrayAsync

result |> should be Empty
}

[<Fact>]
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 =
[<Fact>]
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 |]
}

[<Fact>]
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 |]
}

[<Fact>]
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 |]
}

[<Fact>]
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 |]
}

[<Fact>]
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 |]
}

[<Fact>]
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 |]
}

[<Fact>]
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
}

[<Fact>]
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
}

[<Fact>]
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 |]
}

[<Fact>]
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
}

[<Fact>]
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 |]
}

[<Fact>]
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 |]
}
3 changes: 3 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 28 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeq.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,34 @@ type TaskSeq =
/// <returns>The resulting task sequence.</returns>
static member initInfiniteAsync: initializer: (int -> #Task<'T>) -> TaskSeq<'T>

/// <summary>
/// Returns a task sequence generated by applying the generator function to a state value, until it returns <c>None</c>.
/// Each call to <paramref name="generator" /> returns either <c>None</c>, which terminates the sequence, or
/// <c>Some(element, newState)</c>, which yields <paramref name="element" /> and updates the state for the next call.
/// Unlike <see cref="TaskSeq.init" />, the number of elements need not be known in advance.
/// If the generator function is asynchronous, consider using <see cref="TaskSeq.unfoldAsync" />.
/// </summary>
///
/// <param name="generator">A function that takes the current state and returns either <c>None</c> to terminate,
/// or <c>Some(element, newState)</c> to yield an element and continue with a new state.</param>
/// <param name="state">The initial state value.</param>
/// <returns>The resulting task sequence.</returns>
static member unfold: generator: ('State -> ('T * 'State) option) -> state: 'State -> TaskSeq<'T>

/// <summary>
/// Returns a task sequence generated by applying the asynchronous generator function to a state value, until it
/// returns <c>None</c>. Each call to <paramref name="generator" /> returns either <c>None</c>, which terminates the
/// sequence, or <c>Some(element, newState)</c>, which yields <paramref name="element" /> and updates the state.
/// Unlike <see cref="TaskSeq.initAsync" />, the number of elements need not be known in advance.
/// If the generator function is synchronous, consider using <see cref="TaskSeq.unfold" />.
/// </summary>
///
/// <param name="generator">An async function that takes the current state and returns either <c>None</c> to terminate,
/// or <c>Some(element, newState)</c> to yield an element and continue with a new state.</param>
/// <param name="state">The initial state value.</param>
/// <returns>The resulting task sequence.</returns>
static member unfoldAsync: generator: ('State -> Task<('T * 'State) option>) -> state: 'State -> TaskSeq<'T>

/// <summary>
/// Combines the given task sequence of task sequences and concatenates them end-to-end, to form a
/// new flattened, single task sequence, like <paramref name="TaskSeq.collect id"/>. Each task sequence is
Expand Down
26 changes: 26 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down