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 @@ +[![build](https://github.com/abelbraaksma/TaskSeq/actions/workflows/main.yaml/badge.svg)](https://github.com/abelbraaksma/TaskSeq/actions/workflows/main.yaml) +[![test](https://github.com/abelbraaksma/TaskSeq/actions/workflows/test.yaml/badge.svg)](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 -> () + }