diff --git a/README.md b/README.md index 0d28e59b..57a47784 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ The `TaskSeq` project already has a wide array of functions and functionalities, - [ ] `mapFold` - [x] `pairwise` (see [#293]) - [ ] `allpairs` / `permute` / `distinct` / `distinctBy` - - [ ] `replicate` + - [x] `replicate` - [x] `reduce` / `scan` (see [#299], [#296]) - [x] `unfold` (see [#300]) - [x] Publish package on Nuget, **DONE, PUBLISHED SINCE: 7 November 2022**. See https://www.nuget.org/packages/FSharp.Control.TaskSeq @@ -341,7 +341,7 @@ This is what has been implemented so far, is planned or skipped: | 🚫 | `reduceBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | | ✅ [#236][]| `removeAt` | `removeAt` | | | | ✅ [#236][]| `removeManyAt` | `removeManyAt` | | | -| | `replicate` | `replicate` | | | +| ✅ | `replicate` | `replicate` | | | | ❓ | `rev` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | | ✅ [#296][] | `scan` | `scan` | `scanAsync` | | | 🚫 | `scanBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | @@ -384,7 +384,7 @@ This is what has been implemented so far, is planned or skipped: | ✅ [#217][]| `where` | `where` | `whereAsync` | | | ✅ [#258][] | `windowed` | `windowed` | | | | ✅ [#2][] | `zip` | `zip` | | | -| | `zip3` | `zip3` | | | +| ✅ | `zip3` | `zip3` | | | | | | `zip4` | | | diff --git a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj index 09181383..429bdafa 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -47,6 +47,7 @@ + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Replicate.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Replicate.Tests.fs new file mode 100644 index 00000000..71012c25 --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Replicate.Tests.fs @@ -0,0 +1,81 @@ +module TaskSeq.Tests.Replicate + +open System + +open Xunit +open FsUnit.Xunit + +open FSharp.Control + +// +// TaskSeq.replicate +// + +module EmptySeq = + [] + let ``TaskSeq-replicate with count 0 gives empty sequence`` () = TaskSeq.replicate 0 42 |> verifyEmpty + + [] + let ``TaskSeq-replicate with negative count gives an error`` () = + fun () -> + TaskSeq.replicate -1 42 + |> TaskSeq.toArrayAsync + |> Task.ignore + + |> should throwAsyncExact typeof + + fun () -> + TaskSeq.replicate Int32.MinValue "hello" + |> TaskSeq.toArrayAsync + |> Task.ignore + + |> should throwAsyncExact typeof + +module Immutable = + [] + let ``TaskSeq-replicate produces the correct count and value`` () = task { + let! arr = TaskSeq.replicate 5 99 |> TaskSeq.toArrayAsync + arr |> should haveLength 5 + arr |> should equal [| 99; 99; 99; 99; 99 |] + } + + [] + let ``TaskSeq-replicate with count 1 produces a singleton`` () = task { + let! arr = TaskSeq.replicate 1 "x" |> TaskSeq.toArrayAsync + arr |> should haveLength 1 + arr[0] |> should equal "x" + } + + [] + let ``TaskSeq-replicate with large count`` () = task { + let count = 10_000 + let! arr = TaskSeq.replicate count 7 |> TaskSeq.toArrayAsync + arr |> should haveLength count + arr |> Array.forall ((=) 7) |> should be True + } + + [] + let ``TaskSeq-replicate works with null as value`` () = task { + let! arr = TaskSeq.replicate 3 null |> TaskSeq.toArrayAsync + arr |> should haveLength 3 + arr |> Array.forall (fun x -> x = null) |> should be True + } + + [] + let ``TaskSeq-replicate can be consumed multiple times`` () = task { + let ts = TaskSeq.replicate 4 "a" + let! arr1 = ts |> TaskSeq.toArrayAsync + let! arr2 = ts |> TaskSeq.toArrayAsync + arr1 |> should equal arr2 + } + +module SideEffects = + [] + let ``TaskSeq-replicate with a mutable value captures the value, not a reference`` () = task { + let mutable x = 1 + let ts = TaskSeq.replicate 3 x + x <- 999 + let! arr = ts |> TaskSeq.toArrayAsync + // replicate captures the value at call time (value type) + arr |> should equal [| 1; 1; 1 |] + } diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs index 7443f7fe..67db6a4a 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs @@ -191,3 +191,116 @@ module Other = combined |> should equal [| ("one", 42L); ("two", 43L) |] } + +// +// TaskSeq.zip3 +// + +module EmptySeqZip3 = + [] + let ``Null source is invalid for zip3`` () = + assertNullArg + <| fun () -> TaskSeq.zip3 null TaskSeq.empty TaskSeq.empty + + assertNullArg + <| fun () -> TaskSeq.zip3 TaskSeq.empty null TaskSeq.empty + + assertNullArg + <| fun () -> TaskSeq.zip3 TaskSeq.empty TaskSeq.empty null + + assertNullArg <| fun () -> TaskSeq.zip3 null null null + + [)>] + let ``TaskSeq-zip3 can zip empty sequences`` variant = + TaskSeq.zip3 (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) + |> verifyEmpty + + [)>] + let ``TaskSeq-zip3 stops at first exhausted sequence`` variant = + // second and third are non-empty, first is empty → result is empty + TaskSeq.zip3 (Gen.getEmptyVariant variant) (taskSeq { yield 1 }) (taskSeq { yield 2 }) + |> verifyEmpty + + [)>] + let ``TaskSeq-zip3 stops when second sequence is empty`` variant = + TaskSeq.zip3 (taskSeq { yield 1 }) (Gen.getEmptyVariant variant) (taskSeq { yield 2 }) + |> verifyEmpty + + [)>] + let ``TaskSeq-zip3 stops when third sequence is empty`` variant = + TaskSeq.zip3 (taskSeq { yield 1 }) (taskSeq { yield 2 }) (Gen.getEmptyVariant variant) + |> verifyEmpty + +module ImmutableZip3 = + [)>] + let ``TaskSeq-zip3 zips in correct order`` variant = task { + let one = Gen.getSeqImmutable variant + let two = Gen.getSeqImmutable variant + let three = Gen.getSeqImmutable variant + let! combined = TaskSeq.zip3 one two three |> TaskSeq.toArrayAsync + + combined |> should haveLength 10 + + combined + |> should equal (Array.init 10 (fun x -> x + 1, x + 1, x + 1)) + } + + [] + let ``TaskSeq-zip3 produces correct triples with mixed types`` () = task { + let one = taskSeq { + yield "a" + yield "b" + } + + let two = taskSeq { + yield 1 + yield 2 + } + + let three = taskSeq { + yield true + yield false + } + + let! combined = TaskSeq.zip3 one two three |> TaskSeq.toArrayAsync + + combined + |> should equal [| ("a", 1, true); ("b", 2, false) |] + } + + [] + let ``TaskSeq-zip3 truncates to shortest sequence`` () = task { + let one = taskSeq { yield! [ 1..10 ] } + let two = taskSeq { yield! [ 1..5 ] } + let three = taskSeq { yield! [ 1..3 ] } + let! combined = TaskSeq.zip3 one two three |> TaskSeq.toArrayAsync + + combined |> should haveLength 3 + + combined + |> should equal [| (1, 1, 1); (2, 2, 2); (3, 3, 3) |] + } + + [] + let ``TaskSeq-zip3 works with a single-element sequences`` () = task { + let! combined = + TaskSeq.zip3 (TaskSeq.singleton 1) (TaskSeq.singleton "x") (TaskSeq.singleton true) + |> TaskSeq.toArrayAsync + + combined |> should equal [| (1, "x", true) |] + } + +module SideEffectsZip3 = + [)>] + let ``TaskSeq-zip3 can deal with side effects in sequences`` variant = task { + let one = Gen.getSeqWithSideEffect variant + let two = Gen.getSeqWithSideEffect variant + let three = Gen.getSeqWithSideEffect variant + let! combined = TaskSeq.zip3 one two three |> TaskSeq.toArrayAsync + + combined + |> Array.forall (fun (x, y, z) -> x = y && y = z) + |> should be True + + combined |> should haveLength 10 + } diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index c9c96cc9..11e8dc8d 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -123,6 +123,7 @@ type TaskSeq private () = // the 'private ()' ensure that a constructor is emitted, which is required by IL static member singleton(value: 'T) = Internal.singleton value + static member replicate count value = Internal.replicate count value static member isEmpty source = Internal.isEmpty source @@ -512,6 +513,7 @@ type TaskSeq private () = // static member zip source1 source2 = Internal.zip source1 source2 + static member zip3 source1 source2 source3 = Internal.zip3 source1 source2 source3 static member fold folder state source = Internal.fold (FolderAction folder) state source static member foldAsync folder state source = Internal.fold (AsyncFolderAction folder) state source static member scan folder state source = Internal.scan (FolderAction folder) state source diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index 76b7ffaa..9a6ccc04 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -103,6 +103,16 @@ type TaskSeq = /// The input item to use as the single item of the task sequence. static member singleton: value: 'T -> TaskSeq<'T> + /// + /// Creates a task sequence by replicating a total of times. + /// + /// + /// The number of times to replicate the value. + /// The value to replicate. + /// A task sequence containing copies of . + /// Thrown when is negative. + static member replicate: count: int -> value: 'T -> TaskSeq<'T> + /// /// Returns if the task sequence contains no elements, otherwise. /// @@ -1476,7 +1486,19 @@ type TaskSeq = static member zip: source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'T * 'U> /// - /// Applies the function to each element in the task sequence, threading an accumulator + /// Combines the three task sequences into a new task sequence of triples. The three sequences need not have equal lengths: + /// when one sequence is exhausted any remaining elements in the other sequences are ignored. + /// + /// + /// The first input task sequence. + /// The second input task sequence. + /// The third input task sequence. + /// The result task sequence of triples. + /// Thrown when any of the three input task sequences is null. + static member zip3: + source1: TaskSeq<'T1> -> source2: TaskSeq<'T2> -> source3: TaskSeq<'T3> -> TaskSeq<'T1 * 'T2 * 'T3> + + /// /// argument of type through the computation. If the input function is and the elements are /// then computes. /// If the accumulator function is asynchronous, consider using . diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index b98d8178..e7f0e5f5 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -139,6 +139,14 @@ module internal TaskSeqInternal = } } + let replicate count value = + raiseCannotBeNegative (nameof count) count + + taskSeq { + for _ in 1..count do + yield value + } + /// Returns length unconditionally, or based on a predicate let lengthBy predicate (source: TaskSeq<_>) = checkNonNull (nameof source) source @@ -549,6 +557,29 @@ module internal TaskSeqInternal = go <- step1 && step2 } + let zip3 (source1: TaskSeq<_>) (source2: TaskSeq<_>) (source3: TaskSeq<_>) = + checkNonNull (nameof source1) source1 + checkNonNull (nameof source2) source2 + checkNonNull (nameof source3) source3 + + taskSeq { + use e1 = source1.GetAsyncEnumerator CancellationToken.None + use e2 = source2.GetAsyncEnumerator CancellationToken.None + use e3 = source3.GetAsyncEnumerator CancellationToken.None + let mutable go = true + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let! step3 = e3.MoveNextAsync() + go <- step1 && step2 && step3 + + while go do + yield e1.Current, e2.Current, e3.Current + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let! step3 = e3.MoveNextAsync() + go <- step1 && step2 && step3 + } + let collect (binder: _ -> #IAsyncEnumerable<_>) (source: TaskSeq<_>) = checkNonNull (nameof source) source