Skip to content
Draft
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ This is what has been implemented so far, is planned or skipped:

| Done | `Seq` | `TaskSeq` | Variants | Remarks |
|------------------|--------------------|----------------------|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ❓ | `allPairs` | `allPairs` | | [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.") |
| ✅ | `allPairs` | `allPairs` | |
| ✅ [#81][] | `append` | `append` | | |
| ✅ [#81][] | | | `appendSeq` | |
| ✅ [#81][] | | | `prependSeq` | |
Expand Down
1 change: 1 addition & 0 deletions release-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Release notes:

0.6.0
- adds TaskSeq.allPairs
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
- adds TaskSeq.pairwise, #289
- adds TaskSeq.groupBy and TaskSeq.groupByAsync, #289
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
<Compile Include="TaskSeq.ToXXX.Tests.fs" />
<Compile Include="TaskSeq.UpdateAt.Tests.fs" />
<Compile Include="TaskSeq.Zip.Tests.fs" />
<Compile Include="TaskSeq.AllPairs.Tests.fs" />
<Compile Include="TaskSeq.ChunkBySize.Tests.fs" />
<Compile Include="TaskSeq.Windowed.Tests.fs" />
<Compile Include="TaskSeq.Tests.CE.fs" />
Expand Down
152 changes: 152 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.AllPairs.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
module TaskSeq.Tests.AllPairs

open Xunit
open FsUnit.Xunit

open FSharp.Control

//
// TaskSeq.allPairs
//

module EmptySeq =
[<Fact>]
let ``TaskSeq-allPairs with null source1 raises`` () =
assertNullArg
<| fun () -> TaskSeq.allPairs null TaskSeq.empty<int>

[<Fact>]
let ``TaskSeq-allPairs with null source2 raises`` () =
assertNullArg
<| fun () -> TaskSeq.allPairs TaskSeq.empty<int> null

[<Fact>]
let ``TaskSeq-allPairs with both null raises`` () = assertNullArg <| fun () -> TaskSeq.allPairs null null

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-allPairs returns empty when source1 is empty`` variant = task {
let! result =
TaskSeq.allPairs (Gen.getEmptyVariant variant) (taskSeq { yield! [ 1..5 ] })
|> TaskSeq.toArrayAsync

result |> should be Empty
}

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-allPairs returns empty when source2 is empty`` variant = task {
let! result =
TaskSeq.allPairs (taskSeq { yield! [ 1..5 ] }) (Gen.getEmptyVariant variant)
|> TaskSeq.toArrayAsync

result |> should be Empty
}

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-allPairs returns empty when both sources are empty`` variant = task {
let! result =
TaskSeq.allPairs (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant)
|> TaskSeq.toArrayAsync

result |> should be Empty
}


module Immutable =
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-allPairs produces all pairs in row-major order`` variant = task {
let! result =
TaskSeq.allPairs (Gen.getSeqImmutable variant) (taskSeq { yield! [ 'a'; 'b'; 'c' ] })
|> TaskSeq.toArrayAsync

// source1 has 10 elements (1..10), source2 has 3 chars β†’ 30 pairs
result |> should be (haveLength 30)

// check that for each element of source1, all elements of source2 appear consecutively
for i in 0..9 do
result.[i * 3 + 0] |> should equal (i + 1, 'a')
result.[i * 3 + 1] |> should equal (i + 1, 'b')
result.[i * 3 + 2] |> should equal (i + 1, 'c')
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-allPairs with singleton source1 gives one pair per source2 element`` variant = task {
let! result =
TaskSeq.allPairs (TaskSeq.singleton 42) (Gen.getSeqImmutable variant)
|> TaskSeq.toArrayAsync

result |> should be (haveLength 10)
result |> should equal (Array.init 10 (fun i -> 42, i + 1))
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-allPairs with singleton source2 gives one pair per source1 element`` variant = task {
let! result =
TaskSeq.allPairs (Gen.getSeqImmutable variant) (TaskSeq.singleton 42)
|> TaskSeq.toArrayAsync

result |> should be (haveLength 10)
result |> should equal (Array.init 10 (fun i -> i + 1, 42))
}

[<Fact>]
let ``TaskSeq-allPairs matches Seq.allPairs for small sequences`` () = task {
let xs = [ 1; 2; 3 ]
let ys = [ 10; 20 ]

let expected = Seq.allPairs xs ys |> Seq.toArray

let! result =
TaskSeq.allPairs (TaskSeq.ofList xs) (TaskSeq.ofList ys)
|> TaskSeq.toArrayAsync

result |> should equal expected
}

[<Fact>]
let ``TaskSeq-allPairs source2 is fully materialised before source1 is iterated`` () = task {
// Verify: if source2 raises, it raises before any element of the result is consumed.
let source2 = taskSeq {
yield 1
failwith "source2 error"
}

let result = TaskSeq.allPairs (TaskSeq.ofList [ 'a'; 'b' ]) source2

// Consuming even the first element should surface the exception from source2
do! task {
try
let! _ = TaskSeq.head result
failwith "expected exception"
with ex ->
ex.Message |> should equal "source2 error"
}
}


module SideEffects =
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
let ``TaskSeq-allPairs works with side-effect source1`` variant = task {
let! result =
TaskSeq.allPairs (Gen.getSeqWithSideEffect variant) (taskSeq { yield! [ 'x'; 'y' ] })
|> TaskSeq.toArrayAsync

result |> should be (haveLength 20)

for i in 0..9 do
result.[i * 2 + 0] |> should equal (i + 1, 'x')
result.[i * 2 + 1] |> should equal (i + 1, 'y')
}

[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
let ``TaskSeq-allPairs works with side-effect source2`` variant = task {
let! result =
TaskSeq.allPairs (taskSeq { yield! [ 'x'; 'y' ] }) (Gen.getSeqWithSideEffect variant)
|> TaskSeq.toArrayAsync

result |> should be (haveLength 20)

for i in 0..1 do
for j in 0..9 do
result.[i * 10 + j]
|> should equal ((if i = 0 then 'x' else 'y'), j + 1)
}
1 change: 1 addition & 0 deletions src/FSharp.Control.TaskSeq/TaskSeq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,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 allPairs source1 source2 = Internal.allPairs source1 source2
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
12 changes: 12 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeq.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,18 @@ type TaskSeq =
static member zip3:
source1: TaskSeq<'T1> -> source2: TaskSeq<'T2> -> source3: TaskSeq<'T3> -> TaskSeq<'T1 * 'T2 * 'T3>

/// <summary>
/// Returns a new task sequence that contains all pairings of elements from the first and second task sequences.
/// The second task sequence is fully evaluated before iteration begins. The output is produced lazily as the first
/// sequence is consumed.
/// </summary>
///
/// <param name="source1">The first input task sequence.</param>
/// <param name="source2">The second input task sequence, which is fully evaluated before the result sequence is iterated.</param>
/// <returns>The result task sequence of all pairs from the two input sequences.</returns>
/// <exception cref="T:ArgumentNullException">Thrown when either of the two input task sequences is null.</exception>
static member allPairs: source1: TaskSeq<'T1> -> source2: TaskSeq<'T2> -> TaskSeq<'T1 * 'T2>

/// <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" />.
Expand Down
12 changes: 12 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,18 @@ module internal TaskSeqInternal =
go <- step1 && step2 && step3
}

let allPairs (source1: TaskSeq<_>) (source2: TaskSeq<_>) =
checkNonNull (nameof source1) source1
checkNonNull (nameof source2) source2

taskSeq {
let! arr2 = toResizeArrayAsync source2

for x in source1 do
for y in arr2 do
yield x, y
}

let collect (binder: _ -> #IAsyncEnumerable<_>) (source: TaskSeq<_>) =
checkNonNull (nameof source) source

Expand Down
Loading