diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index f18f9bfb..5c845725 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -1,4 +1,4 @@
-name: ci
+name: ci-build
on: [pull_request]
@@ -18,55 +18,7 @@ jobs:
# build it, test it, pack it
- name: Run dotnet build
run: ./build.cmd
-
- test-release:
- name: Test Release Build
- runs-on: windows-latest
- steps:
- # checkout the code
- - name: checkout-code
- uses: actions/checkout@v3
- with:
- fetch-depth: 0
- # setup dotnet based on global.json
- - name: setup-dotnet
- uses: actions/setup-dotnet@v3
- # build it, test it, pack it
- - name: Run dotnet test - release
- run: dotnet test -c Release --logger "trx;LogFileName=test-results-release.trx" .\src\FSharpy.TaskSeq.Test\FSharpy.TaskSeq.Test.fsproj
- - name: Publish test results - release
- uses: dorny/test-reporter@v1
- if: always()
- with:
- name: Report release tests
- # this path glob pattern requires forward slashes!
- path: ./src/FSharpy.TaskSeq.Test/TestResults/test-results-release.trx
- reporter: dotnet-trx
-
- test-debug:
- name: Test Debug Build
- runs-on: windows-latest
- steps:
- # checkout the code
- - name: checkout-code
- uses: actions/checkout@v3
- with:
- fetch-depth: 0
- # setup dotnet based on global.json
- - name: setup-dotnet
- uses: actions/setup-dotnet@v3
- # build it, test it, pack it
- - name: Run dotnet test - debug
- run: dotnet test -c Debug --logger "trx;LogFileName=test-results-debug.trx" .\src\FSharpy.TaskSeq.Test\FSharpy.TaskSeq.Test.fsproj
- - name: Publish test results - debug
- uses: dorny/test-reporter@v1
- if: always()
- with:
- name: Report debug tests
- # this path glob pattern requires forward slashes!
- path: ./src/FSharpy.TaskSeq.Test/TestResults/test-results-debug.trx
- reporter: dotnet-trx
-
+
# deploy:
# name: deploy
# runs-on: ubuntu-latest
diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index 3186261d..7d06a2c3 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -36,7 +36,7 @@ jobs:
uses: actions/setup-dotnet@v3
# build it, test it, pack it
- name: Run dotnet test - release
- run: dotnet test -c Release --logger "trx;LogFileName=test-results-release.trx" .\src\FSharpy.TaskSeq.Test\FSharpy.TaskSeq.Test.fsproj
+ run: dotnet test -c Release --blame-hang-timeout 15000ms --logger "trx;LogFileName=test-results-release.trx" --logger "console;verbosity=detailed" .\src\FSharpy.TaskSeq.Test\FSharpy.TaskSeq.Test.fsproj
- name: Publish test results - release
uses: dorny/test-reporter@v1
if: always()
@@ -60,7 +60,7 @@ jobs:
uses: actions/setup-dotnet@v3
# build it, test it, pack it
- name: Run dotnet test - debug
- run: dotnet test -c Debug --logger "trx;LogFileName=test-results-debug.trx" .\src\FSharpy.TaskSeq.Test\FSharpy.TaskSeq.Test.fsproj
+ run: dotnet test -c Debug --blame-hang-timeout 15000ms --logger "trx;LogFileName=test-results-debug.trx" --logger "console;verbosity=detailed" .\src\FSharpy.TaskSeq.Test\FSharpy.TaskSeq.Test.fsproj
- name: Publish test results - debug
uses: dorny/test-reporter@v1
if: always()
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 00000000..f4e2ee73
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,53 @@
+name: ci-test
+
+on: [pull_request]
+
+jobs:
+ test-release:
+ name: Test Release Build
+ runs-on: windows-latest
+ steps:
+ # checkout the code
+ - name: checkout-code
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ # setup dotnet based on global.json
+ - name: setup-dotnet
+ uses: actions/setup-dotnet@v3
+ # build it, test it, pack it
+ - name: Run dotnet test - release
+ run: dotnet test -c Release --blame-hang-timeout 15000ms --logger "trx;LogFileName=test-results-release.trx" --logger "console;verbosity=detailed" .\src\FSharpy.TaskSeq.Test\FSharpy.TaskSeq.Test.fsproj
+ - name: Publish test results - release
+ uses: dorny/test-reporter@v1
+ if: always()
+ with:
+ name: Report release tests
+ # this path glob pattern requires forward slashes!
+ path: ./src/FSharpy.TaskSeq.Test/TestResults/test-results-release.trx
+ reporter: dotnet-trx
+
+ test-debug:
+ name: Test Debug Build
+ runs-on: windows-latest
+ steps:
+ # checkout the code
+ - name: checkout-code
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ # setup dotnet based on global.json
+ - name: setup-dotnet
+ uses: actions/setup-dotnet@v3
+ # build it, test it, pack it
+ - name: Run dotnet test - debug
+ run: dotnet test -c Debug --blame-hang-timeout 15000ms --logger "trx;LogFileName=test-results-debug.trx" --logger "console;verbosity=detailed" .\src\FSharpy.TaskSeq.Test\FSharpy.TaskSeq.Test.fsproj
+ - name: Publish test results - debug
+ uses: dorny/test-reporter@v1
+ if: always()
+ with:
+ name: Report debug tests
+ # this path glob pattern requires forward slashes!
+ path: ./src/FSharpy.TaskSeq.Test/TestResults/test-results-debug.trx
+ reporter: dotnet-trx
+
\ No newline at end of file
diff --git a/README.md b/README.md
index dacd55cb..024b8683 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,6 @@
+[](https://github.com/abelbraaksma/TaskSeq/actions/workflows/main.yaml)
+[](https://github.com/abelbraaksma/TaskSeq/actions/workflows/test.yaml)
+
# TaskSeq
An implementation [`IAsyncEnumerable<'T>`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iasyncenumerable-1?view=net-7.0) as a `taskSeq` CE for F# with accompanying `TaskSeq` module.
@@ -24,8 +27,8 @@ Not necessarily in order of importance:
## Current set of `TaskSeq` utility functions
-The following is the current surface area of the `TaskSeq` utility functions. This is just a dump of the signatures, proper
-documentation will be added soon(ish):
+The following is the current surface area of the `TaskSeq` utility functions. This is just a dump of the signatures with doc comments
+to be used as a quick ref.
```f#
module TaskSeq =
@@ -36,6 +39,11 @@ module TaskSeq =
/// Initialize an empty taskSeq.
val empty<'T> : taskSeq<'T>
+ ///
+ /// Returns if the task sequence contains no elements, otherwise.
+ ///
+ val isEmpty: taskSeq: taskSeq<'T> -> Task
+
/// Returns taskSeq as an array. This function is blocking until the sequence is exhausted and will properly dispose of the resources.
val toList: t: taskSeq<'T> -> 'T list
@@ -131,13 +139,171 @@ module TaskSeq =
/// Applies the given async function to the items in the taskSeq and concatenates all the results in order.
val collectSeqAsync: binder: ('T -> #Task<'SeqU>) -> taskSeq: taskSeq<'T> -> taskSeq<'U> when 'SeqU :> seq<'U>
+ ///
+ /// Returns the first element of the , or if the sequence is empty.
+ ///
+ /// Thrown when the sequence is empty.
+ val tryHead: taskSeq: taskSeq<'T> -> Task<'T option>
+
+ ///
+ /// Returns the first element of the .
+ ///
+ /// Thrown when the sequence is empty.
+ val head: taskSeq: taskSeq<'T> -> Task<'T>
+
+ ///
+ /// Returns the last element of the , or if the sequence is empty.
+ ///
+ /// Thrown when the sequence is empty.
+ val tryLast: taskSeq: taskSeq<'T> -> Task<'T option>
+
+ ///
+ /// Returns the last element of the .
+ ///
+ /// Thrown when the sequence is empty.
+ val last: taskSeq: taskSeq<'T> -> Task<'T>
+
+ ///
+ /// Returns the nth element of the , or if the sequence
+ /// does not contain enough elements, or if is negative.
+ /// Parameter is zero-based, that is, the value 0 returns the first element.
+ ///
+ val tryItem: index: int -> taskSeq: taskSeq<'T> -> Task<'T option>
+
+ ///
+ /// Returns the nth element of the , or if the sequence
+ /// does not contain enough elements, or if is negative.
+ ///
+ /// Thrown when the sequence has insufficient length or
+ /// is negative.
+ val item: index: int -> taskSeq: taskSeq<'T> -> Task<'T>
+
+ ///
+ /// Returns the only element of the task sequence, or if the sequence is empty of
+ /// contains more than one element.
+ ///
+ val tryExactlyOne: source: taskSeq<'T> -> Task<'T option>
+
+ ///
+ /// Returns the only element of the task sequence.
+ ///
+ /// Thrown when the input sequence does not contain precisely one element.
+ val exactlyOne: source: taskSeq<'T> -> Task<'T>
+
+ ///
+ /// Applies the given function to each element of the task sequence. Returns
+ /// a sequence comprised of the results "x" for each element where
+ /// the function returns Some(x).
+ /// If is asynchronous, consider using .
+ ///
+ val choose: chooser: ('T -> 'U option) -> source: taskSeq<'T> -> taskSeq<'U>
+
+ ///
+ /// Applies the given asynchronous function to each element of the task sequence. Returns
+ /// a sequence comprised of the results "x" for each element where
+ /// the function returns .
+ /// If does not need to be asynchronous, consider using .
+ ///
+ val chooseAsync: chooser: ('T -> #Task<'U option>) -> source: taskSeq<'T> -> taskSeq<'U>
+
+ ///
+ /// Returns a new collection containing only the elements of the collection
+ /// for which the given function returns .
+ /// If is asynchronous, consider using .
+ ///
+ val filter: predicate: ('T -> bool) -> source: taskSeq<'T> -> taskSeq<'T>
+
+ ///
+ /// Returns a new collection containing only the elements of the collection
+ /// for which the given asynchronous function returns .
+ /// If does not need to be asynchronous, consider using .
+ ///
+ val filterAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> taskSeq<'T>
+
+ ///
+ /// Applies the given function to successive elements of the task sequence
+ /// in , returning the first result where the function returns .
+ /// If is asynchronous, consider using .
+ ///
+ val tryPick: chooser: ('T -> 'U option) -> source: taskSeq<'T> -> Task<'U option>
+
+ ///
+ /// Applies the given asynchronous function to successive elements of the task sequence
+ /// in , returning the first result where the function returns .
+ /// If does not need to be asynchronous, consider using .
+ ///
+ val tryPickAsync: chooser: ('T -> #Task<'U option>) -> source: taskSeq<'T> -> Task<'U option>
+
+ ///
+ /// Returns the first element of the task sequence in for which the given function
+ /// returns . Returns if no such element exists.
+ /// If is asynchronous, consider using .
+ ///
+ val tryFind: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task<'T option>
+
+ ///
+ /// Returns the first element of the task sequence in for which the given asynchronous function
+ /// returns . Returns if no such element exists.
+ /// If does not need to be asynchronous, consider using .
+ ///
+ val tryFindAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task<'T option>
+
+
+ ///
+ /// Applies the given function to successive elements of the task sequence
+ /// in , returning the first result where the function returns .
+ /// If is asynchronous, consider using .
+ /// Thrown when every item of the sequence
+ /// evaluates to when the given function is applied.
+ ///
+ val pick: chooser: ('T -> 'U option) -> source: taskSeq<'T> -> Task<'U>
+
+ ///
+ /// Applies the given asynchronous function to successive elements of the task sequence
+ /// in , returning the first result where the function returns .
+ /// If does not need to be asynchronous, consider using .
+ /// Thrown when every item of the sequence
+ /// evaluates to when the given function is applied.
+ ///
+ val pickAsync: chooser: ('T -> #Task<'U option>) -> source: taskSeq<'T> -> Task<'U>
+
+ ///
+ /// Returns the first element of the task sequence in for which the given function
+ /// returns .
+ /// If is asynchronous, consider using .
+ ///
+ /// Thrown if no element returns when
+ /// evaluated by the function.
+ val find: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task<'T>
+
+ ///
+ /// Returns the first element of the task sequence in for which the given
+ /// asynchronous function returns .
+ /// If does not need to be asynchronous, consider using .
+ ///
+ /// Thrown if no element returns when
+ /// evaluated by the function.
+ val findAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task<'T>
+
+ ///
/// Zips two task sequences, returning a taskSeq of the tuples of each sequence, in order. May raise ArgumentException
/// if the sequences are or unequal length.
- val zip: taskSeq1: taskSeq<'T> -> taskSeq2: taskSeq<'U> -> taskSeq<'T * 'U>
-
- /// Applies a function to each element of the task sequence, threading an accumulator argument through the computation.
+ ///
+ /// The sequences have different lengths.
+ val zip: taskSeq1: taskSeq<'T> -> taskSeq2: taskSeq<'U> -> IAsyncEnumerable<'T * 'U>
+
+ ///
+ /// Applies the function to each element in the task sequence,
+ /// threading an accumulator argument of type through the computation.
+ /// If the accumulator function is asynchronous, consider using .
+ ///
val fold: folder: ('State -> 'T -> 'State) -> state: 'State -> taskSeq: taskSeq<'T> -> Task<'State>
- /// Applies an async function to each element of the task sequence, threading an accumulator argument through the computation.
+ ///
+ /// Applies the asynchronous function to each element in the task sequence,
+ /// threading an accumulator argument of type through the computation.
+ /// If the accumulator function does not need to be asynchronous, consider using .
+ ///
val foldAsync: folder: ('State -> 'T -> #Task<'State>) -> state: 'State -> taskSeq: taskSeq<'T> -> Task<'State>
+
```
diff --git a/src/FSharpy.TaskSeq.Test/AssemblyInfo.fs b/src/FSharpy.TaskSeq.Test/AssemblyInfo.fs
new file mode 100644
index 00000000..4f019eaa
--- /dev/null
+++ b/src/FSharpy.TaskSeq.Test/AssemblyInfo.fs
@@ -0,0 +1,8 @@
+namespace FSharpy.Tests
+
+open System.Runtime.CompilerServices
+
+[]
+[]
+
+do ()
diff --git a/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj b/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj
index d99de137..1c09dd60 100644
--- a/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj
+++ b/src/FSharpy.TaskSeq.Test/FSharpy.TaskSeq.Test.fsproj
@@ -1,4 +1,4 @@
-
+
net6.0
@@ -8,15 +8,24 @@
+
+
-
-
-
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/src/FSharpy.TaskSeq.Test/Nunit.Extensions.fs b/src/FSharpy.TaskSeq.Test/Nunit.Extensions.fs
new file mode 100644
index 00000000..71cff4f2
--- /dev/null
+++ b/src/FSharpy.TaskSeq.Test/Nunit.Extensions.fs
@@ -0,0 +1,180 @@
+namespace FSharpy.Tests
+
+open System
+open System.Threading.Tasks
+open FsUnit
+open NHamcrest.Core
+open Microsoft.FSharp.Reflection
+
+open FsToolkit.ErrorHandling
+open Xunit
+open Xunit.Sdk
+
+type AlphabeticalOrderer() =
+ interface ITestCaseOrderer with
+ override this.OrderTestCases(testCases) =
+ testCases
+ |> Seq.sortBy (fun testCase ->
+ // sorting (or getting) the type fails
+ // and as soon as this method fails, no tests are discovered
+ testCase.TestMethod.Method.Name)
+
+
+[]
+module ExtraCustomMatchers =
+ /// Tee operator, combine multiple FsUnit-style test assertions:
+ /// x |>> should be (greaterThan 12) |> should be (lessThan 42)
+ let (|>>) x sideEffect =
+ sideEffect x |> ignore
+ x
+
+ let private baseResultTypeTest value =
+ match value with
+ | null ->
+ MatchException("Result type", "", "Value or None is never Result.Ok or Result.Error")
+ |> raise
+
+ | _ ->
+ let ty = value.GetType()
+
+ if ty.FullName.StartsWith "Microsoft.FSharp.Core.FSharpResult" then
+ FSharpValue.GetUnionFields(value, ty) |> fst
+ else
+ MatchException("Result type", ty.Name, "Type must be Result<_, _>")
+ |> raise
+
+ let private baseOptionTypeTest value =
+ match value with
+ | null ->
+ // An option type interpreted as obj will be for None
+ None
+
+ | _ ->
+ let ty = value.GetType()
+
+ if ty.FullName.StartsWith "Microsoft.FSharp.Core.FSharpOption" then
+ match (FSharpValue.GetUnionFields(value, ty) |> fst).Name with
+ | "Some" -> Some()
+ | "None" -> None
+ | _ ->
+ raise
+ <| MatchException("Option type", ty.Name, "Unexpected field name for F# option type")
+ else
+ MatchException("Option type", ty.Name, "Type must be Option<_>")
+ |> raise
+
+
+ /// Type must be Result, value must be Result.Ok. Use with `not`` only succeeds if using the correct type.
+ let Ok' =
+ let check value =
+ let field = baseResultTypeTest value
+
+ match field.Name with
+ | "Ok" -> true
+ | _ -> false
+
+ CustomMatcher("Result.Ok", check)
+
+ /// Type must be Result, value must be Result.Error. Use with `not`` only succeeds if using the correct type.
+ let Error' =
+ let check value =
+ let field = baseResultTypeTest value
+
+ match field.Name with
+ | "Error" -> true
+ | _ -> false
+
+ CustomMatcher("Result.Error", check)
+
+ /// Succeeds for None or
+ let None' =
+ let check value =
+ baseOptionTypeTest value
+ |> Option.map (fun _ -> false)
+ |> Option.defaultValue true
+
+ CustomMatcher("Option.None", check)
+
+ /// Succeeds for any value Some. Use with `not`` only succeeds if using the correct type.
+ let Some' =
+ let check value =
+ baseOptionTypeTest (unbox value)
+ |> Option.map (fun _ -> true)
+ |> Option.defaultValue false
+
+ CustomMatcher("Option.Some", check)
+
+
+ /// Succeeds if item-under-test contains any of the items in the sequence
+ let anyOf (lst: 'T seq) =
+ CustomMatcher($"anyOf: %A{lst}", (fun item -> lst |> Seq.contains (item :?> 'T)))
+
+ ///
+ /// Asserts any exception that matches, or is derived from the given exception .
+ /// Async exceptions are almost always nested in an , however, in an
+ /// async try/catch in F#, the exception is typically unwrapped. But this is not foolproof, and
+ /// in cases where we just call , and will be raised regardless.
+ /// This assertion will go over all nested exceptions and 'self', to find a matching exception.
+ /// Function to evaluate MUST return a , not a generic
+ /// .
+ /// Calls of xUnit to ensure proper evaluation of async.
+ ///
+ let throwAsync (ex: Type) =
+ let testForThrowing (fn: unit -> Task) = task {
+ let! actualExn = Assert.ThrowsAnyAsync fn
+
+ match actualExn with
+ | :? AggregateException as aggregateEx ->
+ if Object.ReferenceEquals(ex, typeof) then
+ // in case the assertion is for AggregateException itself, just accept it as Passed.
+ return true
+
+ else
+ for ty in aggregateEx.InnerExceptions do
+ Assert.IsAssignableFrom(expectedType = ex, object = ty)
+
+ //Assert.Contains(expected = ex, collection = types)
+ return true // keep FsUnit happy
+ | _ ->
+ // checks if object is of a certain type
+ Assert.IsAssignableFrom(ex, actualExn)
+ return true //keep FsUnit happy
+ }
+
+ CustomMatcher(
+ $"Throws %s{ex.Name} (Below, XUnit does not show actual value properly)",
+ (fun fn -> (testForThrowing (fn :?> (unit -> Task))).Result)
+ )
+
+ ///
+ /// Asserts any exception that exactly matches the given exception .
+ /// Async exceptions are almost always nested in an , however, in an
+ /// async try/catch in F#, the exception is typically unwrapped. But this is not foolproof, and
+ /// in cases where we just call , and will be raised regardless.
+ /// This assertion will go over all nested exceptions and 'self', to find a matching exception.
+ /// Function to evaluate MUST return a , not a generic
+ /// .
+ /// Calls of xUnit to ensure proper evaluation of async.
+ ///
+ let throwAsyncExact (ex: Type) =
+ let testForThrowing (fn: unit -> Task) = task {
+ let! actualExn = Assert.ThrowsAnyAsync fn
+
+ match actualExn with
+ | :? AggregateException as aggregateEx ->
+ let types =
+ aggregateEx.InnerExceptions
+ |> Seq.map (fun x -> x.GetType())
+
+ Assert.Contains(expected = ex, collection = types)
+ return true // keep FsUnit happy
+ | _ ->
+ // checks if object is of a certain type
+ Assert.IsType(ex, actualExn)
+ return true //keep FsUnit happy
+ }
+
+ CustomMatcher(
+ $"Throws %s{ex.Name} (Below, XUnit does not show actual value properly)",
+ (fun fn -> (testForThrowing (fn :?> (unit -> Task))).Result)
+ )
diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Choose.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Choose.Tests.fs
new file mode 100644
index 00000000..c70bbd7e
--- /dev/null
+++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Choose.Tests.fs
@@ -0,0 +1,57 @@
+module FSharpy.Tests.Choose
+
+open System
+open System.Threading.Tasks
+
+open Xunit
+open FsUnit.Xunit
+open FsToolkit.ErrorHandling
+
+open FSharpy
+
+[]
+let ``ZHang timeout test`` () = task {
+ let! empty = Task.Delay 30
+
+ empty |> should be Null
+}
+
+[]
+let ``TaskSeq-choose on an empty sequence`` () = task {
+ let! empty =
+ TaskSeq.empty
+ |> TaskSeq.choose (fun _ -> Some 42)
+ |> TaskSeq.toListAsync
+
+ List.isEmpty empty |> should be True
+}
+
+[]
+let ``TaskSeq-chooseAsync on an empty sequence`` () = task {
+ let! empty =
+ TaskSeq.empty
+ |> TaskSeq.chooseAsync (fun _ -> task { return Some 42 })
+ |> TaskSeq.toListAsync
+
+ List.isEmpty empty |> should be True
+}
+
+[]
+let ``TaskSeq-choose can convert and filter`` () = task {
+ let! alphabet =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.choose (fun number -> if number <= 26 then Some(char number + '@') else None)
+ |> TaskSeq.toArrayAsync
+
+ String alphabet |> should equal "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+}
+
+[]
+let ``TaskSeq-chooseAsync can convert and filter`` () = task {
+ let! alphabet =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.choose (fun number -> if number <= 26 then Some(char number + '@') else None)
+ |> TaskSeq.toArrayAsync
+
+ String alphabet |> should equal "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+}
diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Filter.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Filter.Tests.fs
new file mode 100644
index 00000000..79c413cd
--- /dev/null
+++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Filter.Tests.fs
@@ -0,0 +1,53 @@
+module FSharpy.Tests.Filter
+
+open System
+open Xunit
+open FsUnit.Xunit
+open FsToolkit.ErrorHandling
+
+open FSharpy
+
+[]
+let ``TaskSeq-filter on an empty sequence`` () = task {
+ let! empty =
+ TaskSeq.empty
+ |> TaskSeq.filter ((=) 12)
+ |> TaskSeq.toListAsync
+
+ List.isEmpty empty |> should be True
+}
+
+[]
+let ``TaskSeq-filterAsync on an empty sequence`` () = task {
+ let! empty =
+ TaskSeq.empty
+ |> TaskSeq.filterAsync (fun x -> task { return x = 12 })
+ |> TaskSeq.toListAsync
+
+ List.isEmpty empty |> should be True
+}
+
+[]
+let ``TaskSeq-filter filters correctly`` () = task {
+ let! alphabet =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.filter ((<=) 26) // lambda of '>' etc inverts order of args, so this means 'greater than'
+ |> TaskSeq.map char
+ |> TaskSeq.map ((+) '@')
+ |> TaskSeq.toArrayAsync
+
+ // we filtered all digits above-or-equal-to 26
+ String alphabet |> should equal "Z[\]^_`abcdefghijklmnopqr"
+}
+
+[]
+let ``TaskSeq-filterAsync filters correctly`` () = task {
+ let! alphabet =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.filterAsync (fun x -> task { return x <= 26 })
+ |> TaskSeq.map char
+ |> TaskSeq.map ((+) '@')
+ |> TaskSeq.toArrayAsync
+
+ String alphabet |> should equal "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+}
diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Find.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Find.Tests.fs
new file mode 100644
index 00000000..dcf58b28
--- /dev/null
+++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Find.Tests.fs
@@ -0,0 +1,247 @@
+module FSharpy.Tests.Find
+
+open Xunit
+open FsUnit.Xunit
+open FsToolkit.ErrorHandling
+
+open FSharpy
+open System.Collections.Generic
+
+//
+// TaskSeq.find
+// TaskSeq.findAsync
+// the tryXXX versions are at the bottom half
+//
+
+[]
+let ``TaskSeq-find on an empty sequence raises KeyNotFoundException`` () = task {
+ fun () -> TaskSeq.empty |> TaskSeq.find ((=) 12) |> Task.ignore
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-find on an empty sequence raises KeyNotFoundException - variant`` () = task {
+ fun () -> taskSeq { do () } |> TaskSeq.find ((=) 12) |> Task.ignore
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-findAsync on an empty sequence raises KeyNotFoundException`` () = task {
+ fun () ->
+ TaskSeq.empty
+ |> TaskSeq.findAsync (fun x -> task { return x = 12 })
+ |> Task.ignore
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-find sad path raises KeyNotFoundException`` () = task {
+ fun () ->
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.find ((=) 0) // dummy tasks sequence starts at 1
+ |> Task.ignore
+
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-findAsync sad path raises KeyNotFoundException`` () = task {
+ fun () ->
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.findAsync (fun x -> task { return x = 0 }) // dummy tasks sequence starts at 1
+ |> Task.ignore
+
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-find sad path raises KeyNotFoundException variant`` () = task {
+ fun () ->
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.find ((=) 51) // dummy tasks sequence ends at 50
+ |> Task.ignore
+
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-findAsync sad path raises KeyNotFoundException variant`` () = task {
+ fun () ->
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.findAsync (fun x -> task { return x = 51 }) // dummy tasks sequence ends at 50
+ |> Task.ignore
+
+ |> should throwAsyncExact typeof
+}
+
+
+[]
+let ``TaskSeq-find happy path middle of seq`` () = task {
+ let! twentyFive =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.find (fun x -> x < 26 && x > 24)
+
+ twentyFive |> should equal 25
+}
+
+[]
+let ``TaskSeq-findAsync happy path middle of seq`` () = task {
+ let! twentyFive =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.findAsync (fun x -> task { return x < 26 && x > 24 })
+
+ twentyFive |> should equal 25
+}
+
+[]
+let ``TaskSeq-find happy path first item of seq`` () = task {
+ let! first =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.find ((=) 1) // dummy tasks seq starts at 1
+
+ first |> should equal 1
+}
+
+[]
+let ``TaskSeq-findAsync happy path first item of seq`` () = task {
+ let! first =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.findAsync (fun x -> task { return x = 1 }) // dummy tasks seq starts at 1
+
+ first |> should equal 1
+}
+
+[]
+let ``TaskSeq-find happy path last item of seq`` () = task {
+ let! last =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.find ((=) 50) // dummy tasks seq ends at 50
+
+ last |> should equal 50
+}
+
+[]
+let ``TaskSeq-findAsync happy path last item of seq`` () = task {
+ let! last =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.findAsync (fun x -> task { return x = 50 }) // dummy tasks seq ends at 50
+
+ last |> should equal 50
+}
+
+//
+// TaskSeq.tryFind
+// TaskSeq.tryFindAsync
+//
+
+[]
+let ``TaskSeq-tryFind on an empty sequence returns None`` () = task {
+ let! nothing = TaskSeq.empty |> TaskSeq.tryFind ((=) 12)
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-tryFindAsync on an empty sequence returns None`` () = task {
+ let! nothing =
+ TaskSeq.empty
+ |> TaskSeq.tryFindAsync (fun x -> task { return x = 12 })
+
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-tryFind sad path returns None`` () = task {
+ let! nothing =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryFind ((=) 0) // dummy tasks sequence starts at 1
+
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-tryFindAsync sad path return None`` () = task {
+ let! nothing =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryFindAsync (fun x -> task { return x = 0 }) // dummy tasks sequence starts at 1
+
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-tryFind sad path returns None variant`` () = task {
+ let! nothing =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryFind ((<=) 51) // dummy tasks sequence ends at 50 (inverted sign in lambda!)
+
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-tryFindAsync sad path return None - variant`` () = task {
+ let! nothing =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryFindAsync (fun x -> task { return x >= 51 }) // dummy tasks sequence ends at 50
+
+ nothing |> should be None'
+}
+
+
+[]
+let ``TaskSeq-tryFind happy path middle of seq`` () = task {
+ let! twentyFive =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryFind (fun x -> x < 26 && x > 24)
+
+ twentyFive |> should be Some'
+ twentyFive |> should equal (Some 25)
+}
+
+[]
+let ``TaskSeq-tryFindAsync happy path middle of seq`` () = task {
+ let! twentyFive =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryFindAsync (fun x -> task { return x < 26 && x > 24 })
+
+ twentyFive |> should be Some'
+ twentyFive |> should equal (Some 25)
+}
+
+[]
+let ``TaskSeq-tryFind happy path first item of seq`` () = task {
+ let! first =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryFind ((=) 1) // dummy tasks seq starts at 1
+
+ first |> should be Some'
+ first |> should equal (Some 1)
+}
+
+[]
+let ``TaskSeq-tryFindAsync happy path first item of seq`` () = task {
+ let! first =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryFindAsync (fun x -> task { return x = 1 }) // dummy tasks seq starts at 1
+
+ first |> should be Some'
+ first |> should equal (Some 1)
+}
+
+[]
+let ``TaskSeq-tryFind happy path last item of seq`` () = task {
+ let! last =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryFind ((=) 50) // dummy tasks seq ends at 50
+
+ last |> should be Some'
+ last |> should equal (Some 50)
+}
+
+[]
+let ``TaskSeq-tryFindAsync happy path last item of seq`` () = task {
+ let! last =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryFindAsync (fun x -> task { return x = 50 }) // dummy tasks seq ends at 50
+
+ last |> should be Some'
+ last |> should equal (Some 50)
+}
diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Head.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Head.Tests.fs
new file mode 100644
index 00000000..f11ad2ac
--- /dev/null
+++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Head.Tests.fs
@@ -0,0 +1,57 @@
+module FSharpy.Tests.Head
+
+open System
+open Xunit
+open FsUnit.Xunit
+open FsToolkit.ErrorHandling
+
+open FSharpy
+
+
+[]
+let ``TaskSeq-head throws on empty sequences`` () = task {
+ fun () -> TaskSeq.empty |> TaskSeq.head |> Task.ignore
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-head throws on empty sequences - variant`` () = task {
+ fun () -> taskSeq { do () } |> TaskSeq.head |> Task.ignore
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-tryHead returns None on empty sequences`` () = task {
+ let! nothing = TaskSeq.empty |> TaskSeq.tryHead
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-head gets the first item in a longer sequence`` () = task {
+ let! head = createDummyTaskSeqWith 50L<µs> 1000L<µs> 50 |> TaskSeq.head
+
+ head |> should equal 1
+}
+
+[]
+let ``TaskSeq-head gets the only item in a singleton sequence`` () = task {
+ let! head = taskSeq { yield 10 } |> TaskSeq.head
+ head |> should equal 10
+}
+
+[]
+let ``TaskSeq-tryHead gets the first item in a longer sequence`` () = task {
+ let! head =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryHead
+
+ head |> should be Some'
+ head |> should equal (Some 1)
+}
+
+[]
+let ``TaskSeq-tryHead gets the only item in a singleton sequence`` () = task {
+ let! head = taskSeq { yield 10 } |> TaskSeq.tryHead
+ head |> should be Some'
+ head |> should equal (Some 10)
+}
diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Item.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Item.Tests.fs
new file mode 100644
index 00000000..85c058fc
--- /dev/null
+++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Item.Tests.fs
@@ -0,0 +1,243 @@
+module FSharpy.Tests.Item
+
+open System
+open Xunit
+open FsUnit.Xunit
+open FsToolkit.ErrorHandling
+
+open FSharpy
+
+
+[]
+let ``TaskSeq-item throws on empty sequences`` () = task {
+ fun () -> TaskSeq.empty |> TaskSeq.item 0 |> Task.ignore
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-item throws on empty sequence - variant`` () = task {
+ fun () -> taskSeq { do () } |> TaskSeq.item 50000 |> Task.ignore
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-item throws when not found`` () = task {
+ fun () ->
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.item 51
+ |> Task.ignore
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-item throws when not found - variant`` () = task {
+ fun () ->
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.item Int32.MaxValue
+ |> Task.ignore
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-item throws when accessing 2nd item in singleton sequence`` () = task {
+ fun () -> taskSeq { yield 10 } |> TaskSeq.item 1 |> Task.ignore // zero-based!
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-item always throws with negative values`` () = task {
+ let make50 () = createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+
+ fun () -> make50 () |> TaskSeq.item -1 |> Task.ignore
+ |> should throwAsyncExact typeof
+
+ fun () -> make50 () |> TaskSeq.item -10000 |> Task.ignore
+ |> should throwAsyncExact typeof
+
+ fun () -> make50 () |> TaskSeq.item Int32.MinValue |> Task.ignore
+ |> should throwAsyncExact typeof
+
+ fun () -> TaskSeq.empty |> TaskSeq.item -1 |> Task.ignore
+ |> should throwAsyncExact typeof
+
+ fun () -> TaskSeq.empty |> TaskSeq.item -10000 |> Task.ignore
+ |> should throwAsyncExact typeof
+
+ fun () ->
+ TaskSeq.empty
+ |> TaskSeq.item Int32.MinValue
+ |> Task.ignore
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-tryItem returns None on empty sequences`` () = task {
+ let! nothing = TaskSeq.empty |> TaskSeq.tryItem 0
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-tryItem returns None on empty sequence - variant`` () = task {
+ let! nothing = taskSeq { do () } |> TaskSeq.tryItem 50000
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-tryItem returns None when not found`` () = task {
+ let! nothing =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryItem 50 // zero-based index, so a sequence of 50 items has its last item at index 49
+
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-tryItem returns None when not found - variant`` () = task {
+ let! nothing =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryItem Int32.MaxValue
+
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-tryItem returns None when accessing 2nd item in singleton sequence`` () = task {
+ let! nothing = taskSeq { yield 10 } |> TaskSeq.tryItem 1 // zero-based!
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-tryItem returns None throws with negative values`` () = task {
+ let make50 () = createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+
+ let! nothing = make50 () |> TaskSeq.tryItem -1
+ nothing |> should be None'
+
+ let! nothing = make50 () |> TaskSeq.tryItem -10000
+ nothing |> should be None'
+
+ let! nothing = make50 () |> TaskSeq.tryItem Int32.MinValue
+ nothing |> should be None'
+
+ let! nothing = TaskSeq.empty |> TaskSeq.tryItem -1
+ nothing |> should be None'
+
+ let! nothing = TaskSeq.empty |> TaskSeq.tryItem -10000
+ nothing |> should be None'
+
+ let! nothing = TaskSeq.empty |> TaskSeq.tryItem Int32.MinValue
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-item can get the first item in a longer sequence`` () = task {
+ let! head =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.item 0
+
+ head |> should equal 1
+}
+
+[]
+let ``TaskSeq-item can get the last item in a longer sequence`` () = task {
+ let! head =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.item 49
+
+ head |> should equal 50
+}
+
+[]
+let ``TaskSeq-item can get the first item in a singleton sequence`` () = task {
+ let! head = taskSeq { yield 10 } |> TaskSeq.item 0 // zero-based index!
+ head |> should equal 10
+}
+
+[]
+let ``TaskSeq-tryItem can get the first item in a longer sequence`` () = task {
+ let! head =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryItem 0 // zero-based!
+
+ head |> should be Some'
+ head |> should equal (Some 1)
+}
+
+[]
+let ``TaskSeq-tryItem in a very long sequence (5_000 items - slow variant)`` () = task {
+ let! head = createDummyDirectTaskSeq 5_001 |> TaskSeq.tryItem 5_000 // zero-based!
+
+ head |> should be Some'
+ head |> should equal (Some 5_001)
+}
+
+[]
+let ``TaskSeq-tryItem in a very long sequence (50_000 items - slow variant)`` () = task {
+ let! head = createDummyDirectTaskSeq 50_001 |> TaskSeq.tryItem 50_000 // zero-based!
+
+ head |> should be Some'
+ head |> should equal (Some 50_001)
+}
+
+[]
+let ``TaskSeq-tryItem in a very long sequence (50_000 items - fast variant)`` () = task {
+ let! head =
+ // using taskSeq instead of the delayed-task approach above, which creates an extra closure for each
+ // task, we can really see the speed of the 'taskSeq' CE!! This is
+ taskSeq {
+ for i in [ 0..50_000 ] do
+ yield i
+ }
+ |> TaskSeq.tryItem 50_000 // zero-based!
+
+ head |> should be Some'
+ head |> should equal (Some 50_000)
+}
+
+[]
+let ``TaskSeq-tryItem in a very long sequence (50_000 items - using sync Seq)`` () = task {
+ // this test is just for smoke-test perf comparison with TaskSeq above
+ let head =
+ seq {
+ for i in [ 0..50_000 ] do
+ yield i
+ }
+ |> Seq.tryItem 50_000 // zero-based!
+
+ head |> should be Some'
+ head |> should equal (Some 50_000)
+}
+
+[]
+let ``TaskSeq-tryItem in a very long sequence (500_000 items - fast variant)`` () = task {
+ let! head =
+ taskSeq {
+ for i in [ 0..500_000 ] do
+ yield i
+ }
+ |> TaskSeq.tryItem 500_000 // zero-based!
+
+ head |> should be Some'
+ head |> should equal (Some 500_000)
+}
+
+[]
+let ``TaskSeq-tryItem in a very long sequence (500_000 items - using sync Seq)`` () = task {
+ // this test is just for smoke-test perf comparison with TaskSeq above
+ let head =
+ seq {
+ for i in [ 0..500_000 ] do
+ yield i
+ }
+ |> Seq.tryItem 500_000 // zero-based!
+
+ head |> should be Some'
+ head |> should equal (Some 500_000)
+}
+
+[]
+let ``TaskSeq-tryItem gets the first item in a singleton sequence`` () = task {
+ let! head = taskSeq { yield 10 } |> TaskSeq.tryItem 0 // zero-based!
+ head |> should be Some'
+ head |> should equal (Some 10)
+}
diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Last.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Last.Tests.fs
new file mode 100644
index 00000000..8453457e
--- /dev/null
+++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Last.Tests.fs
@@ -0,0 +1,57 @@
+module FSharpy.Tests.Last
+
+open System
+open Xunit
+open FsUnit.Xunit
+open FsToolkit.ErrorHandling
+
+open FSharpy
+
+
+[]
+let ``TaskSeq-last throws on empty sequences`` () = task {
+ fun () -> TaskSeq.empty |> TaskSeq.last |> Task.ignore
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-last throws on empty sequences - variant`` () = task {
+ fun () -> taskSeq { do () } |> TaskSeq.last |> Task.ignore
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-tryLast returns None on empty sequences`` () = task {
+ let! nothing = TaskSeq.empty |> TaskSeq.tryLast
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-last gets the last item in a longer sequence`` () = task {
+ let! last = createDummyTaskSeqWith 50L<µs> 1000L<µs> 50 |> TaskSeq.last
+
+ last |> should equal 50
+}
+
+[]
+let ``TaskSeq-last gets the only item in a singleton sequence`` () = task {
+ let! last = taskSeq { yield 10 } |> TaskSeq.last
+ last |> should equal 10
+}
+
+[]
+let ``TaskSeq-tryLast gets the last item in a longer sequence`` () = task {
+ let! last =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryLast
+
+ last |> should be Some'
+ last |> should equal (Some 50)
+}
+
+[]
+let ``TaskSeq-tryLast gets the only item in a singleton sequence`` () = task {
+ let! last = taskSeq { yield 10 } |> TaskSeq.tryLast
+ last |> should be Some'
+ last |> should equal (Some 10)
+}
diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Pick.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Pick.Tests.fs
new file mode 100644
index 00000000..6e8c63de
--- /dev/null
+++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Pick.Tests.fs
@@ -0,0 +1,258 @@
+module FSharpy.Tests.Pick
+
+open System
+open System.Collections.Generic
+open Xunit
+open FsUnit.Xunit
+open FsToolkit.ErrorHandling
+
+open FSharpy
+
+
+//
+// TaskSeq.pick
+// TaskSeq.pickAsync
+// the tryXXX versions are at the bottom half
+//
+
+[]
+let ``TaskSeq-pick on an empty sequence raises KeyNotFoundException`` () = task {
+ fun () ->
+ TaskSeq.empty
+ |> TaskSeq.pick (fun x -> if x = 12 then Some x else None)
+ |> Task.ignore
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-pick on an empty sequence raises KeyNotFoundException - variant`` () = task {
+ fun () ->
+ taskSeq { do () }
+ |> TaskSeq.pick (fun x -> if x = 12 then Some x else None)
+ |> Task.ignore
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-pickAsync on an empty sequence raises KeyNotFoundException`` () = task {
+ fun () ->
+ TaskSeq.empty
+ |> TaskSeq.pickAsync (fun x -> task { return if x = 12 then Some x else None })
+ |> Task.ignore
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-pick sad path raises KeyNotFoundException`` () = task {
+ fun () ->
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.pick (fun x -> if x = 0 then Some x else None) // dummy tasks sequence starts at 1
+ |> Task.ignore
+
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-pickAsync sad path raises KeyNotFoundException`` () = task {
+ fun () ->
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.pickAsync (fun x -> task { return if x < 0 then Some x else None }) // dummy tasks sequence starts at 1
+ |> Task.ignore
+
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-pick sad path raises KeyNotFoundException variant`` () = task {
+ fun () ->
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.pick (fun x -> if x = 51 then Some x else None) // dummy tasks sequence ends at 50
+ |> Task.ignore
+
+ |> should throwAsyncExact typeof
+}
+
+[]
+let ``TaskSeq-pickAsync sad path raises KeyNotFoundException variant`` () = task {
+ fun () ->
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.pickAsync (fun x -> task { return if x = 51 then Some x else None }) // dummy tasks sequence ends at 50
+ |> Task.ignore
+
+ |> should throwAsyncExact typeof
+}
+
+
+[]
+let ``TaskSeq-pick happy path middle of seq`` () = task {
+ let! twentyFive =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.pick (fun x -> if x < 26 && x > 24 then Some "foo" else None)
+
+ twentyFive |> should equal "foo"
+}
+
+[]
+let ``TaskSeq-pickAsync happy path middle of seq`` () = task {
+ let! twentyFive =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.pickAsync (fun x -> task { return if x < 26 && x > 24 then Some "foo" else None })
+
+ twentyFive |> should equal "foo"
+}
+
+[]
+let ``TaskSeq-pick happy path first item of seq`` () = task {
+ let! first =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.pick (fun x -> if x = 1 then Some $"first{x}" else None) // dummy tasks seq starts at 1
+
+ first |> should equal "first1"
+}
+
+[]
+let ``TaskSeq-pickAsync happy path first item of seq`` () = task {
+ let! first =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.pickAsync (fun x -> task { return if x = 1 then Some $"first{x}" else None }) // dummy tasks seq starts at 1
+
+ first |> should equal "first1"
+}
+
+[]
+let ``TaskSeq-pick happy path last item of seq`` () = task {
+ let! last =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.pick (fun x -> if x = 50 then Some $"last{x}" else None) // dummy tasks seq ends at 50
+
+ last |> should equal "last50"
+}
+
+[]
+let ``TaskSeq-pickAsync happy path last item of seq`` () = task {
+ let! last =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.pickAsync (fun x -> task { return if x = 50 then Some $"last{x}" else None }) // dummy tasks seq ends at 50
+
+ last |> should equal "last50"
+}
+
+//
+// TaskSeq.tryPick
+// TaskSeq.tryPickAsync
+//
+
+[]
+let ``TaskSeq-tryPick on an empty sequence returns None`` () = task {
+ let! nothing =
+ TaskSeq.empty
+ |> TaskSeq.tryPick (fun x -> if x = 12 then Some x else None)
+
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-tryPickAsync on an empty sequence returns None`` () = task {
+ let! nothing =
+ TaskSeq.empty
+ |> TaskSeq.tryPickAsync (fun x -> task { return if x = 12 then Some x else None })
+
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-tryPick sad path returns None`` () = task {
+ let! nothing =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryPick (fun x -> if x = 0 then Some x else None) // dummy tasks sequence starts at 1
+
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-tryPickAsync sad path return None`` () = task {
+ let! nothing =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryPickAsync (fun x -> task { return if x = 0 then Some x else None }) // dummy tasks sequence starts at 1
+
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-tryPick sad path returns None variant`` () = task {
+ let! nothing =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryPick (fun x -> if x >= 51 then Some x else None) // dummy tasks sequence ends at 50 (inverted sign in lambda!)
+
+ nothing |> should be None'
+}
+
+[]
+let ``TaskSeq-tryPickAsync sad path return None - variant`` () = task {
+ let! nothing =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryPickAsync (fun x -> task { return if x >= 51 then Some x else None }) // dummy tasks sequence ends at 50
+
+ nothing |> should be None'
+}
+
+
+[]
+let ``TaskSeq-tryPick happy path middle of seq`` () = task {
+ let! twentyFive =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryPick (fun x -> if x < 26 && x > 24 then Some $"foo{x}" else None)
+
+ twentyFive |> should be Some'
+ twentyFive |> should equal (Some "foo25")
+}
+
+[]
+let ``TaskSeq-tryPickAsync happy path middle of seq`` () = task {
+ let! twentyFive =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryPickAsync (fun x -> task { return if x < 26 && x > 24 then Some $"foo{x}" else None })
+
+ twentyFive |> should be Some'
+ twentyFive |> should equal (Some "foo25")
+}
+
+[]
+let ``TaskSeq-tryPick happy path first item of seq`` () = task {
+ let! first =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryPick (sprintf "foo%i" >> Some) // dummy tasks seq starts at 1
+
+ first |> should be Some'
+ first |> should equal (Some "foo1")
+}
+
+[]
+let ``TaskSeq-tryPickAsync happy path first item of seq`` () = task {
+ let! first =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryPickAsync (fun x -> task { return (sprintf "foo%i" >> Some) x }) // dummy tasks seq starts at 1
+
+ first |> should be Some'
+ first |> should equal (Some "foo1")
+}
+
+[]
+let ``TaskSeq-tryPick happy path last item of seq`` () = task {
+ let! last =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryPick (fun x -> if x = 50 then Some $"foo{x}" else None) // dummy tasks seq ends at 50
+
+ last |> should be Some'
+ last |> should equal (Some "foo50")
+}
+
+[]
+let ``TaskSeq-tryPickAsync happy path last item of seq`` () = task {
+ let! last =
+ createDummyTaskSeqWith 50L<µs> 1000L<µs> 50
+ |> TaskSeq.tryPickAsync (fun x -> task { return if x = 50 then Some $"foo{x}" else None }) // dummy tasks seq ends at 50
+
+ last |> should be Some'
+ last |> should equal (Some "foo50")
+}
diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Tests.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Tests.CE.fs
similarity index 100%
rename from src/FSharpy.TaskSeq.Test/TaskSeq.Tests.fs
rename to src/FSharpy.TaskSeq.Test/TaskSeq.Tests.CE.fs
diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Tests.Other.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Tests.Other.fs
new file mode 100644
index 00000000..bd0100f7
--- /dev/null
+++ b/src/FSharpy.TaskSeq.Test/TaskSeq.Tests.Other.fs
@@ -0,0 +1,27 @@
+module FSharpy.Tests.``Other functions``
+
+open Xunit
+open FsUnit.Xunit
+open FsToolkit.ErrorHandling
+
+open FSharpy
+
+
+[]
+let ``TaskSeq-empty returns an empty sequence`` () = task {
+ let! sq = TaskSeq.empty |> TaskSeq.toSeqCachedAsync
+ Seq.isEmpty sq |> should be True
+ Seq.length sq |> should equal 0
+}
+
+[]
+let ``TaskSeq-isEmpty returns true for empty`` () = task {
+ let! isEmpty = TaskSeq.empty |> TaskSeq.isEmpty
+ isEmpty |> should be True
+}
+
+[]
+let ``TaskSeq-isEmpty returns false for non-empty`` () = task {
+ let! isEmpty = taskSeq { yield 42 } |> TaskSeq.isEmpty
+ isEmpty |> should be False
+}
diff --git a/src/FSharpy.TaskSeq.Test/TaskSeq.Tests.Utility.fs b/src/FSharpy.TaskSeq.Test/TaskSeq.Tests.Utility.fs
deleted file mode 100644
index acec581e..00000000
--- a/src/FSharpy.TaskSeq.Test/TaskSeq.Tests.Utility.fs
+++ /dev/null
@@ -1,15 +0,0 @@
-module FSharpy.Tests.``Utility functions``
-
-open Xunit
-open FsUnit.Xunit
-open FsToolkit.ErrorHandling
-
-open FSharpy
-
-
-[]
-let ``TaskSeq-empty is empty`` () = task {
- let! sq = TaskSeq.empty |> TaskSeq.toSeqCachedAsync
- Seq.isEmpty sq |> should be True
- Seq.length sq |> should equal 0
-}
diff --git a/src/FSharpy.TaskSeq.sln b/src/FSharpy.TaskSeq.sln
index 797ee2df..50bdd390 100644
--- a/src/FSharpy.TaskSeq.sln
+++ b/src/FSharpy.TaskSeq.sln
@@ -16,6 +16,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
..\.github\workflows\build.yaml = ..\.github\workflows\build.yaml
..\.github\dependabot.yml = ..\.github\dependabot.yml
..\.github\workflows\main.yaml = ..\.github\workflows\main.yaml
+ ..\.github\workflows\test.yaml = ..\.github\workflows\test.yaml
EndProjectSection
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharpy.TaskSeq.Test", "FSharpy.TaskSeq.Test\FSharpy.TaskSeq.Test.fsproj", "{06CA2C7E-04DA-4A85-BB8E-4D94BD67AEB3}"
diff --git a/src/FSharpy.TaskSeq/TaskSeq.fs b/src/FSharpy.TaskSeq/TaskSeq.fs
index ea814d81..b8157f6b 100644
--- a/src/FSharpy.TaskSeq/TaskSeq.fs
+++ b/src/FSharpy.TaskSeq/TaskSeq.fs
@@ -11,18 +11,17 @@ module TaskSeq =
// Just for convenience
module Internal = TaskSeqInternal
- /// Initialize an empty taskSeq.
let empty<'T> = taskSeq {
for c: 'T in [] do
yield c
}
+ let isEmpty taskSeq = Internal.isEmpty taskSeq
//
// Convert 'ToXXX' functions
//
- /// Returns taskSeq as an array. This function is blocking until the sequence is exhausted and will properly dispose of the resources.
let toList (t: taskSeq<'T>) = [
let e = t.GetAsyncEnumerator(CancellationToken())
@@ -34,7 +33,6 @@ module TaskSeq =
]
- /// Returns taskSeq as an array. This function is blocking until the sequence is exhausted and will properly dispose of the resources.
let toArray (taskSeq: taskSeq<'T>) = [|
let e = taskSeq.GetAsyncEnumerator(CancellationToken())
@@ -45,7 +43,6 @@ module TaskSeq =
e.DisposeAsync().AsTask().Wait()
|]
- /// Returns taskSeq as a seq, similar to Seq.cached. This function is blocking until the sequence is exhausted and will properly dispose of the resources.
let toSeqCached (taskSeq: taskSeq<'T>) = seq {
let e = taskSeq.GetAsyncEnumerator(CancellationToken())
@@ -56,88 +53,94 @@ module TaskSeq =
e.DisposeAsync().AsTask().Wait()
}
- /// Unwraps the taskSeq as a Task>. This function is non-blocking.
+ let toSeqOfTasks (taskSeq: taskSeq<'T>) = seq {
+ let e = taskSeq.GetAsyncEnumerator(CancellationToken())
+
+ // TODO: check this!
+ try
+ let mutable go = false
+
+ while go do
+ yield task {
+ let! step = e.MoveNextAsync()
+ go <- step
+
+ if step then
+ return e.Current
+ else
+ return Unchecked.defaultof<_> // FIXME!
+ }
+
+ finally
+ e.DisposeAsync().AsTask().Wait()
+ }
+
let toArrayAsync taskSeq =
Internal.toResizeArrayAsync taskSeq
|> Task.map (fun a -> a.ToArray())
- /// Unwraps the taskSeq as a Task>. This function is non-blocking.
let toListAsync taskSeq = Internal.toResizeArrayAndMapAsync List.ofSeq taskSeq
- /// Unwraps the taskSeq as a Task>. This function is non-blocking.
let toResizeArrayAsync taskSeq = Internal.toResizeArrayAsync taskSeq
- /// Unwraps the taskSeq as a Task>. This function is non-blocking.
let toIListAsync taskSeq = Internal.toResizeArrayAndMapAsync (fun x -> x :> IList<_>) taskSeq
- /// Unwraps the taskSeq as a Task>. This function is non-blocking,
- /// exhausts the sequence and caches the results of the tasks in the sequence.
let toSeqCachedAsync taskSeq = Internal.toResizeArrayAndMapAsync (fun x -> x :> seq<_>) taskSeq
//
// Convert 'OfXXX' functions
//
- /// Create a taskSeq of an array.
let ofArray (array: 'T[]) = taskSeq {
for c in array do
yield c
}
- /// Create a taskSeq of a list.
let ofList (list: 'T list) = taskSeq {
for c in list do
yield c
}
- /// Create a taskSeq of a seq.
let ofSeq (sequence: 'T seq) = taskSeq {
for c in sequence do
yield c
}
- /// Create a taskSeq of a ResizeArray, aka List.
let ofResizeArray (data: 'T ResizeArray) = taskSeq {
for c in data do
yield c
}
- /// Create a taskSeq of a sequence of tasks, that may already have hot-started.
let ofTaskSeq (sequence: #Task<'T> seq) = taskSeq {
for c in sequence do
let! c = c
yield c
}
- /// Create a taskSeq of a list of tasks, that may already have hot-started.
let ofTaskList (list: #Task<'T> list) = taskSeq {
for c in list do
let! c = c
yield c
}
- /// Create a taskSeq of an array of tasks, that may already have hot-started.
let ofTaskArray (array: #Task<'T> array) = taskSeq {
for c in array do
let! c = c
yield c
}
- /// Create a taskSeq of a seq of async.
let ofAsyncSeq (sequence: Async<'T> seq) = taskSeq {
for c in sequence do
let! c = task { return! c }
yield c
}
- /// Create a taskSeq of a list of async.
let ofAsyncList (list: Async<'T> list) = taskSeq {
for c in list do
let! c = Task.ofAsync c
yield c
}
- /// Create a taskSeq of an array of async.
let ofAsyncArray (array: Async<'T> array) = taskSeq {
for c in array do
let! c = Async.toTask c
@@ -149,57 +152,110 @@ module TaskSeq =
// iter/map/collect functions
//
- /// 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.
let iter action taskSeq = Internal.iter (SimpleAction action) taskSeq
- /// 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.
let iteri action taskSeq = Internal.iter (CountableAction action) taskSeq
- /// Iterates over the taskSeq applying the async action to each item. This function is non-blocking
- /// exhausts the sequence as soon as the task is evaluated.
let iterAsync action taskSeq = Internal.iter (AsyncSimpleAction action) taskSeq
- /// Iterates over the taskSeq, applying the async action to each item. This function is non-blocking,
- /// exhausts the sequence as soon as the task is evaluated.
let iteriAsync action taskSeq = Internal.iter (AsyncCountableAction action) taskSeq
- /// Maps over the taskSeq, applying the mapper function to each item. This function is non-blocking.
let map (mapper: 'T -> 'U) taskSeq = Internal.map (SimpleAction mapper) taskSeq
- /// Maps over the taskSeq with an index, applying the mapper function to each item. This function is non-blocking.
let mapi (mapper: int -> 'T -> 'U) taskSeq = Internal.map (CountableAction mapper) taskSeq
- /// Maps over the taskSeq, applying the async mapper function to each item. This function is non-blocking.
let mapAsync mapper taskSeq = Internal.map (AsyncSimpleAction mapper) taskSeq
- /// Maps over the taskSeq with an index, applying the async mapper function to each item. This function is non-blocking.
let mapiAsync mapper taskSeq = Internal.map (AsyncCountableAction mapper) taskSeq
- /// Applies the given function to the items in the taskSeq and concatenates all the results in order.
let collect (binder: 'T -> #IAsyncEnumerable<'U>) taskSeq = Internal.collect binder taskSeq
- /// Applies the given function to the items in the taskSeq and concatenates all the results in order.
let collectSeq (binder: 'T -> #seq<'U>) taskSeq = Internal.collectSeq binder taskSeq
- /// Applies the given async function to the items in the taskSeq and concatenates all the results in order.
let collectAsync (binder: 'T -> #Task<#IAsyncEnumerable<'U>>) taskSeq : taskSeq<'U> =
Internal.collectAsync binder taskSeq
- /// Applies the given async function to the items in the taskSeq and concatenates all the results in order.
let collectSeqAsync (binder: 'T -> #Task<#seq<'U>>) taskSeq : taskSeq<'U> = Internal.collectSeqAsync binder taskSeq
+ //
+ // choosers, pickers and the like
+ //
+
+ let tryHead taskSeq = Internal.tryHead taskSeq
+
+ let head taskSeq = task {
+ match! Internal.tryHead taskSeq with
+ | Some head -> return head
+ | None -> return Internal.raiseEmptySeq ()
+ }
+
+ let tryLast taskSeq = Internal.tryLast taskSeq
+
+ let last taskSeq = task {
+ match! Internal.tryLast taskSeq with
+ | Some last -> return last
+ | None -> return Internal.raiseEmptySeq ()
+ }
+
+ let tryItem index taskSeq = Internal.tryItem index taskSeq
+
+ let item index taskSeq = task {
+ match! Internal.tryItem index taskSeq with
+ | Some item -> return item
+ | None ->
+ if index < 0 then
+ return invalidArg (nameof index) "The input must be non-negative."
+ else
+ return Internal.raiseInsufficient ()
+ }
+
+ let tryExactlyOne source = Internal.tryExactlyOne source
+
+ let exactlyOne source = task {
+ match! Internal.tryExactlyOne source with
+ | Some item -> return item
+ | None -> return invalidArg (nameof source) "The input sequence contains more than one element."
+ }
+
+ let choose chooser source = Internal.choose (TryPick chooser) source
+ let chooseAsync chooser source = Internal.choose (TryPickAsync chooser) source
+ let filter predicate source = Internal.filter (TryFilter predicate) source
+ let filterAsync predicate source = Internal.filter (TryFilterAsync predicate) source
+ let tryPick chooser source = Internal.tryPick (TryPick chooser) source
+ let tryPickAsync chooser source = Internal.tryPick (TryPickAsync chooser) source
+ let tryFind predicate source = Internal.tryFind (TryFilter predicate) source
+ let tryFindAsync predicate source = Internal.tryFind (TryFilterAsync predicate) source
+
+ let pick chooser source = task {
+ match! Internal.tryPick (TryPick chooser) source with
+ | Some item -> return item
+ | None -> return Internal.raiseNotFound ()
+ }
+
+ let pickAsync chooser source = task {
+ match! Internal.tryPick (TryPickAsync chooser) source with
+ | Some item -> return item
+ | None -> return Internal.raiseNotFound ()
+ }
+
+ let find predicate source = task {
+ match! Internal.tryFind (TryFilter predicate) source with
+ | Some item -> return item
+ | None -> return Internal.raiseNotFound ()
+ }
+
+ let findAsync predicate source = task {
+ match! Internal.tryFind (TryFilterAsync predicate) source with
+ | Some item -> return item
+ | None -> return Internal.raiseNotFound ()
+ }
+
//
// zip/unzip etc functions
//
- /// Zips two task sequences, returning a taskSeq of the tuples of each sequence, in order. May raise ArgumentException
- /// if the sequences are or unequal length.
let zip taskSeq1 taskSeq2 = Internal.zip taskSeq1 taskSeq2
- /// Applies a function to each element of the task sequence, threading an accumulator argument through the computation.
let fold folder state taskSeq = Internal.fold (FolderAction folder) state taskSeq
- /// Applies an async function to each element of the task sequence, threading an accumulator argument through the computation.
let foldAsync folder state taskSeq = Internal.fold (AsyncFolderAction folder) state taskSeq
diff --git a/src/FSharpy.TaskSeq/TaskSeq.fsi b/src/FSharpy.TaskSeq/TaskSeq.fsi
index 548877a0..feb7da43 100644
--- a/src/FSharpy.TaskSeq/TaskSeq.fsi
+++ b/src/FSharpy.TaskSeq/TaskSeq.fsi
@@ -8,6 +8,11 @@ module TaskSeq =
/// Initialize an empty taskSeq.
val empty<'T> : taskSeq<'T>
+ ///
+ /// Returns if the task sequence contains no elements, otherwise.
+ ///
+ val isEmpty: taskSeq: taskSeq<'T> -> Task
+
/// Returns taskSeq as an array. This function is blocking until the sequence is exhausted and will properly dispose of the resources.
val toList: t: taskSeq<'T> -> 'T list
@@ -103,12 +108,169 @@ module TaskSeq =
/// Applies the given async function to the items in the taskSeq and concatenates all the results in order.
val collectSeqAsync: binder: ('T -> #Task<'SeqU>) -> taskSeq: taskSeq<'T> -> taskSeq<'U> when 'SeqU :> seq<'U>
+ ///
+ /// Returns the first element of the , or if the sequence is empty.
+ ///
+ /// Thrown when the sequence is empty.
+ val tryHead: taskSeq: taskSeq<'T> -> Task<'T option>
+
+ ///
+ /// Returns the first element of the .
+ ///
+ /// Thrown when the sequence is empty.
+ val head: taskSeq: taskSeq<'T> -> Task<'T>
+
+ ///
+ /// Returns the last element of the , or if the sequence is empty.
+ ///
+ /// Thrown when the sequence is empty.
+ val tryLast: taskSeq: taskSeq<'T> -> Task<'T option>
+
+ ///
+ /// Returns the last element of the .
+ ///
+ /// Thrown when the sequence is empty.
+ val last: taskSeq: taskSeq<'T> -> Task<'T>
+
+ ///
+ /// Returns the nth element of the , or if the sequence
+ /// does not contain enough elements, or if is negative.
+ /// Parameter is zero-based, that is, the value 0 returns the first element.
+ ///
+ val tryItem: index: int -> taskSeq: taskSeq<'T> -> Task<'T option>
+
+ ///
+ /// Returns the nth element of the , or if the sequence
+ /// does not contain enough elements, or if is negative.
+ ///
+ /// Thrown when the sequence has insufficient length or
+ /// is negative.
+ val item: index: int -> taskSeq: taskSeq<'T> -> Task<'T>
+
+ ///
+ /// Returns the only element of the task sequence, or if the sequence is empty of
+ /// contains more than one element.
+ ///
+ val tryExactlyOne: source: taskSeq<'T> -> Task<'T option>
+
+ ///
+ /// Returns the only element of the task sequence.
+ ///
+ /// Thrown when the input sequence does not contain precisely one element.
+ val exactlyOne: source: taskSeq<'T> -> Task<'T>
+
+ ///
+ /// Applies the given function to each element of the task sequence. Returns
+ /// a sequence comprised of the results "x" for each element where
+ /// the function returns Some(x).
+ /// If is asynchronous, consider using .
+ ///
+ val choose: chooser: ('T -> 'U option) -> source: taskSeq<'T> -> taskSeq<'U>
+
+ ///
+ /// Applies the given asynchronous function to each element of the task sequence. Returns
+ /// a sequence comprised of the results "x" for each element where
+ /// the function returns .
+ /// If does not need to be asynchronous, consider using .
+ ///
+ val chooseAsync: chooser: ('T -> #Task<'U option>) -> source: taskSeq<'T> -> taskSeq<'U>
+
+ ///
+ /// Returns a new collection containing only the elements of the collection
+ /// for which the given function returns .
+ /// If is asynchronous, consider using .
+ ///
+ val filter: predicate: ('T -> bool) -> source: taskSeq<'T> -> taskSeq<'T>
+
+ ///
+ /// Returns a new collection containing only the elements of the collection
+ /// for which the given asynchronous function returns .
+ /// If does not need to be asynchronous, consider using .
+ ///
+ val filterAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> taskSeq<'T>
+
+ ///
+ /// Applies the given function to successive elements of the task sequence
+ /// in , returning the first result where the function returns .
+ /// If is asynchronous, consider using .
+ ///
+ val tryPick: chooser: ('T -> 'U option) -> source: taskSeq<'T> -> Task<'U option>
+
+ ///
+ /// Applies the given asynchronous function to successive elements of the task sequence
+ /// in , returning the first result where the function returns .
+ /// If does not need to be asynchronous, consider using .
+ ///
+ val tryPickAsync: chooser: ('T -> #Task<'U option>) -> source: taskSeq<'T> -> Task<'U option>
+
+ ///
+ /// Returns the first element of the task sequence in for which the given function
+ /// returns . Returns if no such element exists.
+ /// If is asynchronous, consider using .
+ ///
+ val tryFind: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task<'T option>
+
+ ///
+ /// Returns the first element of the task sequence in for which the given asynchronous function
+ /// returns . Returns if no such element exists.
+ /// If does not need to be asynchronous, consider using .
+ ///
+ val tryFindAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task<'T option>
+
+
+ ///
+ /// Applies the given function to successive elements of the task sequence
+ /// in , returning the first result where the function returns .
+ /// If is asynchronous, consider using .
+ /// Thrown when every item of the sequence
+ /// evaluates to when the given function is applied.
+ ///
+ val pick: chooser: ('T -> 'U option) -> source: taskSeq<'T> -> Task<'U>
+
+ ///
+ /// Applies the given asynchronous function to successive elements of the task sequence
+ /// in , returning the first result where the function returns .
+ /// If does not need to be asynchronous, consider using .
+ /// Thrown when every item of the sequence
+ /// evaluates to when the given function is applied.
+ ///
+ val pickAsync: chooser: ('T -> #Task<'U option>) -> source: taskSeq<'T> -> Task<'U>
+
+ ///
+ /// Returns the first element of the task sequence in for which the given function
+ /// returns .
+ /// If is asynchronous, consider using .
+ ///
+ /// Thrown if no element returns when
+ /// evaluated by the function.
+ val find: predicate: ('T -> bool) -> source: taskSeq<'T> -> Task<'T>
+
+ ///
+ /// Returns the first element of the task sequence in for which the given
+ /// asynchronous function returns .
+ /// If does not need to be asynchronous, consider using .
+ ///
+ /// Thrown if no element returns when
+ /// evaluated by the function.
+ val findAsync: predicate: ('T -> #Task) -> source: taskSeq<'T> -> Task<'T>
+
+ ///
/// Zips two task sequences, returning a taskSeq of the tuples of each sequence, in order. May raise ArgumentException
/// if the sequences are or unequal length.
- val zip: taskSeq1: taskSeq<'T> -> taskSeq2: taskSeq<'U> -> taskSeq<'T * 'U>
-
- /// Applies a function to each element of the task sequence, threading an accumulator argument through the computation.
+ ///
+ /// The sequences have different lengths.
+ val zip: taskSeq1: taskSeq<'T> -> taskSeq2: taskSeq<'U> -> IAsyncEnumerable<'T * 'U>
+
+ ///
+ /// Applies the function to each element in the task sequence,
+ /// threading an accumulator argument of type through the computation.
+ /// If the accumulator function is asynchronous, consider using .
+ ///
val fold: folder: ('State -> 'T -> 'State) -> state: 'State -> taskSeq: taskSeq<'T> -> Task<'State>
- /// Applies an async function to each element of the task sequence, threading an accumulator argument through the computation.
+ ///
+ /// Applies the asynchronous function to each element in the task sequence,
+ /// threading an accumulator argument of type through the computation.
+ /// If the accumulator function does not need to be asynchronous, consider using .
+ ///
val foldAsync: folder: ('State -> 'T -> #Task<'State>) -> state: 'State -> taskSeq: taskSeq<'T> -> Task<'State>
diff --git a/src/FSharpy.TaskSeq/TaskSeqInternal.fs b/src/FSharpy.TaskSeq/TaskSeqInternal.fs
index 462b0a19..97d350a8 100644
--- a/src/FSharpy.TaskSeq/TaskSeqInternal.fs
+++ b/src/FSharpy.TaskSeq/TaskSeqInternal.fs
@@ -1,5 +1,6 @@
namespace FSharpy
+open System
open System.Collections.Generic
open System.Threading
open System.Threading.Tasks
@@ -22,7 +23,52 @@ type FolderAction<'T, 'State, 'TaskState when 'TaskState :> Task<'State>> =
| FolderAction of state_action: ('State -> 'T -> 'State)
| AsyncFolderAction of async_state_action: ('State -> 'T -> 'TaskState)
+[]
+type ChooserAction<'T, 'U, 'TaskOption when 'TaskOption :> Task<'U option>> =
+ | TryPick of try_pick: ('T -> 'U option)
+ | TryPickAsync of async_try_pick: ('T -> 'TaskOption)
+
+[]
+type FilterAction<'T, 'U, 'TaskBool when 'TaskBool :> Task> =
+ | TryFilter of try_filter: ('T -> bool)
+ | TryFilterAsync of async_try_filter: ('T -> 'TaskBool)
+
module internal TaskSeqInternal =
+ let inline raiseEmptySeq () =
+ ArgumentException("The asynchronous input sequence was empty.", "source")
+ |> raise
+
+ let inline raiseInsufficient () =
+ ArgumentException("The asynchronous input sequence was has an insufficient number of elements.", "source")
+ |> raise
+
+ let inline raiseNotFound () =
+ KeyNotFoundException("The predicate function or index did not satisfy any item in the async sequence.")
+ |> raise
+
+ let isEmpty (taskSeq: taskSeq<_>) = task {
+ let e = taskSeq.GetAsyncEnumerator(CancellationToken())
+ let! step = e.MoveNextAsync()
+ return not step
+ }
+
+ let tryExactlyOne (taskSeq: taskSeq<_>) = task {
+ let e = taskSeq.GetAsyncEnumerator(CancellationToken())
+
+ match! e.MoveNextAsync() with
+ | true ->
+ // grab first item and test if there's a second item
+ let current = e.Current
+
+ match! e.MoveNextAsync() with
+ | true -> return None // 2 or more items
+ | false -> return Some current // exactly one
+
+ | false ->
+ // zero items
+ return None
+ }
+
let iter action (taskSeq: taskSeq<_>) = task {
let e = taskSeq.GetAsyncEnumerator(CancellationToken())
let mutable go = true
@@ -141,10 +187,10 @@ module internal TaskSeqInternal =
go <- step1 && step2
if step1 then
- invalidArg "taskSequence1" "The task sequences had different lengths."
+ invalidArg "taskSequence1" "The task sequences have different lengths."
if step2 then
- invalidArg "taskSequence2" "The task sequences had different lengths."
+ invalidArg "taskSequence2" "The task sequences have different lengths."
}
let collect (binder: _ -> #IAsyncEnumerable<_>) (taskSequence: taskSeq<_>) = taskSeq {
@@ -169,13 +215,149 @@ module internal TaskSeqInternal =
yield! result :> seq<_>
}
- /// Returns taskSeq as an array. This function is blocking until the sequence is exhausted.
- let toListResult (t: taskSeq<'T>) = [
- let e = t.GetAsyncEnumerator(CancellationToken())
+ let tryLast (taskSeq: taskSeq<_>) = task {
+ let e = taskSeq.GetAsyncEnumerator(CancellationToken())
+ let mutable go = true
+ let mutable last = ValueNone
+ let! step = e.MoveNextAsync()
+ go <- step
+
+ while go do
+ last <- ValueSome e.Current
+ let! step = e.MoveNextAsync()
+ go <- step
+
+ match last with
+ | ValueSome value -> return Some value
+ | ValueNone -> return None
+ }
+
+ let tryHead (taskSeq: taskSeq<_>) = task {
+ let e = taskSeq.GetAsyncEnumerator(CancellationToken())
+ let mutable go = true
+ let! step = e.MoveNextAsync()
+ go <- step
+
+ if go then return Some e.Current else return None
+ }
+
+ let tryItem index (taskSeq: taskSeq<_>) = task {
+ if index < 0 then
+ // while the loop below wouldn't run anyway, we don't want to call MoveNext in this case
+ // to prevent side effects hitting unnecessarily
+ return None
+ else
+ let e = taskSeq.GetAsyncEnumerator(CancellationToken())
+ let mutable go = true
+ let mutable idx = 0
+ let mutable foundItem = None
+ let! step = e.MoveNextAsync()
+ go <- step
+
+ while go && idx <= index do
+ if idx = index then
+ foundItem <- Some e.Current
- try
- while (let vt = e.MoveNextAsync() in if vt.IsCompleted then vt.Result else vt.AsTask().Result) do
- yield e.Current
- finally
- e.DisposeAsync().AsTask().Wait()
- ]
+ let! step = e.MoveNextAsync()
+ go <- step
+ idx <- idx + 1
+
+ return foundItem
+ }
+
+ let tryPick chooser (taskSeq: taskSeq<_>) = task {
+ let e = taskSeq.GetAsyncEnumerator(CancellationToken())
+
+ let mutable go = true
+ let mutable foundItem = None
+ let! step = e.MoveNextAsync()
+ go <- step
+
+ match chooser with
+ | TryPick picker ->
+ while go do
+ match picker e.Current with
+ | Some value ->
+ foundItem <- Some value
+ go <- false
+ | None ->
+ let! step = e.MoveNextAsync()
+ go <- step
+
+ | TryPickAsync picker ->
+ while go do
+ match! picker e.Current with
+ | Some value ->
+ foundItem <- Some value
+ go <- false
+ | None ->
+ let! step = e.MoveNextAsync()
+ go <- step
+
+ return foundItem
+ }
+
+ let tryFind chooser (taskSeq: taskSeq<_>) = task {
+ let e = taskSeq.GetAsyncEnumerator(CancellationToken())
+
+ let mutable go = true
+ let mutable foundItem = None
+ let! step = e.MoveNextAsync()
+ go <- step
+
+ match chooser with
+ | TryFilter filterer ->
+ while go do
+ let current = e.Current
+
+ match filterer current with
+ | true ->
+ foundItem <- Some current
+ go <- false
+ | false ->
+ let! step = e.MoveNextAsync()
+ go <- step
+
+ | TryFilterAsync filterer ->
+ while go do
+ let current = e.Current
+
+ match! filterer current with
+ | true ->
+ foundItem <- Some current
+ go <- false
+ | false ->
+ let! step = e.MoveNextAsync()
+ go <- step
+
+ return foundItem
+ }
+
+ let choose chooser (taskSeq': taskSeq<_>) = taskSeq {
+ match chooser with
+ | TryPick picker ->
+ for item in taskSeq' do
+ match picker item with
+ | Some value -> yield value
+ | None -> ()
+
+ | TryPickAsync picker ->
+ for item in taskSeq' do
+ match! picker item with
+ | Some value -> yield value
+ | None -> ()
+ }
+
+ let filter chooser (taskSeq': taskSeq<_>) = taskSeq {
+ match chooser with
+ | TryFilter filterer ->
+ for item in taskSeq' do
+ if filterer item then
+ yield item
+
+ | TryFilterAsync filterer ->
+ for item in taskSeq' do
+ match! filterer item with
+ | true -> yield item
+ | false -> ()
+ }