Skip to content

Commit dcda447

Browse files
committed
Implement TaskSeq.box/unbox/cast and add tests for them
1 parent dbd93dd commit dcda447

File tree

5 files changed

+205
-5
lines changed

5 files changed

+205
-5
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<Compile Include="AssemblyInfo.fs" />
1313
<Compile Include="Nunit.Extensions.fs" />
1414
<Compile Include="TestUtils.fs" />
15+
<Compile Include="TaskSeq.Cast.Tests.fs" />
1516
<Compile Include="TaskSeq.Choose.Tests.fs" />
1617
<Compile Include="TaskSeq.Collect.Tests.fs" />
1718
<Compile Include="TaskSeq.Empty.Tests.fs" />
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
module FSharpy.Tests.Cast
2+
3+
open System
4+
5+
open Xunit
6+
open FsUnit.Xunit
7+
open FsToolkit.ErrorHandling
8+
9+
open FSharpy
10+
11+
//
12+
// TaskSeq.box
13+
// TaskSeq.unbox
14+
// TaskSeq.cast
15+
//
16+
17+
/// Asserts that a sequence contains the char values 'A'..'J'.
18+
let validateSequence ts =
19+
ts
20+
|> TaskSeq.toSeqCachedAsync
21+
|> Task.map (Seq.map string)
22+
|> Task.map (String.concat "")
23+
|> Task.map (should equal "12345678910")
24+
25+
module EmptySeq =
26+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
27+
let ``TaskSeq-box empty`` variant = Gen.getEmptyVariant variant |> TaskSeq.box |> verifyEmpty
28+
29+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
30+
let ``TaskSeq-unbox empty`` variant =
31+
Gen.getEmptyVariant variant
32+
|> TaskSeq.box
33+
|> TaskSeq.unbox<int>
34+
|> verifyEmpty
35+
36+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
37+
let ``TaskSeq-cast empty`` variant =
38+
Gen.getEmptyVariant variant
39+
|> TaskSeq.box
40+
|> TaskSeq.cast<int>
41+
|> verifyEmpty
42+
43+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
44+
let ``TaskSeq-unbox empty to invalid type should not fail`` variant =
45+
Gen.getEmptyVariant variant
46+
|> TaskSeq.box
47+
|> TaskSeq.unbox<Guid> // cannot cast to int, but for empty sequences, the exception won't be thrown
48+
|> verifyEmpty
49+
50+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
51+
let ``TaskSeq-cast empty to invalid type should not fail`` variant =
52+
Gen.getEmptyVariant variant
53+
|> TaskSeq.box
54+
|> TaskSeq.cast<string> // cannot cast to int, but for empty sequences, the exception won't be thrown
55+
|> verifyEmpty
56+
57+
module Immutable =
58+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
59+
let ``TaskSeq-box`` variant =
60+
Gen.getSeqImmutable variant
61+
|> TaskSeq.box
62+
|> validateSequence
63+
64+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
65+
let ``TaskSeq-unbox`` variant =
66+
Gen.getSeqImmutable variant
67+
|> TaskSeq.box
68+
|> TaskSeq.unbox<int>
69+
|> validateSequence
70+
71+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
72+
let ``TaskSeq-cast`` variant =
73+
Gen.getSeqImmutable variant
74+
|> TaskSeq.box
75+
|> TaskSeq.cast<int>
76+
|> validateSequence
77+
78+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
79+
let ``TaskSeq-unbox invalid type should throw`` variant =
80+
fun () ->
81+
Gen.getSeqImmutable variant
82+
|> TaskSeq.box
83+
|> TaskSeq.unbox<uint> // cannot unbox from int to uint, even though types have the same size
84+
|> TaskSeq.toArrayAsync
85+
|> Task.ignore
86+
87+
|> should throwAsyncExact typeof<InvalidCastException>
88+
89+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
90+
let ``TaskSeq-cast invalid type should throw`` variant =
91+
fun () ->
92+
Gen.getSeqImmutable variant
93+
|> TaskSeq.box
94+
|> TaskSeq.cast<string>
95+
|> TaskSeq.toArrayAsync
96+
|> Task.ignore
97+
98+
|> should throwAsyncExact typeof<InvalidCastException>
99+
100+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
101+
let ``TaskSeq-unbox invalid type should NOT throw before sequence is iterated`` variant =
102+
fun () ->
103+
Gen.getSeqImmutable variant
104+
|> TaskSeq.box
105+
|> TaskSeq.unbox<uint> // no iteration done
106+
|> ignore
107+
108+
|> should not' (throw typeof<Exception>)
109+
110+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
111+
let ``TaskSeq-cast invalid type should NOT throw before sequence is iterated`` variant =
112+
fun () ->
113+
Gen.getSeqImmutable variant
114+
|> TaskSeq.box
115+
|> TaskSeq.cast<string> // no iteration done
116+
|> ignore
117+
118+
|> should not' (throw typeof<Exception>)
119+
120+
module SideEffects =
121+
[<Fact>]
122+
let ``TaskSeq-box prove that it has no effect until executed`` () =
123+
let mutable i = 0
124+
125+
let ts = taskSeq {
126+
i <- i + 1 // we should not get here
127+
i <- i + 1
128+
yield 42
129+
i <- i + 1
130+
}
131+
132+
// point of this test: just calling 'box' won't execute anything of the sequence!
133+
let boxed = ts |> TaskSeq.box |> TaskSeq.box |> TaskSeq.box
134+
135+
// no side effect until iterated
136+
i |> should equal 0
137+
138+
boxed
139+
|> TaskSeq.last
140+
|> Task.map (should equal 42)
141+
|> Task.map (fun () -> i = 9)
142+
143+
[<Fact>]
144+
let ``TaskSeq-unbox prove that it has no effect until executed`` () =
145+
let mutable i = 0
146+
147+
let ts = taskSeq {
148+
i <- i + 1 // we should not get here
149+
i <- i + 1
150+
yield box 42
151+
i <- i + 1
152+
}
153+
154+
// point of this test: just calling 'unbox' won't execute anything of the sequence!
155+
let unboxed = ts |> TaskSeq.unbox
156+
157+
// no side effect until iterated
158+
i |> should equal 0
159+
160+
unboxed
161+
|> TaskSeq.last
162+
|> Task.map (should equal 42)
163+
|> Task.map (fun () -> i = 3)
164+
165+
[<Fact>]
166+
let ``TaskSeq-cast prove that it has no effect until executed`` () =
167+
let mutable i = 0
168+
169+
let ts = taskSeq {
170+
i <- i + 1 // we should not get here
171+
i <- i + 1
172+
yield box 42
173+
i <- i + 1
174+
}
175+
176+
// point of this test: just calling 'cast' won't execute anything of the sequence!
177+
let cast = ts |> TaskSeq.cast<int>
178+
i |> should equal 0 // no side effect until iterated
179+
180+
cast
181+
|> TaskSeq.last
182+
|> Task.map (should equal 42)
183+
|> Task.map (fun () -> i = 3)

src/FSharpy.TaskSeq/TaskSeq.fs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ module TaskSeq =
163163
// iter/map/collect functions
164164
//
165165

166-
let cast source : taskSeq<'U> = Internal.map (SimpleAction(fun x -> box x :?> 'U)) source
166+
let cast source : taskSeq<'T> = Internal.map (SimpleAction(fun (x: obj) -> x :?> 'T)) source
167+
let box source = Internal.map (SimpleAction(fun x -> box x)) source
168+
let unbox<'U when 'U: struct> (source: taskSeq<obj>) : taskSeq<'U> = Internal.map (SimpleAction(fun x -> unbox x)) source
167169
let iter action source = Internal.iter (SimpleAction action) source
168170
let iteri action source = Internal.iter (CountableAction action) source
169171
let iterAsync action source = Internal.iter (AsyncSimpleAction action) source

src/FSharpy.TaskSeq/TaskSeq.fsi

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,23 @@ module TaskSeq =
8888
val ofAsyncArray: source: Async<'T> array -> taskSeq<'T>
8989

9090
/// <summary>
91-
/// Casts each item in the <paramref name="source" /> sequence asynchyronously. This function does unconstrainted casting,
92-
/// by boxing the value and then casting it to the target type. For non-reference types, it is recommended
93-
/// to use <see cref="TaskSeq.map" /> instead.
91+
/// Boxes as type <see cref="obj" /> each item in the <paramref name="source" /> sequence asynchyronously.
92+
/// </summary>
93+
val box: source: taskSeq<'T> -> taskSeq<obj>
94+
95+
/// <summary>
96+
/// Unboxes to the target type <see cref="'U" /> each item in the <paramref name="source" /> sequence asynchyronously.
97+
/// The target type must be a <see cref="struct" /> or a built-in value type.
98+
/// </summary>
99+
/// <exception cref="InvalidCastException">Thrown when the function is unable to cast an item to the target type.</exception>
100+
val unbox<'U when 'U: struct> : source: taskSeq<obj> -> taskSeq<'U>
101+
102+
/// <summary>
103+
/// Casts each item in the untyped <paramref name="source" /> sequence asynchyronously. If your types are boxed struct types
104+
/// it is recommended to use <see cref="TaskSeq.unbox" /> instead.
94105
/// </summary>
95106
/// <exception cref="InvalidCastException">Thrown when the function is unable to cast an item to the target type.</exception>
96-
val cast: source: taskSeq<'T> -> taskSeq<'U>
107+
val cast: source: taskSeq<obj> -> taskSeq<'T>
97108

98109
/// Iterates over the taskSeq applying the action function to each item. This function is non-blocking
99110
/// exhausts the sequence as soon as the task is evaluated.

src/FSharpy.TaskSeq/Utils.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ module Task =
4848
/// Bind a Task<'T>
4949
let inline bind binder (task: Task<'T>) : Task<'U> = TaskBuilder.task { return! binder task }
5050

51+
/// Create a task from a value
52+
let inline fromResult (value: 'U) : Task<'U> = TaskBuilder.task { return value }
53+
5154
module Async =
5255
/// Convert an Task<'T> into an Async<'T>
5356
let inline ofTask (task: Task<'T>) = Async.AwaitTask task

0 commit comments

Comments
 (0)