Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.") |
Expand Down Expand Up @@ -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` | | |


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
<Compile Include="TaskSeq.Pick.Tests.fs" />
<Compile Include="TaskSeq.RemoveAt.Tests.fs" />
<Compile Include="TaskSeq.Singleton.Tests.fs" />
<Compile Include="TaskSeq.Replicate.Tests.fs" />
<Compile Include="TaskSeq.Skip.Tests.fs" />
<Compile Include="TaskSeq.SkipWhile.Tests.fs" />
<Compile Include="TaskSeq.Tail.Tests.fs" />
Expand Down
81 changes: 81 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Replicate.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
module TaskSeq.Tests.Replicate

open System

open Xunit
open FsUnit.Xunit

open FSharp.Control

//
// TaskSeq.replicate
//

module EmptySeq =
[<Fact>]
let ``TaskSeq-replicate with count 0 gives empty sequence`` () = TaskSeq.replicate 0 42 |> verifyEmpty

[<Fact>]
let ``TaskSeq-replicate with negative count gives an error`` () =
fun () ->
TaskSeq.replicate -1 42
|> TaskSeq.toArrayAsync
|> Task.ignore

|> should throwAsyncExact typeof<ArgumentException>

fun () ->
TaskSeq.replicate Int32.MinValue "hello"
|> TaskSeq.toArrayAsync
|> Task.ignore

|> should throwAsyncExact typeof<ArgumentException>

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

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

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

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

[<Fact>]
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 =
[<Fact>]
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 |]
}
113 changes: 113 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Zip.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,116 @@ module Other =

combined |> should equal [| ("one", 42L); ("two", 43L) |]
}

//
// TaskSeq.zip3
//

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

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-zip3 can zip empty sequences`` variant =
TaskSeq.zip3 (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant)
|> verifyEmpty

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

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-zip3 stops when second sequence is empty`` variant =
TaskSeq.zip3 (taskSeq { yield 1 }) (Gen.getEmptyVariant variant) (taskSeq { yield 2 })
|> verifyEmpty

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-zip3 stops when third sequence is empty`` variant =
TaskSeq.zip3 (taskSeq { yield 1 }) (taskSeq { yield 2 }) (Gen.getEmptyVariant variant)
|> verifyEmpty

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

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

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

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

Expand Down Expand Up @@ -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
Expand Down
24 changes: 23 additions & 1 deletion src/FSharp.Control.TaskSeq/TaskSeq.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ type TaskSeq =
/// <param name="value">The input item to use as the single item of the task sequence.</param>
static member singleton: value: 'T -> TaskSeq<'T>

/// <summary>
/// Creates a task sequence by replicating <paramref name="value" /> a total of <paramref name="count" /> times.
/// </summary>
///
/// <param name="count">The number of times to replicate the value.</param>
/// <param name="value">The value to replicate.</param>
/// <returns>A task sequence containing <paramref name="count" /> copies of <paramref name="value" />.</returns>
/// <exception cref="T:ArgumentException">Thrown when <paramref name="count" /> is negative.</exception>
static member replicate: count: int -> value: 'T -> TaskSeq<'T>

/// <summary>
/// Returns <see cref="true" /> if the task sequence contains no elements, <see cref="false" /> otherwise.
/// </summary>
Expand Down Expand Up @@ -1476,7 +1486,19 @@ type TaskSeq =
static member zip: source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'T * 'U>

/// <summary>
/// Applies the function <paramref name="folder" /> 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.
/// </summary>
///
/// <param name="source1">The first input task sequence.</param>
/// <param name="source2">The second input task sequence.</param>
/// <param name="source3">The third input task sequence.</param>
/// <returns>The result task sequence of triples.</returns>
/// <exception cref="T:ArgumentNullException">Thrown when any of the three input task sequences is null.</exception>
static member zip3:
source1: TaskSeq<'T1> -> source2: TaskSeq<'T2> -> source3: TaskSeq<'T3> -> TaskSeq<'T1 * 'T2 * 'T3>

/// <summary>
/// argument of type <typeref name="'State" /> through the computation. If the input function is <paramref name="f" /> and the elements are <paramref name="i0...iN" />
/// then computes<paramref name="f (... (f s i0)...) iN" />.
/// If the accumulator function <paramref name="folder" /> is asynchronous, consider using <see cref="TaskSeq.foldAsync" />.
Expand Down
31 changes: 31 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down