Skip to content

Commit e8e62ec

Browse files
committed
Implement TaskSeq.init, initAsync, initInfinite, initInfiniteAsync and TaskSeq.concat, plus tests and docs
1 parent 1f547ca commit e8e62ec

File tree

6 files changed

+347
-2
lines changed

6 files changed

+347
-2
lines changed

src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<Compile Include="TaskSeq.Cast.Tests.fs" />
1616
<Compile Include="TaskSeq.Choose.Tests.fs" />
1717
<Compile Include="TaskSeq.Collect.Tests.fs" />
18+
<Compile Include="TaskSeq.Concat.Tests.fs" />
1819
<Compile Include="TaskSeq.Empty.Tests.fs" />
1920
<Compile Include="TaskSeq.ExactlyOne.Tests.fs" />
2021
<Compile Include="TaskSeq.Filter.Tests.fs" />
@@ -23,6 +24,7 @@
2324
<Compile Include="TaskSeq.Fold.Tests.fs" />
2425
<Compile Include="TaskSeq.Head.Tests.fs" />
2526
<Compile Include="TaskSeq.Indexed.Tests.fs" />
27+
<Compile Include="TaskSeq.Init.Tests.fs" />
2628
<Compile Include="TaskSeq.IsEmpty.fs" />
2729
<Compile Include="TaskSeq.Item.Tests.fs" />
2830
<Compile Include="TaskSeq.Iter.Tests.fs" />
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
module FSharpy.Tests.Concat
2+
3+
open System
4+
5+
open Xunit
6+
open FsUnit.Xunit
7+
open FsToolkit.ErrorHandling
8+
9+
open FSharpy
10+
open System.Collections.Generic
11+
12+
//
13+
// TaskSeq.concat
14+
//
15+
16+
let validateSequence ts =
17+
ts
18+
|> TaskSeq.toSeqCachedAsync
19+
|> Task.map (Seq.map string)
20+
|> Task.map (String.concat "")
21+
|> Task.map (should equal "123456789101234567891012345678910")
22+
23+
module EmptySeq =
24+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
25+
let ``TaskSeq-concat with empty sequences`` variant =
26+
taskSeq {
27+
yield Gen.getEmptyVariant variant // not yield-bang!
28+
yield Gen.getEmptyVariant variant
29+
yield Gen.getEmptyVariant variant
30+
}
31+
|> TaskSeq.concat
32+
|> verifyEmpty
33+
34+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
35+
let ``TaskSeq-concat with top sequence empty`` variant =
36+
Gen.getEmptyVariant variant
37+
|> TaskSeq.box
38+
|> TaskSeq.cast<IAsyncEnumerable<int>> // casting an int to an enumerable, LOL!
39+
|> TaskSeq.concat
40+
|> verifyEmpty
41+
42+
module Immutable =
43+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
44+
let ``TaskSeq-concat with empty sequences`` variant =
45+
taskSeq {
46+
yield Gen.getSeqImmutable variant // not yield-bang!
47+
yield Gen.getSeqImmutable variant
48+
yield Gen.getSeqImmutable variant
49+
}
50+
|> TaskSeq.concat
51+
|> validateSequence
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
module FSharpy.Tests.Init
2+
3+
open System
4+
5+
open Xunit
6+
open FsUnit.Xunit
7+
open FsToolkit.ErrorHandling
8+
9+
open FSharpy
10+
11+
//
12+
// TaskSeq.init
13+
// TaskSeq.initInfinite
14+
// TaskSeq.initAsync
15+
// TaskSeq.initInfiniteAsync
16+
//
17+
18+
/// Asserts that a sequence contains the char values 'A'..'J'.
19+
20+
module EmptySeq =
21+
[<Fact>]
22+
let ``TaskSeq-init can generate an empty sequence`` () = TaskSeq.init 0 (fun x -> x) |> verifyEmpty
23+
24+
[<Fact>]
25+
let ``TaskSeq-initAsync can generate an empty sequence`` () =
26+
TaskSeq.initAsync 0 (fun x -> Task.fromResult x)
27+
|> verifyEmpty
28+
29+
[<Fact>]
30+
let ``TaskSeq-init with a negative count gives an error`` () =
31+
fun () ->
32+
TaskSeq.init -1 (fun x -> Task.fromResult x)
33+
|> TaskSeq.toArrayAsync
34+
|> Task.ignore
35+
36+
|> should throwAsyncExact typeof<ArgumentException>
37+
38+
fun () ->
39+
TaskSeq.init Int32.MinValue (fun x -> Task.fromResult x)
40+
|> TaskSeq.toArrayAsync
41+
|> Task.ignore
42+
43+
|> should throwAsyncExact typeof<ArgumentException>
44+
45+
[<Fact>]
46+
let ``TaskSeq-initAsync with a negative count gives an error`` () =
47+
fun () ->
48+
TaskSeq.initAsync Int32.MinValue (fun x -> Task.fromResult x)
49+
|> TaskSeq.toArrayAsync
50+
|> Task.ignore
51+
52+
|> should throwAsyncExact typeof<ArgumentException>
53+
54+
module Immutable =
55+
[<Fact>]
56+
let ``TaskSeq-init singleton`` () =
57+
TaskSeq.init 1 id
58+
|> TaskSeq.head
59+
|> Task.map (should equal 0)
60+
61+
[<Fact>]
62+
let ``TaskSeq-initAsync singleton`` () =
63+
TaskSeq.initAsync 1 (id >> Task.fromResult)
64+
|> TaskSeq.head
65+
|> Task.map (should equal 0)
66+
67+
[<Fact>]
68+
let ``TaskSeq-init some values`` () =
69+
TaskSeq.init 42 (fun x -> x / 2)
70+
|> TaskSeq.length
71+
|> Task.map (should equal 42)
72+
73+
[<Fact>]
74+
let ``TaskSeq-initAsync some values`` () =
75+
TaskSeq.init 42 (fun x -> Task.fromResult (x / 2))
76+
|> TaskSeq.length
77+
|> Task.map (should equal 42)
78+
79+
[<Fact>]
80+
let ``TaskSeq-initInfinite`` () =
81+
TaskSeq.initInfinite (fun x -> x / 2)
82+
|> TaskSeq.item 1_000_001
83+
|> Task.map (should equal 500_000)
84+
85+
[<Fact>]
86+
let ``TaskSeq-initInfiniteAsync`` () =
87+
TaskSeq.initInfiniteAsync (fun x -> Task.fromResult (x / 2))
88+
|> TaskSeq.item 1_000_001
89+
|> Task.map (should equal 500_000)
90+
91+
module SideEffects =
92+
let inc (i: int byref) =
93+
i <- i + 1
94+
i
95+
96+
[<Fact>]
97+
let ``TaskSeq-init singleton with side effects`` () = task {
98+
let mutable x = 0
99+
100+
let ts = TaskSeq.init 1 (fun _ -> inc &x)
101+
102+
do! TaskSeq.head ts |> Task.map (should equal 1)
103+
do! TaskSeq.head ts |> Task.map (should equal 2)
104+
do! TaskSeq.head ts |> Task.map (should equal 3) // state mutates
105+
}
106+
107+
[<Fact>]
108+
let ``TaskSeq-init singleton with side effects -- Current`` () = task {
109+
let mutable x = 0
110+
111+
let ts = TaskSeq.init 1 (fun _ -> inc &x)
112+
113+
let enumerator = ts.GetAsyncEnumerator()
114+
let! _ = enumerator.MoveNextAsync()
115+
do enumerator.Current |> should equal 1
116+
do enumerator.Current |> should equal 1
117+
do enumerator.Current |> should equal 1 // current state does not mutate
118+
}
119+
120+
[<Fact>]
121+
let ``TaskSeq-initAsync singleton with side effects`` () = task {
122+
let mutable x = 0
123+
124+
let ts = TaskSeq.initAsync 1 (fun _ -> Task.fromResult (inc &x))
125+
126+
do! TaskSeq.head ts |> Task.map (should equal 1)
127+
do! TaskSeq.head ts |> Task.map (should equal 2)
128+
do! TaskSeq.head ts |> Task.map (should equal 3) // state mutates
129+
}
130+
131+
[<Fact>]
132+
let ``TaskSeq-initAsync singleton with side effects -- Current`` () = task {
133+
let mutable x = 0
134+
135+
let ts = TaskSeq.initAsync 1 (fun _ -> Task.fromResult (inc &x))
136+
137+
let enumerator = ts.GetAsyncEnumerator()
138+
let! _ = enumerator.MoveNextAsync()
139+
do enumerator.Current |> should equal 1
140+
do enumerator.Current |> should equal 1
141+
do enumerator.Current |> should equal 1 // current state does not mutate
142+
}

src/FSharpy.TaskSeq/TaskSeq.fs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,18 @@ module TaskSeq =
156156
//
157157

158158
let length source = Internal.lengthBy None source
159+
let lengthOrMax max source = Internal.lengthBeforeMax max source
159160
let lengthBy predicate source = Internal.lengthBy (Some(Predicate predicate)) source
160161
let lengthByAsync predicate source = Internal.lengthBy (Some(PredicateAsync predicate)) source
162+
let init count initializer = Internal.init (Some count) (InitAction initializer)
163+
let initInfinite initializer = Internal.init None (InitAction initializer)
164+
let initAsync count initializer = Internal.init (Some count) (InitActionAsync initializer)
165+
let initInfiniteAsync initializer = Internal.init None (InitActionAsync initializer)
166+
167+
let concat (sources: taskSeq<#taskSeq<'T>>) = taskSeq {
168+
for ts in sources do
169+
yield! (ts :> taskSeq<'T>)
170+
}
161171

162172
//
163173
// iter/map/collect functions
@@ -262,6 +272,7 @@ module TaskSeq =
262272
| None -> return Internal.raiseNotFound ()
263273
}
264274

275+
265276
let findAsync predicate source = task {
266277
match! Internal.tryFind (PredicateAsync predicate) source with
267278
| Some item -> return item

src/FSharpy.TaskSeq/TaskSeq.fsi

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,17 @@ module TaskSeq =
1515

1616
/// <summary>
1717
/// Returns the length of the sequence. This operation requires the whole sequence to be evaluated and
18-
/// should not be used on potentially infinite sequences.
18+
/// should not be used on potentially infinite sequences, see <see cref="lengthOrMax" /> for an alternative.
1919
/// </summary>
2020
val length: source: taskSeq<'T> -> Task<int>
2121

22+
/// <summary>
23+
/// Returns the length of the sequence, or <paramref name="max" />, whichever comes first. This operation requires the task sequence
24+
/// to be evaluated in full, or until <paramref name="max" /> items have been processed. Use this method instead of
25+
/// <see cref="TaskSeq.length" /> if you want to prevent too many items to be evaluated, or if the sequence is potentially infinite.
26+
/// </summary>
27+
val lengthOrMax: max: int -> source: taskSeq<'T> -> Task<int>
28+
2229
/// <summary>
2330
/// Returns the length of the sequence of all items for which the <paramref name="predicate" /> returns true.
2431
/// This operation requires the whole sequence to be evaluated and should not be used on potentially infinite sequences.
@@ -32,6 +39,74 @@ module TaskSeq =
3239
/// </summary>
3340
val lengthByAsync: predicate: ('T -> #Task<bool>) -> source: taskSeq<'T> -> Task<int>
3441

42+
/// <summary>
43+
/// Generates a new task sequence which, when iterated, will return successive elements by calling the given function
44+
/// with the current index, up to the given count. Each element is saved after its initialization for successive access to
45+
/// <see cref="IAsyncEnumerator.Current" />, which will not re-evaluate the <paramref name="initializer" />. However,
46+
/// re-iterating the returned task sequence will re-evaluate the initialization function. The returned sequence may
47+
/// be passed between threads safely. However, individual IEnumerator values generated from the returned sequence should
48+
/// not be accessed concurrently.
49+
/// </summary>
50+
///
51+
/// <param name="count">The maximum number of items to generate for the sequence.</param>
52+
/// <param name="initializer">A function that generates an item in the sequence from a given index.</param>
53+
/// <returns>The resulting task sequence.</returns>
54+
/// <exception cref="T:System.ArgumentException">Thrown when count is negative.</exception>
55+
val init: count: int -> initializer: (int -> 'T) -> taskSeq<'T>
56+
57+
/// <summary>
58+
/// Generates a new task sequence which, when iterated, will return successive elements by calling the given function
59+
/// with the current index, up to the given count. Each element is saved after its initialization for successive access to
60+
/// <see cref="IAsyncEnumerator.Current" />, which will not re-evaluate the <paramref name="initializer" />. However,
61+
/// re-iterating the returned task sequence will re-evaluate the initialization function. The returned sequence may
62+
/// be passed between threads safely. However, individual IEnumerator values generated from the returned sequence should
63+
/// not be accessed concurrently.
64+
/// </summary>
65+
///
66+
/// <param name="count">The maximum number of items to generate for the sequence.</param>
67+
/// <param name="initializer">A function that generates an item in the sequence from a given index.</param>
68+
/// <returns>The resulting task sequence.</returns>
69+
/// <exception cref="T:System.ArgumentException">Thrown when count is negative.</exception>
70+
val initAsync: count: int -> initializer: (int -> #Task<'T>) -> taskSeq<'T>
71+
72+
/// <summary>
73+
/// Generates a new task sequence which, when iterated, will return successive elements by calling the given function
74+
/// with the current index, ad infinitum, or until <see cref="Int32.MaxValue" /> is reached.
75+
/// Each element is saved after its initialization for successive access to
76+
/// <see cref="IAsyncEnumerator.Current" />, which will not re-evaluate the <paramref name="initializer" />. However,
77+
/// re-iterating the returned task sequence will re-evaluate the initialization function. The returned sequence may
78+
/// be passed between threads safely. However, individual IEnumerator values generated from the returned sequence should
79+
/// not be accessed concurrently.
80+
/// </summary>
81+
///
82+
/// <param name="initializer">A function that generates an item in the sequence from a given index.</param>
83+
/// <returns>The resulting task sequence.</returns>
84+
val initInfinite: initializer: (int -> 'T) -> taskSeq<'T>
85+
86+
/// <summary>
87+
/// Generates a new task sequence which, when iterated, will return successive elements by calling the given function
88+
/// with the current index, ad infinitum, or until <see cref="Int32.MaxValue" /> is reached.
89+
/// Each element is saved after its initialization for successive access to
90+
/// <see cref="IAsyncEnumerator.Current" />, which will not re-evaluate the <paramref name="initializer" />. However,
91+
/// re-iterating the returned task sequence will re-evaluate the initialization function. The returned sequence may
92+
/// be passed between threads safely. However, individual IEnumerator values generated from the returned sequence should
93+
/// not be accessed concurrently.
94+
/// </summary>
95+
///
96+
/// <param name="initializer">A function that generates an item in the sequence from a given index.</param>
97+
/// <returns>The resulting task sequence.</returns>
98+
val initInfiniteAsync: initializer: (int -> #Task<'T>) -> taskSeq<'T>
99+
100+
/// <summary>
101+
/// Combines the given task sequence of task sequences and concatenates them end-to-end, to form a
102+
/// new flattened, single task sequence. Each task sequence is awaited item by item, before the next is iterated.
103+
/// </summary>
104+
///
105+
/// <param name="sources">The input enumeration-of-enumerations.</param>
106+
/// <returns>The resulting task sequence.</returns>
107+
/// <exception cref="T:ArgumentNullException">Thrown when the input sequence is null.</exception>
108+
val concat: sources: taskSeq<#taskSeq<'T>> -> taskSeq<'T>
109+
35110
/// Returns taskSeq as an array. This function is blocking until the sequence is exhausted and will properly dispose of the resources.
36111
val toList: source: taskSeq<'T> -> 'T list
37112

0 commit comments

Comments
 (0)