Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<Compile Include="TaskSeq.Map.Tests.fs" />
<Compile Include="TaskSeq.OfXXX.Tests.fs" />
<Compile Include="TaskSeq.Pick.Tests.fs" />
<Compile Include="TaskSeq.Tail.Tests.fs" />
<Compile Include="TaskSeq.ToXXX.Tests.fs" />
<Compile Include="TaskSeq.Zip.Tests.fs" />
<Compile Include="TaskSeq.Tests.CE.fs" />
Expand Down
29 changes: 20 additions & 9 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Last.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ open FSharp.Control
module EmptySeq =

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
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<ArgumentException>
}

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
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'
}

[<Fact>]
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
Expand All @@ -37,10 +37,21 @@ module EmptySeq =
x |> should equal 1
}

[<Fact>]
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 =
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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
Expand All @@ -62,7 +73,7 @@ module Immutable =
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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
Expand All @@ -86,7 +97,7 @@ module Immutable =

module SideEffects =
[<Fact>]
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 {
Expand All @@ -102,7 +113,7 @@ module SideEffects =
}

[<Fact>]
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 {
Expand All @@ -120,7 +131,7 @@ module SideEffects =
}

[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
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
Expand All @@ -132,7 +143,7 @@ module SideEffects =
}

[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
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
Expand Down
182 changes: 182 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Tail.Tests.fs
Original file line number Diff line number Diff line change
@@ -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 =

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-tail throws`` variant = task {
fun () -> Gen.getEmptyVariant variant |> TaskSeq.tail |> Task.ignore
|> should throwAsyncExact typeof<ArgumentException>
}

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-tryTail returns None`` variant = task {
let! nothing = Gen.getEmptyVariant variant |> TaskSeq.tryTail
nothing |> should be None'
}

[<Fact>]
let ``TaskSeq-tail executes side effect`` () = task {
let mutable x = 0

fun () -> taskSeq { do x <- x + 1 } |> TaskSeq.tail |> Task.ignore
|> should throwAsyncExact typeof<ArgumentException>

// side effect must have run!
x |> should equal 1
}

[<Fact>]
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 |])

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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')

}

[<Fact>]
let ``TaskSeq-tail return empty from a singleton sequence`` () = task {
let ts = taskSeq { yield 42 }

let! tail = TaskSeq.tail ts
do! verifyEmpty tail
}

[<Fact>]
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 =
[<Fact>]
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
}

[<Fact>]
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

}

[<Fact>]
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
}

[<Fact>]
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
}
7 changes: 7 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
17 changes: 13 additions & 4 deletions src/FSharp.Control.TaskSeq/TaskSeq.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -232,21 +232,30 @@ module TaskSeq =
val collectSeqAsync: binder: ('T -> #Task<'SeqU>) -> source: taskSeq<'T> -> taskSeq<'U> when 'SeqU :> seq<'U>

/// <summary>
/// Returns the first element of the <see cref="taskSeq" />, or <see cref="None" /> if the sequence is empty.
/// Returns the first element of the task sequence from <paramref name="source" />, or <see cref="None" /> if the sequence is empty.
/// </summary>
/// <exception cref="ArgumentException">Thrown when the sequence is empty.</exception>
val tryHead: source: taskSeq<'T> -> Task<'T option>

/// <summary>
/// Returns the first element of the <see cref="taskSeq" />.
/// Returns the first elementof the task sequence from <paramref name="source" />
/// </summary>
/// <exception cref="ArgumentException">Thrown when the sequence is empty.</exception>
val head: source: taskSeq<'T> -> Task<'T>

/// <summary>
/// Returns the last element of the <see cref="taskSeq" />, or <see cref="None" /> if the sequence is empty.
/// Returns the whole task sequence from <paramref name="source" />, minus its first element, or <see cref="None" /> if the sequence is empty.
/// </summary>
val tryTail: source: taskSeq<'T> -> Task<taskSeq<'T> option>

/// <summary>
/// Returns the whole task sequence from <paramref name="source" />, minus its first element.
/// </summary>
/// <exception cref="ArgumentException">Thrown when the sequence is empty.</exception>
val tail: source: taskSeq<'T> -> Task<taskSeq<'T>>

/// <summary>
/// Returns the last element of the task sequence from <paramref name="source" />, or <see cref="None" /> if the sequence is empty.
/// </summary>
val tryLast: source: taskSeq<'T> -> Task<'T option>

/// <summary>
Expand Down
27 changes: 23 additions & 4 deletions src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 5 additions & 1 deletion src/FSharp.Control.TaskSeq/Utils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down