Skip to content

Commit a5f412d

Browse files
committed
Adds ValueOption variants for choose
1 parent f3f20ee commit a5f412d

File tree

5 files changed

+298
-0
lines changed

5 files changed

+298
-0
lines changed

src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<Compile Include="TaskSeq.Append.Tests.fs" />
1313
<Compile Include="TaskSeq.Cast.Tests.fs" />
1414
<Compile Include="TaskSeq.Choose.Tests.fs" />
15+
<Compile Include="TaskSeq.ChooseV.Tests.fs" />
1516
<Compile Include="TaskSeq.Collect.Tests.fs" />
1617
<Compile Include="TaskSeq.Concat.Tests.fs" />
1718
<Compile Include="TaskSeq.Contains.Tests.fs" />
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
module TaskSeq.Tests.ChooseV
2+
3+
open System
4+
5+
open Xunit
6+
open FsUnit.Xunit
7+
8+
open FSharp.Control
9+
10+
//
11+
// TaskSeq.chooseV
12+
// TaskSeq.chooseVAsync
13+
//
14+
15+
module EmptySeq =
16+
[<Fact>]
17+
let ``Null source is invalid`` () =
18+
assertNullArg
19+
<| fun () -> TaskSeq.chooseV (fun _ -> ValueNone) null
20+
21+
assertNullArg
22+
<| fun () -> TaskSeq.chooseVAsync (fun _ -> Task.fromResult ValueNone) null
23+
24+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
25+
let ``TaskSeq-chooseV`` variant = task {
26+
let! empty =
27+
Gen.getEmptyVariant variant
28+
|> TaskSeq.chooseV (fun _ -> ValueSome 42)
29+
|> TaskSeq.toListAsync
30+
31+
List.isEmpty empty |> should be True
32+
}
33+
34+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
35+
let ``TaskSeq-chooseVAsync`` variant = task {
36+
let! empty =
37+
Gen.getEmptyVariant variant
38+
|> TaskSeq.chooseVAsync (fun _ -> task { return ValueSome 42 })
39+
|> TaskSeq.toListAsync
40+
41+
List.isEmpty empty |> should be True
42+
}
43+
44+
module Immutable =
45+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
46+
let ``TaskSeq-chooseV can convert and filter`` variant = task {
47+
let chooser number =
48+
if number <= 5 then
49+
ValueSome(char number + '@')
50+
else
51+
ValueNone
52+
53+
let ts = Gen.getSeqImmutable variant
54+
55+
let! letters1 = TaskSeq.chooseV chooser ts |> TaskSeq.toArrayAsync
56+
let! letters2 = TaskSeq.chooseV chooser ts |> TaskSeq.toArrayAsync
57+
58+
String letters1 |> should equal "ABCDE"
59+
String letters2 |> should equal "ABCDE"
60+
}
61+
62+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
63+
let ``TaskSeq-chooseVAsync can convert and filter`` variant = task {
64+
let chooser number = task {
65+
return
66+
if number <= 5 then
67+
ValueSome(char number + '@')
68+
else
69+
ValueNone
70+
}
71+
72+
let ts = Gen.getSeqImmutable variant
73+
74+
let! letters1 = TaskSeq.chooseVAsync chooser ts |> TaskSeq.toArrayAsync
75+
let! letters2 = TaskSeq.chooseVAsync chooser ts |> TaskSeq.toArrayAsync
76+
77+
String letters1 |> should equal "ABCDE"
78+
String letters2 |> should equal "ABCDE"
79+
}
80+
81+
module Immutable2 =
82+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
83+
let ``TaskSeq-chooseV returns all when chooser always returns ValueSome`` variant = task {
84+
let ts = Gen.getSeqImmutable variant
85+
let! xs = ts |> TaskSeq.chooseV ValueSome |> TaskSeq.toArrayAsync
86+
xs |> should equal [| 1..10 |]
87+
}
88+
89+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
90+
let ``TaskSeq-chooseVAsync returns all when chooser always returns ValueSome`` variant = task {
91+
let ts = Gen.getSeqImmutable variant
92+
93+
let! xs =
94+
ts
95+
|> TaskSeq.chooseVAsync (fun x -> task { return ValueSome x })
96+
|> TaskSeq.toArrayAsync
97+
98+
xs |> should equal [| 1..10 |]
99+
}
100+
101+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
102+
let ``TaskSeq-chooseV returns empty when chooser always returns ValueNone`` variant = task {
103+
let ts = Gen.getSeqImmutable variant
104+
105+
do! ts |> TaskSeq.chooseV (fun _ -> ValueNone) |> verifyEmpty
106+
}
107+
108+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
109+
let ``TaskSeq-chooseVAsync returns empty when chooser always returns ValueNone`` variant = task {
110+
let ts = Gen.getSeqImmutable variant
111+
112+
do!
113+
ts
114+
|> TaskSeq.chooseVAsync (fun _ -> task { return ValueNone })
115+
|> verifyEmpty
116+
}
117+
118+
[<Fact>]
119+
let ``TaskSeq-chooseV with singleton sequence and ValueSome chooser returns singleton`` () = task {
120+
let! xs =
121+
taskSeq { yield 42 }
122+
|> TaskSeq.chooseV (fun x -> ValueSome(x * 2))
123+
|> TaskSeq.toListAsync
124+
125+
xs |> should equal [ 84 ]
126+
}
127+
128+
[<Fact>]
129+
let ``TaskSeq-chooseV with singleton sequence and ValueNone chooser returns empty`` () =
130+
taskSeq { yield 42 }
131+
|> TaskSeq.chooseV (fun _ -> ValueNone)
132+
|> verifyEmpty
133+
134+
[<Fact>]
135+
let ``TaskSeq-chooseV can change the element type`` () = task {
136+
// choose maps int -> string voption, verifying type-changing behavior
137+
let chooser n =
138+
if n % 2 = 0 then
139+
ValueSome(sprintf "even-%d" n)
140+
else
141+
ValueNone
142+
143+
let! xs =
144+
taskSeq { yield! [ 1..6 ] }
145+
|> TaskSeq.chooseV chooser
146+
|> TaskSeq.toListAsync
147+
148+
xs |> should equal [ "even-2"; "even-4"; "even-6" ]
149+
}
150+
151+
[<Fact>]
152+
let ``TaskSeq-chooseVAsync can change the element type`` () = task {
153+
let chooser n = task {
154+
return
155+
if n % 2 = 0 then
156+
ValueSome(sprintf "even-%d" n)
157+
else
158+
ValueNone
159+
}
160+
161+
let! xs =
162+
taskSeq { yield! [ 1..6 ] }
163+
|> TaskSeq.chooseVAsync chooser
164+
|> TaskSeq.toListAsync
165+
166+
xs |> should equal [ "even-2"; "even-4"; "even-6" ]
167+
}
168+
169+
module SideEffects =
170+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
171+
let ``TaskSeq-chooseV applied multiple times`` variant = task {
172+
let ts = Gen.getSeqWithSideEffect variant
173+
174+
let chooser x number =
175+
if number <= x then
176+
ValueSome(char number + '@')
177+
else
178+
ValueNone
179+
180+
let! lettersA = ts |> TaskSeq.chooseV (chooser 5) |> TaskSeq.toArrayAsync
181+
let! lettersK = ts |> TaskSeq.chooseV (chooser 15) |> TaskSeq.toArrayAsync
182+
let! lettersU = ts |> TaskSeq.chooseV (chooser 25) |> TaskSeq.toArrayAsync
183+
184+
String lettersA |> should equal "ABCDE"
185+
String lettersK |> should equal "KLMNO"
186+
String lettersU |> should equal "UVWXY"
187+
}
188+
189+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
190+
let ``TaskSeq-chooseVAsync applied multiple times`` variant = task {
191+
let ts = Gen.getSeqWithSideEffect variant
192+
193+
let chooser x number = task {
194+
return
195+
if number <= x then
196+
ValueSome(char number + '@')
197+
else
198+
ValueNone
199+
}
200+
201+
let! lettersA = TaskSeq.chooseVAsync (chooser 5) ts |> TaskSeq.toArrayAsync
202+
let! lettersK = TaskSeq.chooseVAsync (chooser 15) ts |> TaskSeq.toArrayAsync
203+
let! lettersU = TaskSeq.chooseVAsync (chooser 25) ts |> TaskSeq.toArrayAsync
204+
205+
String lettersA |> should equal "ABCDE"
206+
String lettersK |> should equal "KLMNO"
207+
String lettersU |> should equal "UVWXY"
208+
}
209+
210+
[<Fact>]
211+
let ``TaskSeq-chooseV evaluates each source element exactly once`` () = task {
212+
let mutable count = 0
213+
214+
let ts = taskSeq {
215+
for i in 1..5 do
216+
count <- count + 1
217+
yield i
218+
}
219+
220+
let! xs =
221+
ts
222+
|> TaskSeq.chooseV (fun x -> if x < 3 then ValueSome x else ValueNone)
223+
|> TaskSeq.toListAsync
224+
225+
count |> should equal 5 // all 5 elements were visited
226+
xs |> should equal [ 1; 2 ]
227+
}
228+
229+
[<Fact>]
230+
let ``TaskSeq-chooseVAsync evaluates each source element exactly once`` () = task {
231+
let mutable count = 0
232+
233+
let ts = taskSeq {
234+
for i in 1..5 do
235+
count <- count + 1
236+
yield i
237+
}
238+
239+
let! xs =
240+
ts
241+
|> TaskSeq.chooseVAsync (fun x -> task { return if x < 3 then ValueSome x else ValueNone })
242+
|> TaskSeq.toListAsync
243+
244+
count |> should equal 5
245+
xs |> should equal [ 1; 2 ]
246+
}

src/FSharp.Control.TaskSeq/TaskSeq.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,9 @@ type TaskSeq private () =
441441
}
442442

443443
static member choose chooser source = Internal.choose (TryPick chooser) source
444+
static member chooseV chooser source = Internal.chooseV (TryPickV chooser) source
444445
static member chooseAsync chooser source = Internal.choose (TryPickAsync chooser) source
446+
static member chooseVAsync chooser source = Internal.chooseV (TryPickVAsync chooser) source
445447

446448
static member filter predicate source = Internal.filter (Predicate predicate) source
447449
static member filterAsync predicate source = Internal.filter (PredicateAsync predicate) source

src/FSharp.Control.TaskSeq/TaskSeq.fsi

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,18 @@ type TaskSeq =
10111011
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
10121012
static member choose: chooser: ('T -> 'U option) -> source: TaskSeq<'T> -> TaskSeq<'U>
10131013

1014+
/// <summary>
1015+
/// Applies the given function <paramref name="chooser" /> to each element of the task sequence. Returns
1016+
/// a sequence comprised of the results where the function returns <see cref="ValueSome(x)" />.
1017+
/// If <paramref name="chooser" /> is asynchronous, consider using <see cref="TaskSeq.chooseAsync" />.
1018+
/// </summary>
1019+
///
1020+
/// <param name="chooser">A function to transform items of type <paramref name="'T" /> into value options of type <paramref name="'U" />.</param>
1021+
/// <param name="source">The input task sequence.</param>
1022+
/// <returns>The resulting task sequence.</returns>
1023+
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
1024+
static member chooseV: chooser: ('T -> 'U voption) -> source: TaskSeq<'T> -> TaskSeq<'U>
1025+
10141026
/// <summary>
10151027
/// Applies the given asynchronous function <paramref name="chooser" /> to each element of the task sequence.
10161028
/// Returns a sequence comprised of the results where the function returns a <see cref="task" /> result
@@ -1024,6 +1036,19 @@ type TaskSeq =
10241036
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
10251037
static member chooseAsync: chooser: ('T -> #Task<'U option>) -> source: TaskSeq<'T> -> TaskSeq<'U>
10261038

1039+
/// <summary>
1040+
/// Applies the given asynchronous function <paramref name="chooser" /> to each element of the task sequence.
1041+
/// Returns a sequence comprised of the results where the function returns a <see cref="task" /> result
1042+
/// of <see cref="ValueSome(x)" />.
1043+
/// If <paramref name="chooser" /> is synchronous, consider using <see cref="TaskSeq.chooseV" />.
1044+
/// </summary>
1045+
///
1046+
/// <param name="chooser">An asynchronous function to transform items of type <paramref name="'T" /> into value options of type <paramref name="'U" />.</param>
1047+
/// <param name="source">The input task sequence.</param>
1048+
/// <returns>The resulting task sequence.</returns>
1049+
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
1050+
static member chooseVAsync: chooser: ('T -> #Task<'U voption>) -> source: TaskSeq<'T> -> TaskSeq<'U>
1051+
10271052
/// <summary>
10281053
/// Returns a new task sequence containing only the elements of the collection
10291054
/// for which the given function <paramref name="predicate" /> returns <see cref="true" />.

src/FSharp.Control.TaskSeq/TaskSeqInternal.fs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ type internal ChooserAction<'T, 'U, 'TaskOption when 'TaskOption :> Task<'U opti
3939
| TryPick of try_pick: ('T -> 'U option)
4040
| TryPickAsync of async_try_pick: ('T -> 'TaskOption)
4141

42+
[<Struct>]
43+
type internal ChooserVAction<'T, 'U, 'TaskValueOption when 'TaskValueOption :> Task<'U voption>> =
44+
| TryPickV of try_pickv: ('T -> 'U voption)
45+
| TryPickVAsync of async_try_pickv: ('T -> 'TaskValueOption)
46+
4247
[<Struct>]
4348
type internal PredicateAction<'T, 'TaskBool when 'TaskBool :> Task<bool>> =
4449
| Predicate of try_filter: ('T -> bool)
@@ -1055,6 +1060,25 @@ module internal TaskSeqInternal =
10551060
| None -> ()
10561061
}
10571062

1063+
let chooseV chooser (source: TaskSeq<_>) =
1064+
checkNonNull (nameof source) source
1065+
1066+
taskSeq {
1067+
1068+
match chooser with
1069+
| TryPickV picker ->
1070+
for item in source do
1071+
match picker item with
1072+
| ValueSome value -> yield value
1073+
| ValueNone -> ()
1074+
1075+
| TryPickVAsync picker ->
1076+
for item in source do
1077+
match! picker item with
1078+
| ValueSome value -> yield value
1079+
| ValueNone -> ()
1080+
}
1081+
10581082
let filter predicate (source: TaskSeq<_>) =
10591083
checkNonNull (nameof source) source
10601084

0 commit comments

Comments
 (0)