Skip to content

Commit 953f218

Browse files
authored
Merge pull request #240 from fsprojects/implement-forall
Implement `TaskSeq.forall` and `forallAsync`
2 parents 38dce91 + 219f89d commit 953f218

File tree

6 files changed

+272
-8
lines changed

6 files changed

+272
-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
@@ -24,6 +24,7 @@
2424
<Compile Include="TaskSeq.FindIndex.Tests.fs" />
2525
<Compile Include="TaskSeq.Find.Tests.fs" />
2626
<Compile Include="TaskSeq.Fold.Tests.fs" />
27+
<Compile Include="TaskSeq.Forall.Tests.fs" />
2728
<Compile Include="TaskSeq.Head.Tests.fs" />
2829
<Compile Include="TaskSeq.Indexed.Tests.fs" />
2930
<Compile Include="TaskSeq.Init.Tests.fs" />

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ module Immutable =
8282

8383
module SideEffects =
8484
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
85-
let ``TaskSeq-exists KeyNotFoundException only sometimes for mutated state`` variant = task {
85+
let ``TaskSeq-exists success only sometimes for mutated state`` variant = task {
8686
let ts = Gen.getSeqWithSideEffect variant
8787
let finder = (=) 11
8888

@@ -100,7 +100,7 @@ module SideEffects =
100100
}
101101

102102
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
103-
let ``TaskSeq-existsAsync KeyNotFoundException only sometimes for mutated state`` variant = task {
103+
let ``TaskSeq-existsAsync success only sometimes for mutated state`` variant = task {
104104
let ts = Gen.getSeqWithSideEffect variant
105105
let finder x = task { return x = 11 }
106106

@@ -201,7 +201,7 @@ module SideEffects =
201201
found |> should be True
202202
i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated
203203

204-
// find some next item. We do get a new iterator, but mutable state is now starting at '1'
204+
// find some next item. We do get a new iterator, but mutable state is now still starting at '0'
205205
let! found = ts |> TaskSeq.exists ((=) 4)
206206
found |> should be True
207207
i |> should equal 4 // only partial evaluation!
@@ -221,7 +221,7 @@ module SideEffects =
221221
found |> should be True
222222
i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated
223223

224-
// find some next item. We do get a new iterator, but mutable state is now starting at '1'
224+
// find some next item. We do get a new iterator, but mutable state is now still starting at '0'
225225
let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 4 })
226226
found |> should be True
227227
i |> should equal 4 // only partial evaluation!
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
module TaskSeq.Tests.Forall
2+
3+
open Xunit
4+
open FsUnit.Xunit
5+
6+
open FSharp.Control
7+
8+
//
9+
// TaskSeq.forall
10+
// TaskSeq.forallAsyncc
11+
//
12+
13+
module EmptySeq =
14+
[<Fact>]
15+
let ``Null source is invalid`` () =
16+
assertNullArg
17+
<| fun () -> TaskSeq.forall (fun _ -> false) null
18+
19+
assertNullArg
20+
<| fun () -> TaskSeq.forallAsync (fun _ -> Task.fromResult false) null
21+
22+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
23+
let ``TaskSeq-forall always returns true`` variant =
24+
Gen.getEmptyVariant variant
25+
|> TaskSeq.forall ((=) 12)
26+
|> Task.map (should be True)
27+
28+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
29+
let ``TaskSeq-forallAsync always returns true`` variant =
30+
Gen.getEmptyVariant variant
31+
|> TaskSeq.forallAsync (fun x -> task { return x = 12 })
32+
|> Task.map (should be True)
33+
34+
module Immutable =
35+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
36+
let ``TaskSeq-forall sad path returns false`` variant = task {
37+
do!
38+
Gen.getSeqImmutable variant
39+
|> TaskSeq.forall ((=) 0)
40+
|> Task.map (should be False)
41+
42+
do!
43+
Gen.getSeqImmutable variant
44+
|> TaskSeq.forall ((>) 9) // lt
45+
|> Task.map (should be False)
46+
}
47+
48+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
49+
let ``TaskSeq-forallAsync sad path returns false`` variant = task {
50+
do!
51+
Gen.getSeqImmutable variant
52+
|> TaskSeq.forallAsync (fun x -> task { return x = 0 })
53+
|> Task.map (should be False)
54+
55+
do!
56+
Gen.getSeqImmutable variant
57+
|> TaskSeq.forallAsync (fun x -> task { return x < 9 })
58+
|> Task.map (should be False)
59+
}
60+
61+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
62+
let ``TaskSeq-forall happy path whole seq true`` variant =
63+
Gen.getSeqImmutable variant
64+
|> TaskSeq.forall (fun x -> x < 6 || x > 5)
65+
|> Task.map (should be True)
66+
67+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
68+
let ``TaskSeq-forallAsync happy path whole seq true`` variant =
69+
Gen.getSeqImmutable variant
70+
|> TaskSeq.forallAsync (fun x -> task { return x <= 10 && x >= 0 })
71+
|> Task.map (should be True)
72+
73+
module SideEffects =
74+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
75+
let ``TaskSeq-forall mutated state can change result`` variant = task {
76+
let ts = Gen.getSeqWithSideEffect variant
77+
let predicate x = x > 10
78+
79+
// first: false
80+
let! found = TaskSeq.forall predicate ts
81+
found |> should be False // fails on first item, not many side effects yet
82+
83+
// ensure side effects executes
84+
do! consumeTaskSeq ts
85+
86+
// find again: found now, because of side effects
87+
let! found = TaskSeq.forall predicate ts
88+
found |> should be True
89+
90+
// find once more, still true, as numbers increase
91+
do! consumeTaskSeq ts // ensure side effects executes
92+
let! found = TaskSeq.forall predicate ts
93+
found |> should be True
94+
}
95+
96+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
97+
let ``TaskSeq-forallAsync mutated state can change result`` variant = task {
98+
let ts = Gen.getSeqWithSideEffect variant
99+
let predicate x = Task.fromResult (x > 10)
100+
101+
// first: false
102+
let! found = TaskSeq.forallAsync predicate ts
103+
found |> should be False // fails on first item, not many side effects yet
104+
105+
// ensure side effects executes
106+
do! consumeTaskSeq ts
107+
108+
// find again: found now, because of side effects
109+
let! found = TaskSeq.forallAsync predicate ts
110+
found |> should be True
111+
112+
// find once more, still true, as numbers increase
113+
do! consumeTaskSeq ts // ensure side effects executes
114+
let! found = TaskSeq.forallAsync predicate ts
115+
found |> should be True
116+
}
117+
118+
[<Fact>]
119+
let ``TaskSeq-forall _specialcase_ prove we don't read past the first failing item`` () = task {
120+
let mutable i = 0
121+
122+
let ts = taskSeq {
123+
for _ in 0..9 do
124+
i <- i + 1
125+
yield i
126+
}
127+
128+
let! found = ts |> TaskSeq.forall ((>) 3)
129+
found |> should be False
130+
i |> should equal 3 // only partial evaluation!
131+
132+
// find next item. We do get a new iterator, but mutable state is now starting at '3', so first item now returned is '4'.
133+
let! found = ts |> TaskSeq.forall ((<=) 4)
134+
found |> should be True
135+
i |> should equal 13 // we evaluated to the end
136+
}
137+
138+
[<Fact>]
139+
let ``TaskSeq-forallAsync _specialcase_ prove we don't read past the first failing item`` () = task {
140+
let mutable i = 0
141+
142+
let ts = taskSeq {
143+
for _ in 0..9 do
144+
i <- i + 1
145+
yield i
146+
}
147+
148+
let! found = ts |> TaskSeq.forallAsync (fun x -> Task.fromResult (x < 3))
149+
found |> should be False
150+
i |> should equal 3 // only partial evaluation!
151+
152+
// find next item. We do get a new iterator, but mutable state is now starting at '3', so first item now returned is '4'.
153+
let! found =
154+
ts
155+
|> TaskSeq.forallAsync (fun x -> Task.fromResult (x >= 4))
156+
157+
found |> should be True
158+
i |> should equal 13 // we evaluated to the end
159+
}
160+
161+
162+
[<Fact>]
163+
let ``TaskSeq-forall _specialcase_ prove statement after first false result is not evaluated`` () = task {
164+
let mutable i = 0
165+
166+
let ts = taskSeq {
167+
for _ in 0..9 do
168+
yield i
169+
i <- i + 1
170+
}
171+
172+
let! found = ts |> TaskSeq.forall ((>) 0)
173+
found |> should be False
174+
i |> should equal 0 // notice that it should be one higher if the statement after 'yield' was evaluated
175+
176+
// find some next item. We do get a new iterator, but mutable state is still starting at '0'
177+
let! found = ts |> TaskSeq.forall ((>) 4)
178+
found |> should be False
179+
i |> should equal 4 // only partial evaluation!
180+
}
181+
182+
[<Fact>]
183+
let ``TaskSeq-forallAsync _specialcase_ prove statement after first false result is not evaluated`` () = task {
184+
let mutable i = 0
185+
186+
let ts = taskSeq {
187+
for _ in 0..9 do
188+
yield i
189+
i <- i + 1
190+
}
191+
192+
let! found = ts |> TaskSeq.forallAsync (fun x -> Task.fromResult (x < 0))
193+
found |> should be False
194+
i |> should equal 0 // notice that it should be one higher if the statement after 'yield' was evaluated
195+
196+
// find some next item. We do get a new iterator, but mutable state is still starting at '0'
197+
let! found = ts |> TaskSeq.forallAsync (fun x -> Task.fromResult (x < 4))
198+
found |> should be False
199+
i |> should equal 4 // only partial evaluation!
200+
}

src/FSharp.Control.TaskSeq/TaskSeq.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,9 @@ type TaskSeq private () =
358358
static member except itemsToExclude source = Internal.except itemsToExclude source
359359
static member exceptOfSeq itemsToExclude source = Internal.exceptOfSeq itemsToExclude source
360360

361+
static member forall predicate source = Internal.forall (Predicate predicate) source
362+
static member forallAsync predicate source = Internal.forall (PredicateAsync predicate) source
363+
361364
static member exists predicate source =
362365
Internal.tryFind (Predicate predicate) source
363366
|> Task.map Option.isSome

src/FSharp.Control.TaskSeq/TaskSeq.fsi

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,30 @@ type TaskSeq =
875875
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
876876
static member whereAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> TaskSeq<'T>
877877

878+
/// <summary>
879+
/// Tests if all elements of the sequence satisfy the given predicate. Stops evaluating
880+
/// as soon as <paramref name="predicate" /> returns <see cref="false" />.
881+
/// If <paramref name="predicate" /> is asynchronous, consider using <see cref="TaskSeq.forallAsync" />.
882+
/// </summary>
883+
///
884+
/// <param name="predicate">A function to test an element of the input sequence.</param>
885+
/// <param name="source">The input task sequence.</param>
886+
/// <returns>A task that, after awaiting, holds true if every element of the sequence satisfies the predicate; false otherwise.</returns>
887+
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
888+
static member forall: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task<bool>
889+
890+
/// <summary>
891+
/// Tests if all elements of the sequence satisfy the given asynchronous predicate. Stops evaluating
892+
/// as soon as <paramref name="predicate" /> returns <see cref="false" />.
893+
/// If <paramref name="predicate" /> is synchronous, consider using <see cref="TaskSeq.forall" />.
894+
/// </summary>
895+
///
896+
/// <param name="predicate">A function to test an element of the input sequence.</param>
897+
/// <param name="source">The input task sequence.</param>
898+
/// <returns>A task that, after awaiting, holds true if every element of the sequence satisfies the predicate; false otherwise.</returns>
899+
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
900+
static member forallAsync: predicate: ('T -> #Task<bool>) -> source: TaskSeq<'T> -> Task<bool>
901+
878902
/// <summary>
879903
/// Returns a task sequence that, when iterated, skips <paramref name="count" /> elements of the underlying
880904
/// sequence, and then yields the remainder. Raises an exception if there are not <paramref name="count" />

src/FSharp.Control.TaskSeq/TaskSeqInternal.fs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -690,18 +690,54 @@ module internal TaskSeqInternal =
690690

691691
taskSeq {
692692
match predicate with
693-
| Predicate predicate ->
693+
| Predicate syncPredicate ->
694694
for item in source do
695-
if predicate item then
695+
if syncPredicate item then
696696
yield item
697697

698-
| PredicateAsync predicate ->
698+
| PredicateAsync asyncPredicate ->
699699
for item in source do
700-
match! predicate item with
700+
match! asyncPredicate item with
701701
| true -> yield item
702702
| false -> ()
703703
}
704704

705+
let forall predicate (source: TaskSeq<_>) =
706+
checkNonNull (nameof source) source
707+
708+
match predicate with
709+
| Predicate syncPredicate -> task {
710+
use e = source.GetAsyncEnumerator CancellationToken.None
711+
let mutable state = true
712+
let! cont = e.MoveNextAsync()
713+
let mutable hasMore = cont
714+
715+
while state && hasMore do
716+
state <- syncPredicate e.Current
717+
718+
if state then
719+
let! cont = e.MoveNextAsync()
720+
hasMore <- cont
721+
722+
return state
723+
}
724+
725+
| PredicateAsync asyncPredicate -> task {
726+
use e = source.GetAsyncEnumerator CancellationToken.None
727+
let mutable state = true
728+
let! cont = e.MoveNextAsync()
729+
let mutable hasMore = cont
730+
731+
while state && hasMore do
732+
let! pred = asyncPredicate e.Current
733+
state <- pred
734+
735+
if state then
736+
let! cont = e.MoveNextAsync()
737+
hasMore <- cont
738+
739+
return state
740+
}
705741

706742
let skipOrTake skipOrTake count (source: TaskSeq<_>) =
707743
checkNonNull (nameof source) source

0 commit comments

Comments
 (0)