diff --git a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj index 209204e5..e18b3a0e 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -35,6 +35,7 @@ + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Last.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Last.Tests.fs index 9fdae2f5..31085217 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Last.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Last.Tests.fs @@ -15,19 +15,19 @@ open FSharp.Control module EmptySeq = [)>] - let ``TaskSeq-last throws on empty sequences`` variant = task { + let ``TaskSeq-last throws`` variant = task { fun () -> Gen.getEmptyVariant variant |> TaskSeq.last |> Task.ignore |> should throwAsyncExact typeof } [)>] - let ``TaskSeq-tryLast returns None on empty sequences`` variant = task { + let ``TaskSeq-tryLast returns None`` variant = task { let! nothing = Gen.getEmptyVariant variant |> TaskSeq.tryLast nothing |> should be None' } [] - let ``TaskSeq-last throws on empty sequences, but side effect is executed`` () = task { + let ``TaskSeq-last executes side effect`` () = task { let mutable x = 0 fun () -> taskSeq { do x <- x + 1 } |> TaskSeq.last |> Task.ignore @@ -37,10 +37,21 @@ module EmptySeq = x |> should equal 1 } + [] + let ``TaskSeq-tryLast executes side effect`` () = task { + let mutable x = 0 + + let! nothing = taskSeq { do x <- x + 1 } |> TaskSeq.tryLast + nothing |> should be None' + + // side effect must have run! + x |> should equal 1 + } + module Immutable = [)>] - let ``TaskSeq-last gets the last item in a longer sequence`` variant = task { + let ``TaskSeq-last gets the last item`` variant = task { let ts = Gen.getSeqImmutable variant let! last = TaskSeq.last ts @@ -62,7 +73,7 @@ module Immutable = } [)>] - let ``TaskSeq-tryLast gets the last item in a longer sequence`` variant = task { + let ``TaskSeq-tryLast gets the last item`` variant = task { let ts = Gen.getSeqImmutable variant let! last = TaskSeq.tryLast ts @@ -86,7 +97,7 @@ module Immutable = module SideEffects = [] - let ``TaskSeq-last gets the only item in a singleton sequence, with change`` () = task { + let ``TaskSeq-last executes side effect after first item`` () = task { let mutable x = 42 let one = taskSeq { @@ -102,7 +113,7 @@ module SideEffects = } [] - let ``TaskSeq-tryLast gets the only item in a singleton sequence, with change`` () = task { + let ``TaskSeq-tryLast executes side effect after first item`` () = task { let mutable x = 42 let one = taskSeq { @@ -120,7 +131,7 @@ module SideEffects = } [)>] - let ``TaskSeq-last gets the last item in a longer sequence, with change`` variant = task { + let ``TaskSeq-last gets the last item`` variant = task { let ts = Gen.getSeqWithSideEffect variant let! ten = TaskSeq.last ts @@ -132,7 +143,7 @@ module SideEffects = } [)>] - let ``TaskSeq-tryLast gets the last item in a longer sequence, with change`` variant = task { + let ``TaskSeq-tryLast gets the last item`` variant = task { let ts = Gen.getSeqWithSideEffect variant let! ten = TaskSeq.tryLast ts diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Tail.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Tail.Tests.fs new file mode 100644 index 00000000..ee38b1ad --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Tail.Tests.fs @@ -0,0 +1,182 @@ +module TaskSeq.Tests.Tail + +open System +open Xunit +open FsUnit.Xunit +open FsToolkit.ErrorHandling + +open FSharp.Control + +// +// TaskSeq.tail +// TaskSeq.tryTail +// + +module EmptySeq = + + [)>] + let ``TaskSeq-tail throws`` variant = task { + fun () -> Gen.getEmptyVariant variant |> TaskSeq.tail |> Task.ignore + |> should throwAsyncExact typeof + } + + [)>] + let ``TaskSeq-tryTail returns None`` variant = task { + let! nothing = Gen.getEmptyVariant variant |> TaskSeq.tryTail + nothing |> should be None' + } + + [] + let ``TaskSeq-tail executes side effect`` () = task { + let mutable x = 0 + + fun () -> taskSeq { do x <- x + 1 } |> TaskSeq.tail |> Task.ignore + |> should throwAsyncExact typeof + + // side effect must have run! + x |> should equal 1 + } + + [] + let ``TaskSeq-tryTail executes side effect`` () = task { + let mutable x = 0 + + let! nothing = taskSeq { do x <- x + 1 } |> TaskSeq.tryTail + nothing |> should be None' + + // side effect must have run! + x |> should equal 1 + } + + +module Immutable = + let verifyTail tail = + tail + |> TaskSeq.toArrayAsync + |> Task.map (should equal [| 2..10 |]) + + [)>] + let ``TaskSeq-tail gets the tail items`` variant = task { + let ts = Gen.getSeqImmutable variant + + let! tail = TaskSeq.tail ts + do! verifyTail tail + + let! tail = TaskSeq.tail ts //immutable, so re-iteration does not change outcome + do! verifyTail tail + } + + [)>] + let ``TaskSeq-tryTail gets the tail item`` variant = task { + let ts = Gen.getSeqImmutable variant + + match! TaskSeq.tryTail ts with + | Some tail -> do! verifyTail tail + | x -> do x |> should not' (be None') + + } + + [] + let ``TaskSeq-tail return empty from a singleton sequence`` () = task { + let ts = taskSeq { yield 42 } + + let! tail = TaskSeq.tail ts + do! verifyEmpty tail + } + + [] + let ``TaskSeq-tryTail gets the only item in a singleton sequence`` () = task { + let ts = taskSeq { yield 42 } + + match! TaskSeq.tryTail ts with + | Some tail -> do! verifyEmpty tail + | x -> do x |> should not' (be None') + } + + +module SideEffects = + [] + let ``TaskSeq-tail does not execute side effect after the first item in singleton`` () = task { + let mutable x = 42 + + let one = taskSeq { + yield x + x <- x + 1 // <--- we should never get here + } + + let! _ = one |> TaskSeq.tail + let! _ = one |> TaskSeq.tail // side effect, re-iterating! + + x |> should equal 42 + } + + [] + let ``TaskSeq-tryTail does not execute execute side effect after first item in singleton`` () = task { + let mutable x = 42 + + let one = taskSeq { + yield x + x <- x + 1 // <--- we should never get here + } + + let! _ = one |> TaskSeq.tryTail + let! _ = one |> TaskSeq.tryTail + + // side effect, reiterating causes it to execute again! + x |> should equal 42 + + } + + [] + let ``TaskSeq-tail executes side effect partially`` () = task { + let mutable x = 42 + + let ts = taskSeq { + x <- x + 1 // <--- executed on tail, but not materializing rest + yield 1 + x <- x + 1 // <--- not executed on tail, but on materializing rest + yield 2 + x <- x + 1 // <--- id + } + + let! tail1 = ts |> TaskSeq.tail + x |> should equal 43 // test side effect runs 1x + + let! tail2 = ts |> TaskSeq.tail + x |> should equal 44 // test side effect ran again only 1x + + let! len = TaskSeq.length tail1 + x |> should equal 46 // now 2nd & 3rd side effect runs, but not the first + len |> should equal 1 + + let! len = TaskSeq.length tail2 + x |> should equal 48 // now again 2nd & 3rd side effect runs, but not the first + len |> should equal 1 + } + + [] + let ``TaskSeq-tryTail executes side effect partially`` () = task { + let mutable x = 42 + + let ts = taskSeq { + x <- x + 1 // <--- executed on tail, but not materializing rest + yield 1 + x <- x + 1 // <--- not executed on tail, but on materializing rest + yield 2 + x <- x + 1 // <--- id + } + + let! tail1 = ts |> TaskSeq.tryTail + x |> should equal 43 // test side effect runs 1x + + let! tail2 = ts |> TaskSeq.tryTail + x |> should equal 44 // test side effect ran again only 1x + + let! len = TaskSeq.length tail1.Value + x |> should equal 46 // now 2nd side effect runs, but not the first + len |> should equal 1 + + let! len = TaskSeq.length tail2.Value + x |> should equal 48 // now again 2nd side effect runs, but not the first + len |> should equal 1 + } diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index 3a3b66a3..f66d7e94 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -215,6 +215,13 @@ module TaskSeq = | None -> return Internal.raiseEmptySeq () } + let tryTail source = Internal.tryTail source + + let tail source = task { + match! Internal.tryTail source with + | Some result -> return result + | None -> return Internal.raiseEmptySeq () + } let tryItem index source = Internal.tryItem index source let item index source = task { diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index 7f09fda3..fce10970 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -232,21 +232,30 @@ module TaskSeq = val collectSeqAsync: binder: ('T -> #Task<'SeqU>) -> source: taskSeq<'T> -> taskSeq<'U> when 'SeqU :> seq<'U> /// - /// Returns the first element of the , or if the sequence is empty. + /// Returns the first element of the task sequence from , or if the sequence is empty. /// - /// Thrown when the sequence is empty. val tryHead: source: taskSeq<'T> -> Task<'T option> /// - /// Returns the first element of the . + /// Returns the first elementof the task sequence from /// /// Thrown when the sequence is empty. val head: source: taskSeq<'T> -> Task<'T> /// - /// Returns the last element of the , or if the sequence is empty. + /// Returns the whole task sequence from , minus its first element, or if the sequence is empty. + /// + val tryTail: source: taskSeq<'T> -> Task option> + + /// + /// Returns the whole task sequence from , minus its first element. /// /// Thrown when the sequence is empty. + val tail: source: taskSeq<'T> -> Task> + + /// + /// Returns the last element of the task sequence from , or if the sequence is empty. + /// val tryLast: source: taskSeq<'T> -> Task<'T option> /// diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index 115436ef..f0bcec9d 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -338,11 +338,30 @@ module internal TaskSeqInternal = let tryHead (source: taskSeq<_>) = task { use e = source.GetAsyncEnumerator(CancellationToken()) - let mutable go = true - let! step = e.MoveNextAsync() - go <- step - if go then return Some e.Current else return None + match! e.MoveNextAsync() with + | true -> return Some e.Current + | false -> return None + } + + let tryTail (source: taskSeq<_>) = task { + use e = source.GetAsyncEnumerator(CancellationToken()) + + match! e.MoveNextAsync() with + | false -> return None + | true -> + return + taskSeq { + let mutable go = true + let! step = e.MoveNextAsync() + go <- step + + while go do + yield e.Current + let! step = e.MoveNextAsync() + go <- step + } + |> Some } let tryItem index (source: taskSeq<_>) = task { diff --git a/src/FSharp.Control.TaskSeq/Utils.fs b/src/FSharp.Control.TaskSeq/Utils.fs index fa9b2b69..ecc2f685 100644 --- a/src/FSharp.Control.TaskSeq/Utils.fs +++ b/src/FSharp.Control.TaskSeq/Utils.fs @@ -46,7 +46,11 @@ module Task = } /// Bind a Task<'T> - let inline bind binder (task: Task<'T>) : Task<'U> = TaskBuilder.task { return! binder task } + let inline bind binder (task: Task<'T>) : Task<'U> = + TaskBuilder.task { + let! t = task + return! binder t + } /// Create a task from a value let inline fromResult (value: 'U) : Task<'U> = TaskBuilder.task { return value }