diff --git a/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj b/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj index d67231ca..92376960 100644 --- a/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj +++ b/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj @@ -12,6 +12,7 @@ + diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Cast.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Cast.Tests.fs new file mode 100644 index 00000000..334ed685 --- /dev/null +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Cast.Tests.fs @@ -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 = + [)>] + let ``TaskSeq-box empty`` variant = Gen.getEmptyVariant variant |> TaskSeq.box |> verifyEmpty + + [)>] + let ``TaskSeq-unbox empty`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.box + |> TaskSeq.unbox + |> verifyEmpty + + [)>] + let ``TaskSeq-cast empty`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.box + |> TaskSeq.cast + |> verifyEmpty + + [)>] + let ``TaskSeq-unbox empty to invalid type should not fail`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.box + |> TaskSeq.unbox // cannot cast to int, but for empty sequences, the exception won't be thrown + |> verifyEmpty + + [)>] + let ``TaskSeq-cast empty to invalid type should not fail`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.box + |> TaskSeq.cast // cannot cast to int, but for empty sequences, the exception won't be thrown + |> verifyEmpty + +module Immutable = + [)>] + let ``TaskSeq-box`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.box + |> validateSequence + + [)>] + let ``TaskSeq-unbox`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.box + |> TaskSeq.unbox + |> validateSequence + + [)>] + let ``TaskSeq-cast`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.box + |> TaskSeq.cast + |> validateSequence + + [)>] + let ``TaskSeq-unbox invalid type should throw`` variant = + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.box + |> TaskSeq.unbox // cannot unbox from int to uint, even though types have the same size + |> TaskSeq.toArrayAsync + |> Task.ignore + + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-cast invalid type should throw`` variant = + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.box + |> TaskSeq.cast + |> TaskSeq.toArrayAsync + |> Task.ignore + + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-unbox invalid type should NOT throw before sequence is iterated`` variant = + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.box + |> TaskSeq.unbox // no iteration done + |> ignore + + |> should not' (throw typeof) + + [)>] + let ``TaskSeq-cast invalid type should NOT throw before sequence is iterated`` variant = + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.box + |> TaskSeq.cast // no iteration done + |> ignore + + |> should not' (throw typeof) + +module SideEffects = + [] + 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) + + [] + 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) + + [] + 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 + i |> should equal 0 // no side effect until iterated + + cast + |> TaskSeq.last + |> Task.map (should equal 42) + |> Task.map (fun () -> i = 3) diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Map.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Map.Tests.fs index f9dc6a3a..4b91f58c 100644 --- a/src/FSharpy.TaskSeq.Test/TaskSeq.Map.Tests.fs +++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Map.Tests.fs @@ -36,25 +36,25 @@ let validateSequenceWithOffset offset ts = module EmptySeq = [)>] - let ``TaskSeq-map maps in correct order`` variant = + let ``TaskSeq-map empty`` variant = Gen.getEmptyVariant variant |> TaskSeq.map (fun item -> char (item + 64)) |> verifyEmpty [)>] - let ``TaskSeq-mapi maps in correct order`` variant = + let ``TaskSeq-mapi empty`` variant = Gen.getEmptyVariant variant |> TaskSeq.mapi (fun i _ -> char (i + 65)) |> verifyEmpty [)>] - 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 [)>] - 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 diff --git a/src/FSharpy.TaskSeq/TaskSeq.fs b/src/FSharpy.TaskSeq/TaskSeq.fs index f815a750..9e47f186 100644 --- a/src/FSharpy.TaskSeq/TaskSeq.fs +++ b/src/FSharpy.TaskSeq/TaskSeq.fs @@ -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) : 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 diff --git a/src/FSharpy.TaskSeq/TaskSeq.fsi b/src/FSharpy.TaskSeq/TaskSeq.fsi index 50415e2c..7463d74a 100644 --- a/src/FSharpy.TaskSeq/TaskSeq.fsi +++ b/src/FSharpy.TaskSeq/TaskSeq.fsi @@ -87,6 +87,25 @@ module TaskSeq = /// Create a taskSeq of an array of async. val ofAsyncArray: source: Async<'T> array -> taskSeq<'T> + /// + /// Boxes as type each item in the sequence asynchyronously. + /// + val box: source: taskSeq<'T> -> taskSeq + + /// + /// Unboxes to the target type each item in the sequence asynchyronously. + /// The target type must be a or a built-in value type. + /// + /// Thrown when the function is unable to cast an item to the target type. + val unbox<'U when 'U: struct> : source: taskSeq -> taskSeq<'U> + + /// + /// Casts each item in the untyped sequence asynchyronously. If your types are boxed struct types + /// it is recommended to use instead. + /// + /// Thrown when the function is unable to cast an item to the target type. + val cast: source: taskSeq -> 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 diff --git a/src/FSharpy.TaskSeq/Utils.fs b/src/FSharpy.TaskSeq/Utils.fs index 12f2edcb..cefef0f1 100644 --- a/src/FSharpy.TaskSeq/Utils.fs +++ b/src/FSharpy.TaskSeq/Utils.fs @@ -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