Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -217,7 +217,7 @@ The `TaskSeq` project already has a wide array of functions and functionalities,
- [ ] `exists2` / `map2` / `fold2` / `iter2` and related '2'-functions
- [ ] `mapFold`
- [ ] `pairwise` / `allpairs` / `permute` / `distinct` / `distinctBy`
- [ ] `replicate`
- [x] `replicate`
- [ ] `reduce` / `scan`
- [ ] `unfold`
- [x] Publish package on Nuget, **DONE, PUBLISHED SINCE: 7 November 2022**. See https://www.nuget.org/packages/FSharp.Control.TaskSeq
Expand Down Expand Up @@ -340,7 +340,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.") |
| | `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 @@ -383,7 +383,7 @@ This is what has been implemented so far, is planned or skipped:
| ✅ [#217][]| `where` | `where` | `whereAsync` | |
| | `windowed` | `windowed` | | |
| ✅ [#2][] | `zip` | `zip` | | |
| | `zip3` | `zip3` | | |
| ✅ | `zip3` | `zip3` | | |
| | | `zip4` | | |


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,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 @@ -22,6 +22,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 @@ -408,6 +409,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 @@ -19,6 +19,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 @@ -1358,7 +1368,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 @@ -134,6 +134,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 @@ -513,6 +521,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