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 }