Skip to content

Commit 5f0720f

Browse files
committed
Add implementation of TaskSeq.takeUntil and variants (see details)
This adds `TaskSeq.takeUntil`, `TaskSeq.takeUntilAsync`, `TaskSeq.takeUntilInclusive` and `TaskSeq.takeUntilInclusiveAsync`, plus tests.
1 parent 140d421 commit 5f0720f

File tree

5 files changed

+331
-8
lines changed

5 files changed

+331
-8
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
@@ -35,6 +35,7 @@
3535
<Compile Include="TaskSeq.OfXXX.Tests.fs" />
3636
<Compile Include="TaskSeq.Pick.Tests.fs" />
3737
<Compile Include="TaskSeq.Singleton.Tests.fs" />
38+
<Compile Include="TaskSeq.TakeUntil.Tests.fs" />
3839
<Compile Include="TaskSeq.TakeWhile.Tests.fs" />
3940
<Compile Include="TaskSeq.Tail.Tests.fs" />
4041
<Compile Include="TaskSeq.ToXXX.Tests.fs" />
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
module TaskSeq.Tests.TakeUntil
2+
3+
open System
4+
5+
open Xunit
6+
open FsUnit.Xunit
7+
8+
open FSharp.Control
9+
10+
//
11+
// TaskSeq.takeUntil
12+
// TaskSeq.takeUntilAsync
13+
// TaskSeq.takeUntilInclusive
14+
// TaskSeq.takeUntilInclusiveAsync
15+
//
16+
17+
[<AutoOpen>]
18+
module With =
19+
/// The only real difference in semantics between the base and the *Inclusive variant lies in whether the final item is returned.
20+
/// NOTE the semantics are very clear on only propagating a single failing item in the inclusive case.
21+
let getFunction inclusive isAsync =
22+
match inclusive, isAsync with
23+
| false, false -> TaskSeq.takeUntil
24+
| false, true -> fun pred -> TaskSeq.takeUntilAsync (pred >> Task.fromResult)
25+
| true, false -> TaskSeq.takeUntilInclusive
26+
| true, true -> fun pred -> TaskSeq.takeUntilInclusiveAsync (pred >> Task.fromResult)
27+
28+
/// adds '@' to each number and concatenates the chars before calling 'should equal'
29+
let verifyAsString expected =
30+
TaskSeq.map char
31+
>> TaskSeq.map ((+) '@')
32+
>> TaskSeq.toArrayAsync
33+
>> Task.map (String >> should equal expected)
34+
35+
/// This is the base condition as one would expect in actual code
36+
let inline cond x = x = 6
37+
38+
/// For each of the tests below, we add a guard that will trigger if the predicate is passed items known to be beyond the
39+
/// first failing item in the known sequence (which is 1..6)
40+
let inline condWithGuard x =
41+
let res = cond x
42+
43+
if x > 6 then
44+
failwith "Test sequence should not be enumerated beyond the first item failing the predicate"
45+
46+
res
47+
48+
module EmptySeq =
49+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
50+
let ``TaskSeq-takeUntil has no effect`` variant = task {
51+
do!
52+
Gen.getEmptyVariant variant
53+
|> TaskSeq.takeUntil ((=) 12)
54+
|> verifyEmpty
55+
56+
do!
57+
Gen.getEmptyVariant variant
58+
|> TaskSeq.takeUntilAsync ((=) 12 >> Task.fromResult)
59+
|> verifyEmpty
60+
}
61+
62+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
63+
let ``TaskSeq-takeUntilInclusive has no effect`` variant = task {
64+
do!
65+
Gen.getEmptyVariant variant
66+
|> TaskSeq.takeUntilInclusive ((=) 12)
67+
|> verifyEmpty
68+
69+
do!
70+
Gen.getEmptyVariant variant
71+
|> TaskSeq.takeUntilInclusiveAsync ((=) 12 >> Task.fromResult)
72+
|> verifyEmpty
73+
}
74+
75+
module Immutable =
76+
77+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
78+
let ``TaskSeq-takeUntil filters correctly`` variant = task {
79+
do!
80+
Gen.getSeqImmutable variant
81+
|> TaskSeq.takeUntil condWithGuard
82+
|> verifyAsString "ABCDE"
83+
84+
do!
85+
Gen.getSeqImmutable variant
86+
|> TaskSeq.takeUntilAsync (fun x -> task { return condWithGuard x })
87+
|> verifyAsString "ABCDE"
88+
}
89+
90+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
91+
let ``TaskSeq-takeUntil does not pick first item when true`` variant = task {
92+
do!
93+
Gen.getSeqImmutable variant
94+
|> TaskSeq.takeUntil ((<>) 0)
95+
|> verifyAsString ""
96+
97+
do!
98+
Gen.getSeqImmutable variant
99+
|> TaskSeq.takeUntilAsync ((<>) 0 >> Task.fromResult)
100+
|> verifyAsString ""
101+
}
102+
103+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
104+
let ``TaskSeq-takeUntilInclusive filters correctly`` variant = task {
105+
do!
106+
Gen.getSeqImmutable variant
107+
|> TaskSeq.takeUntilInclusive condWithGuard
108+
|> verifyAsString "ABCDEF"
109+
110+
do!
111+
Gen.getSeqImmutable variant
112+
|> TaskSeq.takeUntilInclusiveAsync (fun x -> task { return condWithGuard x })
113+
|> verifyAsString "ABCDEF"
114+
}
115+
116+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
117+
let ``TaskSeq-takeUntilInclusive always picks at least the first item`` variant = task {
118+
do!
119+
Gen.getSeqImmutable variant
120+
|> TaskSeq.takeUntilInclusive ((<>) 0)
121+
|> verifyAsString "A"
122+
123+
do!
124+
Gen.getSeqImmutable variant
125+
|> TaskSeq.takeUntilInclusiveAsync ((<>) 0 >> Task.fromResult)
126+
|> verifyAsString "A"
127+
}
128+
129+
module SideEffects =
130+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
131+
let ``TaskSeq-takeUntil filters correctly`` variant =
132+
Gen.getSeqWithSideEffect variant
133+
|> TaskSeq.takeUntil condWithGuard
134+
|> verifyAsString "ABCDE"
135+
136+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
137+
let ``TaskSeq-takeUntilAsync filters correctly`` variant =
138+
Gen.getSeqWithSideEffect variant
139+
|> TaskSeq.takeUntilAsync (fun x -> task { return condWithGuard x })
140+
|> verifyAsString "ABCDE"
141+
142+
[<Theory>]
143+
[<InlineData(false, false)>]
144+
[<InlineData(false, true)>]
145+
[<InlineData(true, false)>]
146+
[<InlineData(true, true)>]
147+
let ``TaskSeq-takeUntilXXX prove it does not read beyond the failing yield`` (inclusive, isAsync) = task {
148+
let mutable x = 42 // for this test, the potential mutation should not actually occur
149+
let functionToTest = getFunction inclusive isAsync ((<>) 42)
150+
151+
let items = taskSeq {
152+
yield x // Always passes the test; always returned
153+
yield x * 2 // the failing item (which will also be yielded in the result when using *Inclusive)
154+
x <- x + 1 // we are proving we never get here
155+
}
156+
157+
let expected = if inclusive then [| 42; 84 |] else [| 42 |]
158+
159+
let! first = items |> functionToTest |> TaskSeq.toArrayAsync
160+
let! repeat = items |> functionToTest |> TaskSeq.toArrayAsync
161+
162+
first |> should equal expected
163+
repeat |> should equal expected
164+
x |> should equal 42
165+
}
166+
167+
[<Theory>]
168+
[<InlineData(false, false)>]
169+
[<InlineData(false, true)>]
170+
[<InlineData(true, false)>]
171+
[<InlineData(true, true)>]
172+
let ``TaskSeq-takeUntilXXX prove side effects are executed`` (inclusive, isAsync) = task {
173+
let mutable x = 41
174+
let functionToTest = getFunction inclusive isAsync ((<=) 50)
175+
176+
let items = taskSeq {
177+
x <- x + 1
178+
yield x
179+
x <- x + 2
180+
yield x * 2
181+
x <- x + 200 // as previously proven, we should not trigger this
182+
}
183+
184+
let expectedFirst = if inclusive then [| 42; 44 * 2 |] else [| 42 |]
185+
let expectedRepeat = if inclusive then [| 45; 47 * 2 |] else [| 45 |]
186+
187+
let! first = items |> functionToTest |> TaskSeq.toArrayAsync
188+
x |> should equal 44
189+
let! repeat = items |> functionToTest |> TaskSeq.toArrayAsync
190+
x |> should equal 47
191+
192+
first |> should equal expectedFirst
193+
repeat |> should equal expectedRepeat
194+
}
195+
196+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
197+
let ``TaskSeq-takeUntil consumes the prefix of a longer sequence, with mutation`` variant = task {
198+
let ts = Gen.getSeqWithSideEffect variant
199+
200+
let! first =
201+
TaskSeq.takeUntil (fun x -> x >= 5) ts
202+
|> TaskSeq.toArrayAsync
203+
204+
let expected = [| 1..4 |]
205+
first |> should equal expected
206+
207+
// side effect, reiterating causes it to resume from where we left it (minus the failing item)
208+
let! repeat =
209+
TaskSeq.takeUntil (fun x -> x < 5) ts
210+
|> TaskSeq.toArrayAsync
211+
212+
repeat |> should not' (equal expected)
213+
}
214+
215+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
216+
let ``TaskSeq-takeUntilInclusiveAsync consumes the prefix for a longer sequence, with mutation`` variant = task {
217+
let ts = Gen.getSeqWithSideEffect variant
218+
219+
let! first =
220+
TaskSeq.takeUntilInclusiveAsync (fun x -> task { return x = 6 }) ts
221+
|> TaskSeq.toArrayAsync
222+
223+
let expected = [| 1..6 |] // the '6' is included, we are testing "Inclusive"
224+
first |> should equal expected
225+
226+
// side effect, reiterating causes it to resume from where we left it (minus the failing item)
227+
let! repeat =
228+
TaskSeq.takeUntilInclusiveAsync (fun x -> task { return x < 5 }) ts
229+
|> TaskSeq.toArrayAsync
230+
231+
repeat |> should not' (equal expected)
232+
}
233+
234+
module Other =
235+
[<Theory>]
236+
[<InlineData(false, false)>]
237+
[<InlineData(false, true)>]
238+
[<InlineData(true, false)>]
239+
[<InlineData(true, true)>]
240+
let ``TaskSeq-takeUntilXXX exclude all items after predicate fails`` (inclusive, isAsync) =
241+
let functionToTest = With.getFunction inclusive isAsync
242+
243+
[ 1; 2; 2; 3; 3; 2; 1 ]
244+
|> TaskSeq.ofSeq
245+
|> functionToTest (fun x -> x > 2)
246+
|> verifyAsString (if inclusive then "ABBC" else "ABB")
247+
248+
[<Theory>]
249+
[<InlineData(false, false)>]
250+
[<InlineData(false, true)>]
251+
[<InlineData(true, false)>]
252+
[<InlineData(true, true)>]
253+
let ``TaskSeq-takeUntilXXX stops consuming after predicate fails`` (inclusive, isAsync) =
254+
let functionToTest = With.getFunction inclusive isAsync
255+
256+
seq {
257+
yield! [ 1; 2; 2; 3; 3 ]
258+
yield failwith "Too far"
259+
}
260+
|> TaskSeq.ofSeq
261+
|> functionToTest (fun x -> x > 2)
262+
|> verifyAsString (if inclusive then "ABBC" else "ABB")

src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ module With =
3636
let inline cond x = x <> 6
3737

3838
/// For each of the tests below, we add a guard that will trigger if the predicate is passed items known to be beyond the
39-
/// first failing item in the known sequence (which is 1..10)
39+
/// first failing item in the known sequence (which is 1..6)
4040
let inline condWithGuard x =
4141
let res = cond x
4242

@@ -47,7 +47,7 @@ module With =
4747

4848
module EmptySeq =
4949
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
50-
let ``TaskSeq-takeWhile+A has no effect`` variant = task {
50+
let ``TaskSeq-takeWhile has no effect`` variant = task {
5151
do!
5252
Gen.getEmptyVariant variant
5353
|> TaskSeq.takeWhile ((=) 12)
@@ -60,7 +60,7 @@ module EmptySeq =
6060
}
6161

6262
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
63-
let ``TaskSeq-takeWhileInclusive+A has no effect`` variant = task {
63+
let ``TaskSeq-takeWhileInclusive has no effect`` variant = task {
6464
do!
6565
Gen.getEmptyVariant variant
6666
|> TaskSeq.takeWhileInclusive ((=) 12)
@@ -75,7 +75,7 @@ module EmptySeq =
7575
module Immutable =
7676

7777
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
78-
let ``TaskSeq-takeWhile+A filters correctly`` variant = task {
78+
let ``TaskSeq-takeWhile filters correctly`` variant = task {
7979
do!
8080
Gen.getSeqImmutable variant
8181
|> TaskSeq.takeWhile condWithGuard
@@ -88,7 +88,7 @@ module Immutable =
8888
}
8989

9090
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
91-
let ``TaskSeq-takeWhile+A does not pick first item when false`` variant = task {
91+
let ``TaskSeq-takeWhile does not pick first item when false`` variant = task {
9292
do!
9393
Gen.getSeqImmutable variant
9494
|> TaskSeq.takeWhile ((=) 0)
@@ -101,7 +101,7 @@ module Immutable =
101101
}
102102

103103
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
104-
let ``TaskSeq-takeWhileInclusive+A filters correctly`` variant = task {
104+
let ``TaskSeq-takeWhileInclusive filters correctly`` variant = task {
105105
do!
106106
Gen.getSeqImmutable variant
107107
|> TaskSeq.takeWhileInclusive condWithGuard
@@ -114,7 +114,7 @@ module Immutable =
114114
}
115115

116116
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
117-
let ``TaskSeq-takeWhileInclusive+A always pick at least the first item`` variant = task {
117+
let ``TaskSeq-takeWhileInclusive always picks at least the first item`` variant = task {
118118
do!
119119
Gen.getSeqImmutable variant
120120
|> TaskSeq.takeWhileInclusive ((=) 0)

src/FSharp.Control.TaskSeq/TaskSeq.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@ module TaskSeq =
286286
let takeWhileAsync predicate source = Internal.takeWhile Exclusive (PredicateAsync predicate) source
287287
let takeWhileInclusive predicate source = Internal.takeWhile Inclusive (Predicate predicate) source
288288
let takeWhileInclusiveAsync predicate source = Internal.takeWhile Inclusive (PredicateAsync predicate) source
289+
let takeUntil predicate source = Internal.takeWhile Exclusive (Predicate(predicate >> not)) source
290+
let takeUntilAsync predicate source = Internal.takeWhile Exclusive (PredicateAsync(predicate >> Task.map not)) source
291+
let takeUntilInclusive predicate source = Internal.takeWhile Inclusive (Predicate(predicate >> not)) source
292+
let takeUntilInclusiveAsync predicate source = Internal.takeWhile Inclusive (PredicateAsync(predicate >> Task.map not)) source
289293
let tryPick chooser source = Internal.tryPick (TryPick chooser) source
290294
let tryPickAsync chooser source = Internal.tryPick (TryPickAsync chooser) source
291295
let tryFind predicate source = Internal.tryFind (Predicate predicate) source

0 commit comments

Comments
 (0)