Skip to content

Commit e4b2329

Browse files
committed
Implement TaskSeq.exists/existsAsync and contains, plus tests and xml docs
1 parent e8e62ec commit e4b2329

File tree

6 files changed

+393
-1
lines changed

6 files changed

+393
-1
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
<Compile Include="TaskSeq.Choose.Tests.fs" />
1717
<Compile Include="TaskSeq.Collect.Tests.fs" />
1818
<Compile Include="TaskSeq.Concat.Tests.fs" />
19+
<Compile Include="TaskSeq.Contains.Tests.fs" />
1920
<Compile Include="TaskSeq.Empty.Tests.fs" />
2021
<Compile Include="TaskSeq.ExactlyOne.Tests.fs" />
22+
<Compile Include="TaskSeq.Exists.Tests.fs" />
2123
<Compile Include="TaskSeq.Filter.Tests.fs" />
2224
<Compile Include="TaskSeq.FindIndex.Tests.fs" />
2325
<Compile Include="TaskSeq.Find.Tests.fs" />
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
module FSharpy.Tests.Contains
2+
3+
open Xunit
4+
open FsUnit.Xunit
5+
open FsToolkit.ErrorHandling
6+
7+
open FSharpy
8+
9+
//
10+
// TaskSeq.contains
11+
//
12+
13+
module EmptySeq =
14+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
15+
let ``TaskSeq-contains returns false`` variant =
16+
Gen.getEmptyVariant variant
17+
|> TaskSeq.contains 12
18+
|> Task.map (should be False)
19+
20+
module Immutable =
21+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
22+
let ``TaskSeq-contains sad path returns false`` variant =
23+
Gen.getSeqImmutable variant
24+
|> TaskSeq.contains 0
25+
|> Task.map (should be False)
26+
27+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
28+
let ``TaskSeq-contains happy path middle of seq`` variant =
29+
Gen.getSeqImmutable variant
30+
|> TaskSeq.contains 5
31+
|> Task.map (should be True)
32+
33+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
34+
let ``TaskSeq-contains happy path first item of seq`` variant =
35+
Gen.getSeqImmutable variant
36+
|> TaskSeq.contains 1
37+
|> Task.map (should be True)
38+
39+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
40+
let ``TaskSeq-contains happy path last item of seq`` variant =
41+
Gen.getSeqImmutable variant
42+
|> TaskSeq.contains 10
43+
|> Task.map (should be True)
44+
45+
module SideEffects =
46+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
47+
let ``TaskSeq-contains KeyNotFoundException only sometimes for mutated state`` variant = task {
48+
let ts = Gen.getSeqWithSideEffect variant
49+
50+
// first: false
51+
let! found = TaskSeq.contains 11 ts
52+
found |> should be False
53+
54+
// find again: found now, because of side effects
55+
let! found = TaskSeq.contains 11 ts
56+
found |> should be True
57+
58+
// find once more: false
59+
let! found = TaskSeq.contains 11 ts
60+
found |> should be False
61+
}
62+
63+
[<Fact>]
64+
let ``TaskSeq-contains _specialcase_ prove we don't read past the found item`` () = task {
65+
let mutable i = 0
66+
67+
let ts = taskSeq {
68+
for _ in 0..9 do
69+
i <- i + 1
70+
yield i
71+
}
72+
73+
let! found = ts |> TaskSeq.contains 3
74+
found |> should be True
75+
i |> should equal 3 // only partial evaluation!
76+
77+
// find next item. We do get a new iterator, but mutable state is now starting at '3', so first item now returned is '4'.
78+
let! found = ts |> TaskSeq.contains 4
79+
found |> should be True
80+
i |> should equal 4 // only partial evaluation!
81+
}
82+
83+
[<Fact>]
84+
let ``TaskSeq-contains _specialcase_ prove we don't read past the found item v2`` () = task {
85+
let mutable i = 0
86+
87+
let ts = taskSeq {
88+
yield 42
89+
i <- i + 1
90+
i <- i + 1
91+
}
92+
93+
let! found = ts |> TaskSeq.contains 42
94+
found |> should be True
95+
i |> should equal 0 // because no MoveNext after found item, the last statements are not executed
96+
}
97+
98+
[<Fact>]
99+
let ``TaskSeq-contains _specialcase_ prove statement after yield is not evaluated`` () = task {
100+
let mutable i = 0
101+
102+
let ts = taskSeq {
103+
for _ in 0..9 do
104+
yield i
105+
i <- i + 1
106+
}
107+
108+
let! found = ts |> TaskSeq.contains 0
109+
found |> should be True
110+
i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated
111+
112+
// find some next item. We do get a new iterator, but mutable state is now starting at '1'
113+
let! found = ts |> TaskSeq.contains 4
114+
found |> should be True
115+
i |> should equal 4 // only partial evaluation!
116+
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
module FSharpy.Tests.Exists
2+
3+
open Xunit
4+
open FsUnit.Xunit
5+
open FsToolkit.ErrorHandling
6+
7+
open FSharpy
8+
9+
//
10+
// TaskSeq.exists
11+
// TaskSeq.existsAsyncc
12+
//
13+
14+
module EmptySeq =
15+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
16+
let ``TaskSeq-exists returns false`` variant =
17+
Gen.getEmptyVariant variant
18+
|> TaskSeq.exists ((=) 12)
19+
|> Task.map (should be False)
20+
21+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
22+
let ``TaskSeq-existsAsync returns false`` variant =
23+
Gen.getEmptyVariant variant
24+
|> TaskSeq.existsAsync (fun x -> task { return x = 12 })
25+
|> Task.map (should be False)
26+
27+
module Immutable =
28+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
29+
let ``TaskSeq-exists sad path returns false`` variant =
30+
Gen.getSeqImmutable variant
31+
|> TaskSeq.exists ((=) 0)
32+
|> Task.map (should be False)
33+
34+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
35+
let ``TaskSeq-existsAsync sad path return false`` variant =
36+
Gen.getSeqImmutable variant
37+
|> TaskSeq.existsAsync (fun x -> task { return x = 0 })
38+
|> Task.map (should be False)
39+
40+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
41+
let ``TaskSeq-exists happy path middle of seq`` variant =
42+
Gen.getSeqImmutable variant
43+
|> TaskSeq.exists (fun x -> x < 6 && x > 4)
44+
|> Task.map (should be True)
45+
46+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
47+
let ``TaskSeq-existsAsync happy path middle of seq`` variant =
48+
Gen.getSeqImmutable variant
49+
|> TaskSeq.existsAsync (fun x -> task { return x < 6 && x > 4 })
50+
|> Task.map (should be True)
51+
52+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
53+
let ``TaskSeq-exists happy path first item of seq`` variant =
54+
Gen.getSeqImmutable variant
55+
|> TaskSeq.exists ((=) 1)
56+
|> Task.map (should be True)
57+
58+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
59+
let ``TaskSeq-existsAsync happy path first item of seq`` variant =
60+
Gen.getSeqImmutable variant
61+
|> TaskSeq.existsAsync (fun x -> task { return x = 1 })
62+
|> Task.map (should be True)
63+
64+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
65+
let ``TaskSeq-exists happy path last item of seq`` variant =
66+
Gen.getSeqImmutable variant
67+
|> TaskSeq.exists ((=) 10)
68+
|> Task.map (should be True)
69+
70+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
71+
let ``TaskSeq-existsAsync happy path last item of seq`` variant =
72+
Gen.getSeqImmutable variant
73+
|> TaskSeq.existsAsync (fun x -> task { return x = 10 })
74+
|> Task.map (should be True)
75+
76+
module SideEffects =
77+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
78+
let ``TaskSeq-exists KeyNotFoundException only sometimes for mutated state`` variant = task {
79+
let ts = Gen.getSeqWithSideEffect variant
80+
let finder = (=) 11
81+
82+
// first: false
83+
let! found = TaskSeq.exists finder ts
84+
found |> should be False
85+
86+
// find again: found now, because of side effects
87+
let! found = TaskSeq.exists finder ts
88+
found |> should be True
89+
90+
// find once more: false
91+
let! found = TaskSeq.exists finder ts
92+
found |> should be False
93+
}
94+
95+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
96+
let ``TaskSeq-existsAsync KeyNotFoundException only sometimes for mutated state`` variant = task {
97+
let ts = Gen.getSeqWithSideEffect variant
98+
let finder x = task { return x = 11 }
99+
100+
// first: false
101+
let! found = TaskSeq.existsAsync finder ts
102+
found |> should be False
103+
104+
// find again: found now, because of side effects
105+
let! found = TaskSeq.existsAsync finder ts
106+
found |> should be True
107+
108+
// find once more: false
109+
let! found = TaskSeq.existsAsync finder ts
110+
found |> should be False
111+
}
112+
113+
[<Fact>]
114+
let ``TaskSeq-exists _specialcase_ prove we don't read past the found item`` () = task {
115+
let mutable i = 0
116+
117+
let ts = taskSeq {
118+
for _ in 0..9 do
119+
i <- i + 1
120+
yield i
121+
}
122+
123+
let! found = ts |> TaskSeq.exists ((=) 3)
124+
found |> should be True
125+
i |> should equal 3 // only partial evaluation!
126+
127+
// find next item. We do get a new iterator, but mutable state is now starting at '3', so first item now returned is '4'.
128+
let! found = ts |> TaskSeq.exists ((=) 4)
129+
found |> should be True
130+
i |> should equal 4 // only partial evaluation!
131+
}
132+
133+
[<Fact>]
134+
let ``TaskSeq-existsAsync _specialcase_ prove we don't read past the found item`` () = task {
135+
let mutable i = 0
136+
137+
let ts = taskSeq {
138+
for _ in 0..9 do
139+
i <- i + 1
140+
yield i
141+
}
142+
143+
let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 3 })
144+
found |> should be True
145+
i |> should equal 3 // only partial evaluation!
146+
147+
// find next item. We do get a new iterator, but mutable state is now starting at '3', so first item now returned is '4'.
148+
let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 4 })
149+
found |> should be True
150+
i |> should equal 4
151+
}
152+
153+
[<Fact>]
154+
let ``TaskSeq-exists _specialcase_ prove we don't read past the found item v2`` () = task {
155+
let mutable i = 0
156+
157+
let ts = taskSeq {
158+
yield 42
159+
i <- i + 1
160+
i <- i + 1
161+
}
162+
163+
let! found = ts |> TaskSeq.exists ((=) 42)
164+
found |> should be True
165+
i |> should equal 0 // because no MoveNext after found item, the last statements are not executed
166+
}
167+
168+
[<Fact>]
169+
let ``TaskSeq-existsAsync _specialcase_ prove we don't read past the found item v2`` () = task {
170+
let mutable i = 0
171+
172+
let ts = taskSeq {
173+
yield 42
174+
i <- i + 1
175+
i <- i + 1
176+
}
177+
178+
let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 42 })
179+
found |> should be True
180+
i |> should equal 0 // because no MoveNext after found item, the last statements are not executed
181+
}
182+
183+
[<Fact>]
184+
let ``TaskSeq-exists _specialcase_ prove statement after yield is not evaluated`` () = task {
185+
let mutable i = 0
186+
187+
let ts = taskSeq {
188+
for _ in 0..9 do
189+
yield i
190+
i <- i + 1
191+
}
192+
193+
let! found = ts |> TaskSeq.exists ((=) 0)
194+
found |> should be True
195+
i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated
196+
197+
// find some next item. We do get a new iterator, but mutable state is now starting at '1'
198+
let! found = ts |> TaskSeq.exists ((=) 4)
199+
found |> should be True
200+
i |> should equal 4 // only partial evaluation!
201+
}
202+
203+
[<Fact>]
204+
let ``TaskSeq-existsAsync _specialcase_ prove statement after yield is not evaluated`` () = task {
205+
let mutable i = 0
206+
207+
let ts = taskSeq {
208+
for _ in 0..9 do
209+
yield i
210+
i <- i + 1
211+
}
212+
213+
let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 0 })
214+
found |> should be True
215+
i |> should equal 0 // notice that it should be one higher if the statement after 'yield' is evaluated
216+
217+
// find some next item. We do get a new iterator, but mutable state is now starting at '1'
218+
let! found = ts |> TaskSeq.existsAsync (fun x -> task { return x = 4 })
219+
found |> should be True
220+
i |> should equal 4 // only partial evaluation!
221+
}

src/FSharpy.TaskSeq/TaskSeq.fs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,18 @@ module TaskSeq =
254254
let tryFindIndex predicate source = Internal.tryFindIndex (Predicate predicate) source
255255
let tryFindIndexAsync predicate source = Internal.tryFindIndex (PredicateAsync predicate) source
256256

257+
let exists predicate source =
258+
Internal.tryFind (Predicate predicate) source
259+
|> Task.map (Option.isSome)
260+
261+
let existsAsync predicate source =
262+
Internal.tryFind (PredicateAsync predicate) source
263+
|> Task.map (Option.isSome)
264+
265+
let contains value source =
266+
Internal.tryFind (Predicate((=) value)) source
267+
|> Task.map (Option.isSome)
268+
257269
let pick chooser source = task {
258270
match! Internal.tryPick (TryPick chooser) source with
259271
| Some item -> return item

0 commit comments

Comments
 (0)