Skip to content

Commit 06bc268

Browse files
authored
Merge pull request #219 from fsprojects/implement-skipwhile-and-variants
Implement `TaskSeq.skipWhile`, `skipWhileAsync`, `skipWhileInclusive` and `skipWhileInclusiveAsync`
2 parents de928fb + a213a1f commit 06bc268

File tree

9 files changed

+531
-42
lines changed

9 files changed

+531
-42
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,8 @@ This is what has been implemented so far, is planned or skipped:
319319
| ✅ [#90][] | `singleton` | `singleton` | | |
320320
| ✅ [#209][]| `skip` | `skip` | | |
321321
| ✅ [#209][]| | `drop` | | |
322-
| | `skipWhile` | `skipWhile` | `skipWhileAsync` | |
323-
| | | `skipWhileInclusive` | `skipWhileInclusiveAsync` | |
322+
| ✅ [#219][]| `skipWhile` | `skipWhile` | `skipWhileAsync` | |
323+
| ✅ [#219][]| | `skipWhileInclusive` | `skipWhileInclusiveAsync` | |
324324
| ❓ | `sort` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") |
325325
| ❓ | `sortBy` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") |
326326
| ❓ | `sortByAscending` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") |
@@ -603,6 +603,7 @@ module TaskSeq =
603603
[#133]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/133
604604
[#209]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/209
605605
[#217]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/217
606+
[#219]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/219
606607

607608
[issues]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues
608609
[nuget]: https://www.nuget.org/packages/FSharp.Control.TaskSeq/

assets/nuget-package-readme.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,8 @@ This is what has been implemented so far, is planned or skipped:
199199
| ✅ [#90][] | `singleton` | `singleton` | | |
200200
| ✅ [#209][]| `skip` | `skip` | | |
201201
| ✅ [#209][]| | `drop` | | |
202-
| | `skipWhile` | `skipWhile` | `skipWhileAsync` | |
203-
| | | `skipWhileInclusive` | `skipWhileInclusiveAsync` | |
202+
| ✅ [#219][]| `skipWhile` | `skipWhile` | `skipWhileAsync` | |
203+
| ✅ [#219][]| | `skipWhileInclusive` | `skipWhileInclusiveAsync` | |
204204
| ❓ | `sort` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") |
205205
| ❓ | `sortBy` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") |
206206
| ❓ | `sortByAscending` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") |
@@ -308,3 +308,4 @@ _The motivation for `readOnly` in `Seq` is that a cast from a mutable array or l
308308
[#126]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/126
309309
[#209]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/209
310310
[#217]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/217
311+
[#219]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/219

release-notes.txt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ Release notes:
33
0.4.x (unreleased)
44
- overhaul all doc comments, add exceptions, improve IDE quick-info experience, #136
55
- new surface area functions, fixes #208:
6-
* TaskSeq.take, TaskSeq.skip, #209
7-
* TaskSeq.truncate, TaskSeq.drop, #209
8-
* TaskSeq.where, TaskSeq.whereAsync, #217
6+
* TaskSeq.take, skip, #209
7+
* TaskSeq.truncate, drop, #209
8+
* TaskSeq.where, whereAsync, #217
9+
* TaskSeq.skipWhile, skipWhileInclusive, skipWhileAsync, skipWhileInclusiveAsync, #219
910

1011
- Performance: less thread hops with 'StartImmediateAsTask' instead of 'StartAsTask', fixes #135
1112
- BINARY INCOMPATIBILITY: 'TaskSeq' module is now static members on 'TaskSeq<_>', fixes #184

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<Compile Include="TaskSeq.Pick.Tests.fs" />
3737
<Compile Include="TaskSeq.Singleton.Tests.fs" />
3838
<Compile Include="TaskSeq.Skip.Tests.fs" />
39+
<Compile Include="TaskSeq.SkipWhile.Tests.fs" />
3940
<Compile Include="TaskSeq.Tail.Tests.fs" />
4041
<Compile Include="TaskSeq.Take.Tests.fs" />
4142
<Compile Include="TaskSeq.TakeWhile.Tests.fs" />
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
module TaskSeq.Tests.skipWhile
2+
3+
open System
4+
5+
open Xunit
6+
open FsUnit.Xunit
7+
8+
open FSharp.Control
9+
10+
//
11+
// TaskSeq.skipWhile
12+
// TaskSeq.skipWhileAsync
13+
// TaskSeq.skipWhileInclusive
14+
// TaskSeq.skipWhileInclusiveAsync
15+
//
16+
17+
exception SideEffectPastEnd of string
18+
19+
20+
module EmptySeq =
21+
22+
// TaskSeq-skipWhile+A stands for:
23+
// skipWhile + skipWhileAsync etc.
24+
25+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
26+
let ``TaskSeq-skipWhile+A has no effect`` variant = task {
27+
do!
28+
Gen.getEmptyVariant variant
29+
|> TaskSeq.skipWhile ((=) 12)
30+
|> verifyEmpty
31+
32+
do!
33+
Gen.getEmptyVariant variant
34+
|> TaskSeq.skipWhileAsync ((=) 12 >> Task.fromResult)
35+
|> verifyEmpty
36+
}
37+
38+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
39+
let ``TaskSeq-skipWhileInclusive+A has no effect`` variant = task {
40+
do!
41+
Gen.getEmptyVariant variant
42+
|> TaskSeq.skipWhileInclusive ((=) 12)
43+
|> verifyEmpty
44+
45+
do!
46+
Gen.getEmptyVariant variant
47+
|> TaskSeq.skipWhileInclusiveAsync ((=) 12 >> Task.fromResult)
48+
|> verifyEmpty
49+
}
50+
51+
module Immutable =
52+
53+
// TaskSeq-skipWhile+A stands for:
54+
// skipWhile + skipWhileAsync etc.
55+
56+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
57+
let ``TaskSeq-skipWhile+A filters correctly`` variant = task {
58+
// truth table for f(x) = x < 5
59+
// 1 2 3 4 5 6 7 8 9 10
60+
// T T T T F F F F F F (stops at first F)
61+
// x x x x _ _ _ _ _ _ (skips exclusive)
62+
// A B C D E F G H I J
63+
64+
do!
65+
Gen.getSeqImmutable variant
66+
|> TaskSeq.skipWhile (fun x -> x < 5)
67+
|> verifyDigitsAsString "EFGHIJ"
68+
69+
do!
70+
Gen.getSeqImmutable variant
71+
|> TaskSeq.skipWhileAsync (fun x -> task { return x < 5 })
72+
|> verifyDigitsAsString "EFGHIJ"
73+
}
74+
75+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
76+
let ``TaskSeq-skipWhile+A does not skip first item when false`` variant = task {
77+
do!
78+
Gen.getSeqImmutable variant
79+
|> TaskSeq.skipWhile ((=) 0)
80+
|> verifyDigitsAsString "ABCDEFGHIJ" // all 10 remain!
81+
82+
do!
83+
Gen.getSeqImmutable variant
84+
|> TaskSeq.skipWhileAsync ((=) 0 >> Task.fromResult)
85+
|> verifyDigitsAsString "ABCDEFGHIJ" // all 10 remain!
86+
}
87+
88+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
89+
let ``TaskSeq-skipWhileInclusive+A filters correctly`` variant = task {
90+
// truth table for f(x) = x < 5
91+
// 1 2 3 4 5 6 7 8 9 10
92+
// T T T T F F F F F F (stops at first F)
93+
// x x x x x _ _ _ _ _ (skips inclusively)
94+
// A B C D E F G H I J
95+
96+
do!
97+
Gen.getSeqImmutable variant
98+
|> TaskSeq.skipWhileInclusive (fun x -> x < 5)
99+
|> verifyDigitsAsString "FGHIJ" // last 4
100+
101+
do!
102+
Gen.getSeqImmutable variant
103+
|> TaskSeq.skipWhileInclusiveAsync (fun x -> task { return x < 5 })
104+
|> verifyDigitsAsString "FGHIJ"
105+
}
106+
107+
108+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
109+
let ``TaskSeq-skipWhileInclusive+A returns the empty sequence if always true`` variant = task {
110+
do!
111+
Gen.getSeqImmutable variant
112+
|> TaskSeq.skipWhileInclusive (fun x -> x > -1) // always true
113+
|> verifyEmpty
114+
115+
do!
116+
Gen.getSeqImmutable variant
117+
|> TaskSeq.skipWhileInclusiveAsync (fun x -> Task.fromResult (x > -1))
118+
|> verifyEmpty
119+
}
120+
121+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
122+
let ``TaskSeq-skipWhileInclusive+A always skips at least the first item`` variant = task {
123+
do!
124+
Gen.getSeqImmutable variant
125+
|> TaskSeq.skipWhileInclusive ((=) 0)
126+
|> verifyDigitsAsString "BCDEFGHIJ"
127+
128+
do!
129+
Gen.getSeqImmutable variant
130+
|> TaskSeq.skipWhileInclusiveAsync ((=) 0 >> Task.fromResult)
131+
|> verifyDigitsAsString "BCDEFGHIJ"
132+
}
133+
134+
module SideEffects =
135+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
136+
let ``TaskSeq-skipWhile+A filters correctly`` variant = task {
137+
// truth table for f(x) = x < 6
138+
// 1 2 3 4 5 6 7 8 9 10
139+
// T T T T T F F F F F (stops at first F)
140+
// x x x x x _ _ _ _ _ (skips exclusively)
141+
// A B C D E F G H I J
142+
143+
do!
144+
Gen.getSeqWithSideEffect variant
145+
|> TaskSeq.skipWhile (fun x -> x < 6)
146+
|> verifyDigitsAsString "FGHIJ"
147+
148+
do!
149+
Gen.getSeqWithSideEffect variant
150+
|> TaskSeq.skipWhileAsync (fun x -> task { return x < 6 })
151+
|> verifyDigitsAsString "FGHIJ"
152+
}
153+
154+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
155+
let ``TaskSeq-skipWhileInclusive+A filters correctly`` variant = task {
156+
// truth table for f(x) = x < 6
157+
// 1 2 3 4 5 6 7 8 9 10
158+
// T T T T T F F F F F (stops at first F)
159+
// x x x x x x _ _ _ _ (skips inclusively)
160+
// A B C D E F G H I J
161+
162+
do!
163+
Gen.getSeqWithSideEffect variant
164+
|> TaskSeq.skipWhileInclusive (fun x -> x < 6)
165+
|> verifyDigitsAsString "GHIJ"
166+
167+
do!
168+
Gen.getSeqWithSideEffect variant
169+
|> TaskSeq.skipWhileInclusiveAsync (fun x -> task { return x < 6 })
170+
|> verifyDigitsAsString "GHIJ"
171+
}
172+
173+
[<Fact>]
174+
let ``TaskSeq-skipWhile and variants prove it reads the entire input stream`` () =
175+
176+
let mutable x = 42
177+
178+
let items = taskSeq {
179+
yield x
180+
yield x * 2
181+
x <- x + 1 // we are proving we ALWAYS get here
182+
}
183+
184+
// this needs to be lifted from the task or it raises the infamous
185+
// warning FS3511 on CI: This state machine is not statically compilable
186+
let testSkipper skipper expected = task {
187+
let! first = items |> skipper |> TaskSeq.toArrayAsync
188+
return first |> should equal expected
189+
}
190+
191+
task {
192+
do! testSkipper (TaskSeq.skipWhile ((=) 42)) [| 84 |]
193+
x |> should equal 43
194+
195+
do! testSkipper (TaskSeq.skipWhileInclusive ((=) 43)) [||]
196+
x |> should equal 44
197+
198+
do! testSkipper (TaskSeq.skipWhileAsync (fun x -> Task.fromResult (x = 44))) [| 88 |]
199+
x |> should equal 45
200+
201+
do! testSkipper (TaskSeq.skipWhileInclusiveAsync (fun x -> Task.fromResult (x = 45))) [||]
202+
x |> should equal 46
203+
}
204+
205+
[<Fact>]
206+
let ``TaskSeq-skipWhile and variants prove side effects are properly executed`` () =
207+
let mutable x = 41
208+
209+
let items = taskSeq {
210+
x <- x + 1
211+
yield x
212+
x <- x + 2
213+
yield x * 2
214+
x <- x + 200 // as previously proven, we should ALWAYS trigger this
215+
}
216+
217+
// this needs to be lifted from the task or it raises the infamous
218+
// warning FS3511 on CI: This state machine is not statically compilable
219+
let testSkipper skipper expected = task {
220+
let! first = items |> skipper |> TaskSeq.toArrayAsync
221+
return first |> should equal expected
222+
}
223+
224+
task {
225+
do! testSkipper (TaskSeq.skipWhile ((=) 42)) [| 88 |]
226+
x |> should equal 244
227+
228+
do! testSkipper (TaskSeq.skipWhileInclusive ((=) 245)) [||]
229+
x |> should equal 447
230+
231+
do! testSkipper (TaskSeq.skipWhileAsync (fun x -> Task.fromResult (x = 448))) [| 900 |]
232+
x |> should equal 650
233+
234+
do! testSkipper (TaskSeq.skipWhileInclusiveAsync (fun x -> Task.fromResult (x = 651))) [||]
235+
x |> should equal 853
236+
}
237+
238+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
239+
let ``TaskSeq-skipWhile consumes the prefix of a longer sequence, with mutation`` variant = task {
240+
let ts = Gen.getSeqWithSideEffect variant
241+
242+
let! first =
243+
TaskSeq.skipWhile (fun x -> x < 5) ts
244+
|> TaskSeq.toArrayAsync
245+
246+
let expected = [| 5..10 |]
247+
first |> should equal expected
248+
249+
// side effect, reiterating causes it to resume from where we left it (minus the failing item)
250+
// which means the original sequence has now changed due to the side effect
251+
let! repeat =
252+
TaskSeq.skipWhile (fun x -> x < 5) ts
253+
|> TaskSeq.toArrayAsync
254+
255+
repeat |> should not' (equal expected)
256+
}
257+
258+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
259+
let ``TaskSeq-skipWhileInclusiveAsync consumes the prefix for a longer sequence, with mutation`` variant = task {
260+
let ts = Gen.getSeqWithSideEffect variant
261+
262+
let! first =
263+
TaskSeq.skipWhileInclusiveAsync (fun x -> task { return x < 5 }) ts
264+
|> TaskSeq.toArrayAsync
265+
266+
let expected = [| 6..10 |]
267+
first |> should equal expected
268+
269+
// side effect, reiterating causes it to resume from where we left it (minus the failing item)
270+
// which means the original sequence has now changed due to the side effect
271+
let! repeat =
272+
TaskSeq.skipWhileInclusiveAsync (fun x -> task { return x < 5 }) ts
273+
|> TaskSeq.toArrayAsync
274+
275+
repeat |> should not' (equal expected)
276+
}
277+
278+
module Other =
279+
[<Fact>]
280+
let ``TaskSeq-skipWhileXXX should include all items after predicate fails`` () = task {
281+
do!
282+
[ 1; 2; 2; 3; 3; 2; 1 ]
283+
|> TaskSeq.ofSeq
284+
|> TaskSeq.skipWhile (fun x -> x <= 2)
285+
|> verifyDigitsAsString "CCBA"
286+
287+
do!
288+
[ 1; 2; 2; 3; 3; 2; 1 ]
289+
|> TaskSeq.ofSeq
290+
|> TaskSeq.skipWhileInclusive (fun x -> x <= 2)
291+
|> verifyDigitsAsString "CBA"
292+
293+
do!
294+
[ 1; 2; 2; 3; 3; 2; 1 ]
295+
|> TaskSeq.ofSeq
296+
|> TaskSeq.skipWhileAsync (fun x -> Task.fromResult (x <= 2))
297+
|> verifyDigitsAsString "CCBA"
298+
299+
do!
300+
[ 1; 2; 2; 3; 3; 2; 1 ]
301+
|> TaskSeq.ofSeq
302+
|> TaskSeq.skipWhileInclusiveAsync (fun x -> Task.fromResult (x <= 2))
303+
|> verifyDigitsAsString "CBA"
304+
}
305+
306+
[<Fact>]
307+
let ``TaskSeq-skipWhileXXX stops consuming after predicate fails`` () =
308+
let testSkipper skipper =
309+
fun () ->
310+
seq {
311+
yield! [ 1; 2; 2; 3; 3 ]
312+
yield SideEffectPastEnd "Too far" |> raise
313+
}
314+
|> TaskSeq.ofSeq
315+
|> skipper
316+
|> consumeTaskSeq
317+
|> should throwAsyncExact typeof<SideEffectPastEnd>
318+
319+
testSkipper (TaskSeq.skipWhile (fun x -> x <= 2))
320+
testSkipper (TaskSeq.skipWhileInclusive (fun x -> x <= 2))
321+
testSkipper (TaskSeq.skipWhileAsync (fun x -> Task.fromResult (x <= 2)))
322+
testSkipper (TaskSeq.skipWhileInclusiveAsync (fun x -> Task.fromResult (x <= 2)))

0 commit comments

Comments
 (0)