Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more surface area functions: TaskSeq.cast, box and unbox #67

Merged
merged 3 commits into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<Compile Include="AssemblyInfo.fs" />
<Compile Include="Nunit.Extensions.fs" />
<Compile Include="TestUtils.fs" />
<Compile Include="TaskSeq.Cast.Tests.fs" />
<Compile Include="TaskSeq.Choose.Tests.fs" />
<Compile Include="TaskSeq.Collect.Tests.fs" />
<Compile Include="TaskSeq.Empty.Tests.fs" />
Expand Down
183 changes: 183 additions & 0 deletions src/FSharpy.TaskSeq.Test/TaskSeq.Cast.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
module FSharpy.Tests.Cast

open System

open Xunit
open FsUnit.Xunit
open FsToolkit.ErrorHandling

open FSharpy

//
// TaskSeq.box
// TaskSeq.unbox
// TaskSeq.cast
//

/// Asserts that a sequence contains the char values 'A'..'J'.
let validateSequence ts =
ts
|> TaskSeq.toSeqCachedAsync
|> Task.map (Seq.map string)
|> Task.map (String.concat "")
|> Task.map (should equal "12345678910")

module EmptySeq =
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-box empty`` variant = Gen.getEmptyVariant variant |> TaskSeq.box |> verifyEmpty

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-unbox empty`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.box
|> TaskSeq.unbox<int>
|> verifyEmpty

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-cast empty`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.box
|> TaskSeq.cast<int>
|> verifyEmpty

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-unbox empty to invalid type should not fail`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.box
|> TaskSeq.unbox<Guid> // cannot cast to int, but for empty sequences, the exception won't be thrown
|> verifyEmpty

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-cast empty to invalid type should not fail`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.box
|> TaskSeq.cast<string> // cannot cast to int, but for empty sequences, the exception won't be thrown
|> verifyEmpty

module Immutable =
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-box`` variant =
Gen.getSeqImmutable variant
|> TaskSeq.box
|> validateSequence

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-unbox`` variant =
Gen.getSeqImmutable variant
|> TaskSeq.box
|> TaskSeq.unbox<int>
|> validateSequence

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-cast`` variant =
Gen.getSeqImmutable variant
|> TaskSeq.box
|> TaskSeq.cast<int>
|> validateSequence

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-unbox invalid type should throw`` variant =
fun () ->
Gen.getSeqImmutable variant
|> TaskSeq.box
|> TaskSeq.unbox<uint> // cannot unbox from int to uint, even though types have the same size
|> TaskSeq.toArrayAsync
|> Task.ignore

|> should throwAsyncExact typeof<InvalidCastException>

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-cast invalid type should throw`` variant =
fun () ->
Gen.getSeqImmutable variant
|> TaskSeq.box
|> TaskSeq.cast<string>
|> TaskSeq.toArrayAsync
|> Task.ignore

|> should throwAsyncExact typeof<InvalidCastException>

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-unbox invalid type should NOT throw before sequence is iterated`` variant =
fun () ->
Gen.getSeqImmutable variant
|> TaskSeq.box
|> TaskSeq.unbox<uint> // no iteration done
|> ignore

|> should not' (throw typeof<Exception>)

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-cast invalid type should NOT throw before sequence is iterated`` variant =
fun () ->
Gen.getSeqImmutable variant
|> TaskSeq.box
|> TaskSeq.cast<string> // no iteration done
|> ignore

|> should not' (throw typeof<Exception>)

module SideEffects =
[<Fact>]
let ``TaskSeq-box prove that it has no effect until executed`` () =
let mutable i = 0

let ts = taskSeq {
i <- i + 1 // we should not get here
i <- i + 1
yield 42
i <- i + 1
}

// point of this test: just calling 'box' won't execute anything of the sequence!
let boxed = ts |> TaskSeq.box |> TaskSeq.box |> TaskSeq.box

// no side effect until iterated
i |> should equal 0

boxed
|> TaskSeq.last
|> Task.map (should equal 42)
|> Task.map (fun () -> i = 9)

[<Fact>]
let ``TaskSeq-unbox prove that it has no effect until executed`` () =
let mutable i = 0

let ts = taskSeq {
i <- i + 1 // we should not get here
i <- i + 1
yield box 42
i <- i + 1
}

// point of this test: just calling 'unbox' won't execute anything of the sequence!
let unboxed = ts |> TaskSeq.unbox

// no side effect until iterated
i |> should equal 0

unboxed
|> TaskSeq.last
|> Task.map (should equal 42)
|> Task.map (fun () -> i = 3)

[<Fact>]
let ``TaskSeq-cast prove that it has no effect until executed`` () =
let mutable i = 0

let ts = taskSeq {
i <- i + 1 // we should not get here
i <- i + 1
yield box 42
i <- i + 1
}

// point of this test: just calling 'cast' won't execute anything of the sequence!
let cast = ts |> TaskSeq.cast<int>
i |> should equal 0 // no side effect until iterated

cast
|> TaskSeq.last
|> Task.map (should equal 42)
|> Task.map (fun () -> i = 3)
8 changes: 4 additions & 4 deletions src/FSharpy.TaskSeq.Test/TaskSeq.Map.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,25 @@ let validateSequenceWithOffset offset ts =

module EmptySeq =
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-map maps in correct order`` variant =
let ``TaskSeq-map empty`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.map (fun item -> char (item + 64))
|> verifyEmpty

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-mapi maps in correct order`` variant =
let ``TaskSeq-mapi empty`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.mapi (fun i _ -> char (i + 65))
|> verifyEmpty

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-mapAsync maps in correct order`` variant =
let ``TaskSeq-mapAsync empty`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.mapAsync (fun item -> task { return char (item + 64) })
|> verifyEmpty

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-mapiAsync maps in correct order`` variant =
let ``TaskSeq-mapiAsync empty`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.mapiAsync (fun i _ -> task { return char (i + 65) })
|> verifyEmpty
Expand Down
3 changes: 3 additions & 0 deletions src/FSharpy.TaskSeq/TaskSeq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ module TaskSeq =
// iter/map/collect functions
//

let cast source : taskSeq<'T> = Internal.map (SimpleAction(fun (x: obj) -> x :?> 'T)) source
let box source = Internal.map (SimpleAction(fun x -> box x)) source
let unbox<'U when 'U: struct> (source: taskSeq<obj>) : taskSeq<'U> = Internal.map (SimpleAction(fun x -> unbox x)) source
let iter action source = Internal.iter (SimpleAction action) source
let iteri action source = Internal.iter (CountableAction action) source
let iterAsync action source = Internal.iter (AsyncSimpleAction action) source
Expand Down
19 changes: 19 additions & 0 deletions src/FSharpy.TaskSeq/TaskSeq.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@ module TaskSeq =
/// Create a taskSeq of an array of async.
val ofAsyncArray: source: Async<'T> array -> taskSeq<'T>

/// <summary>
/// Boxes as type <see cref="obj" /> each item in the <paramref name="source" /> sequence asynchyronously.
/// </summary>
val box: source: taskSeq<'T> -> taskSeq<obj>

/// <summary>
/// Unboxes to the target type <see cref="'U" /> each item in the <paramref name="source" /> sequence asynchyronously.
/// The target type must be a <see cref="struct" /> or a built-in value type.
/// </summary>
/// <exception cref="InvalidCastException">Thrown when the function is unable to cast an item to the target type.</exception>
val unbox<'U when 'U: struct> : source: taskSeq<obj> -> taskSeq<'U>

/// <summary>
/// Casts each item in the untyped <paramref name="source" /> sequence asynchyronously. If your types are boxed struct types
/// it is recommended to use <see cref="TaskSeq.unbox" /> instead.
/// </summary>
/// <exception cref="InvalidCastException">Thrown when the function is unable to cast an item to the target type.</exception>
val cast: source: taskSeq<obj> -> taskSeq<'T>

/// Iterates over the taskSeq applying the action function to each item. This function is non-blocking
/// exhausts the sequence as soon as the task is evaluated.
val iter: action: ('T -> unit) -> source: taskSeq<'T> -> Task<unit>
Expand Down
3 changes: 3 additions & 0 deletions src/FSharpy.TaskSeq/Utils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ module Task =
/// Bind a Task<'T>
let inline bind binder (task: Task<'T>) : Task<'U> = TaskBuilder.task { return! binder task }

/// Create a task from a value
let inline fromResult (value: 'U) : Task<'U> = TaskBuilder.task { return value }

module Async =
/// Convert an Task<'T> into an Async<'T>
let inline ofTask (task: Task<'T>) = Async.AwaitTask task
Expand Down