Skip to content

Add TaskSeq.singleton to the surface area #90

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

Merged
merged 8 commits into from
Nov 24, 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
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<Compile Include="TaskSeq.Map.Tests.fs" />
<Compile Include="TaskSeq.OfXXX.Tests.fs" />
<Compile Include="TaskSeq.Pick.Tests.fs" />
<Compile Include="TaskSeq.Singleton.Tests.fs" />
<Compile Include="TaskSeq.Tail.Tests.fs" />
<Compile Include="TaskSeq.ToXXX.Tests.fs" />
<Compile Include="TaskSeq.Zip.Tests.fs" />
Expand Down
96 changes: 96 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Singleton.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
module TaskSeq.Tests.Singleton

open System.Threading.Tasks
open Xunit
open FsUnit.Xunit
open FsToolkit.ErrorHandling

open FSharp.Control

module EmptySeq =

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-singleton with empty has length one`` variant =
taskSeq {
yield! TaskSeq.singleton 10
yield! Gen.getEmptyVariant variant
}
|> TaskSeq.exactlyOne
|> Task.map (should equal 10)

module Other =
[<Fact>]
let ``TaskSeq-singleton creates a sequence of one`` () =
TaskSeq.singleton 42
|> TaskSeq.exactlyOne
|> Task.map (should equal 42)

[<Fact>]
let ``TaskSeq-singleton can be yielded multiple times`` () =
let singleton = TaskSeq.singleton 42

taskSeq {
yield! singleton
yield! singleton
yield! singleton
yield! singleton
}
|> TaskSeq.toList
|> should equal [ 42; 42; 42; 42 ]

[<Fact>]
let ``TaskSeq-singleton with isEmpty`` () =
TaskSeq.singleton 42
|> TaskSeq.isEmpty
|> Task.map (should be False)

[<Fact>]
let ``TaskSeq-singleton with append`` () =
TaskSeq.singleton 42
|> TaskSeq.append (TaskSeq.singleton 42)
|> TaskSeq.toList
|> should equal [ 42; 42 ]

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-singleton with collect`` variant =
Gen.getSeqImmutable variant
|> TaskSeq.collect TaskSeq.singleton
|> verify1To10

[<Fact>]
let ``TaskSeq-singleton does not throw when getting Current before MoveNext`` () = task {
let enumerator = (TaskSeq.singleton 42).GetAsyncEnumerator()
let defaultValue = enumerator.Current // should return the default value for int
defaultValue |> should equal 0
}

[<Fact>]
let ``TaskSeq-singleton does not throw when getting Current after last MoveNext`` () = task {
let enumerator = (TaskSeq.singleton 42).GetAsyncEnumerator()
let! isNext = enumerator.MoveNextAsync()
isNext |> should be True
let value = enumerator.Current // the first and only value
value |> should equal 42

// move past the end
let! isNext = enumerator.MoveNextAsync()
isNext |> should be False
let defaultValue = enumerator.Current // should return the default value for int
defaultValue |> should equal 0
}

[<Fact>]
let ``TaskSeq-singleton multiple MoveNext is fine`` () = task {
let enumerator = (TaskSeq.singleton 42).GetAsyncEnumerator()
let! isNext = enumerator.MoveNextAsync()
isNext |> should be True
let! _ = enumerator.MoveNextAsync()
let! _ = enumerator.MoveNextAsync()
let! _ = enumerator.MoveNextAsync()
let! isNext = enumerator.MoveNextAsync()
isNext |> should be False

// should return the default value for int after moving past the end
let defaultValue = enumerator.Current
defaultValue |> should equal 0
}
5 changes: 5 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TestUtils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ module TestUtils =
|> TaskSeq.toArrayAsync
|> Task.map (Array.isEmpty >> should be True)

let verify1To10 ts =
ts
|> TaskSeq.toArrayAsync
|> Task.map (should equal [| 1..10 |])

/// Delays (no spin-wait!) between 20 and 70ms, assuming a 15.6ms resolution clock
let longDelay () = task { do! Task.Delay(Random().Next(20, 70)) }

Expand Down
5 changes: 3 additions & 2 deletions src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ Generates optimized IL code through the new resumable state machines, and comes
<PackageReadmeFile>nuget-package-readme.md</PackageReadmeFile>
<PackageReleaseNotes>
Release notes:
0.2.3
- improve TaskSeq.empty by not relying on resumable state, #89
0.2.3 (unreleased)
- add TaskSeq.singleton, #90 (by @gusty)
- improve TaskSeq.empty by not relying on resumable state, #89 (by @gusty)
- do not throw exception for unequal lengths in TaskSeq.zip, fixes #32
0.2.2
- removes TaskSeq.toSeqCachedAsync, which was incorrectly named. Use toSeq or toListAsync instead.
Expand Down
6 changes: 2 additions & 4 deletions src/FSharp.Control.TaskSeq/TaskSeq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ module TaskSeq =
}
}

let singleton (source: 'T) = Internal.singleton source

let isEmpty source = Internal.isEmpty source

//
Expand All @@ -37,10 +39,6 @@ module TaskSeq =
e.DisposeAsync().AsTask().Wait()
]

let format x = string x
let f () = format 42


let toArray (source: taskSeq<'T>) = [|
let e = source.GetAsyncEnumerator(CancellationToken())

Expand Down
5 changes: 5 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeq.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ module TaskSeq =
/// Initialize an empty taskSeq.
val empty<'T> : taskSeq<'T>

/// <summary>
/// Creates a <see cref="taskSeq" /> sequence from <paramref name="source" /> that generates a single element and then ends.
/// </summary>
val singleton: source: 'T -> taskSeq<'T>

/// <summary>
/// Returns <see cref="true" /> if the task sequence contains no elements, <see cref="false" /> otherwise.
/// </summary>
Expand Down
31 changes: 31 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ module ExtraTaskSeqOperators =
/// A TaskSeq workflow for IAsyncEnumerable<'T> types.
let taskSeq = TaskSeqBuilder()

[<Struct>]
type AsyncEnumStatus =
| BeforeAll
| WithCurrent
| AfterAll

[<Struct>]
type Action<'T, 'U, 'TaskU when 'TaskU :> Task<'U>> =
| CountableAction of countable_action: (int -> 'T -> 'U)
Expand Down Expand Up @@ -61,6 +67,31 @@ module internal TaskSeqInternal =
return not step
}

let singleton (source: 'T) =
{ new IAsyncEnumerable<'T> with
member _.GetAsyncEnumerator(_) =
let mutable status = BeforeAll

{ new IAsyncEnumerator<'T> with
member _.MoveNextAsync() =
match status with
| BeforeAll ->
status <- WithCurrent
ValueTask.True
| WithCurrent ->
status <- AfterAll
ValueTask.False
| AfterAll -> ValueTask.False

member _.Current: 'T =
match status with
| WithCurrent -> source
| _ -> Unchecked.defaultof<'T>

member _.DisposeAsync() = ValueTask.CompletedTask
}
}

/// Returns length unconditionally, or based on a predicate
let lengthBy predicate (source: taskSeq<_>) = task {
use e = source.GetAsyncEnumerator(CancellationToken())
Expand Down