diff --git a/README.md b/README.md
index dcc1cbf..de10b90 100644
--- a/README.md
+++ b/README.md
@@ -214,7 +214,7 @@ The `TaskSeq` project already has a wide array of functions and functionalities,
- [x] `chunkBySize` / `windowed` (see [#258])
- [ ] `compareWith`
- [ ] `distinct`
- - [ ] `exists2` / `map2` / `fold2` / `iter2` and related '2'-functions
+ - [ ] `exists2` / `fold2` / `forall2` and remaining '2'-functions (`map2`, `iter2` ✅ now implemented)
- [ ] `mapFold`
- [x] `pairwise` (see [#293])
- [ ] `allpairs` / `permute` / `distinct` / `distinctBy`
@@ -305,14 +305,14 @@ This is what has been implemented so far, is planned or skipped:
| ✅ [#23][] | `isEmpty` | `isEmpty` | | |
| ✅ [#23][] | `item` | `item` | | |
| ✅ [#2][] | `iter` | `iter` | `iterAsync` | |
-| | `iter2` | `iter2` | `iter2Async` | |
+| ✅ | `iter2` | `iter2` | `iter2Async` | |
| ✅ [#2][] | `iteri` | `iteri` | `iteriAsync` | |
| | `iteri2` | `iteri2` | `iteri2Async` | |
| ✅ [#23][] | `last` | `last` | | |
| ✅ [#53][] | `length` | `length` | | |
| ✅ [#53][] | | `lengthBy` | `lengthByAsync` | |
| ✅ [#2][] | `map` | `map` | `mapAsync` | |
-| | `map2` | `map2` | `map2Async` | |
+| ✅ | `map2` | `map2` | `map2Async` | |
| | `map3` | `map3` | `map3Async` | |
| | `mapFold` | `mapFold` | `mapFoldAsync` | |
| 🚫 | `mapFoldBack` | | | [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.") |
diff --git a/release-notes.txt b/release-notes.txt
index 5c0b97e..d32d109 100644
--- a/release-notes.txt
+++ b/release-notes.txt
@@ -2,6 +2,8 @@
Release notes:
0.6.0
+ - adds TaskSeq.map2 and TaskSeq.map2Async
+ - adds TaskSeq.iter2 and TaskSeq.iter2Async
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
- adds TaskSeq.pairwise, #289
- adds TaskSeq.groupBy and TaskSeq.groupByAsync, #289
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 89f8d94..d46d8d7 100644
--- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
+++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj
@@ -40,6 +40,7 @@
+
diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Map2Iter2.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Map2Iter2.Tests.fs
new file mode 100644
index 0000000..5b4e889
--- /dev/null
+++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Map2Iter2.Tests.fs
@@ -0,0 +1,310 @@
+module TaskSeq.Tests.Map2Iter2
+
+open System.Threading.Tasks
+open Xunit
+open FsUnit.Xunit
+
+open FSharp.Control
+
+//
+// TaskSeq.iter2
+// TaskSeq.iter2Async
+// TaskSeq.map2
+// TaskSeq.map2Async
+//
+
+module Iter2EmptySeq =
+ []
+ let ``Null source is invalid for iter2`` () =
+ assertNullArg
+ <| fun () -> TaskSeq.iter2 (fun _ _ -> ()) null TaskSeq.empty
+
+ assertNullArg
+ <| fun () -> TaskSeq.iter2 (fun _ _ -> ()) TaskSeq.empty null
+
+ []
+ let ``Null source is invalid for iter2Async`` () =
+ assertNullArg
+ <| fun () -> TaskSeq.iter2Async (fun _ _ -> Task.fromResult ()) null TaskSeq.empty
+
+ assertNullArg
+ <| fun () -> TaskSeq.iter2Async (fun _ _ -> Task.fromResult ()) TaskSeq.empty null
+
+ [)>]
+ let ``TaskSeq-iter2 does nothing on two empty sequences`` variant = task {
+ let tq = Gen.getEmptyVariant variant
+ let mutable sum = 0
+ do! TaskSeq.iter2 (fun a b -> sum <- sum + a + b) tq tq
+ sum |> should equal 0
+ }
+
+ [)>]
+ let ``TaskSeq-iter2 does nothing when first sequence is empty`` variant = task {
+ let tq = Gen.getEmptyVariant variant
+ let mutable sum = 0
+ do! TaskSeq.iter2 (fun a b -> sum <- sum + a + b) tq (taskSeq { yield! [ 1..10 ] })
+ sum |> should equal 0
+ }
+
+ [)>]
+ let ``TaskSeq-iter2 does nothing when second sequence is empty`` variant = task {
+ let tq = Gen.getEmptyVariant variant
+ let mutable sum = 0
+ do! TaskSeq.iter2 (fun a b -> sum <- sum + a + b) (taskSeq { yield! [ 1..10 ] }) tq
+ sum |> should equal 0
+ }
+
+ [)>]
+ let ``TaskSeq-iter2Async does nothing on two empty sequences`` variant = task {
+ let tq = Gen.getEmptyVariant variant
+ let mutable sum = 0
+
+ do!
+ TaskSeq.iter2Async
+ (fun a b ->
+ sum <- sum + a + b
+ Task.fromResult ())
+ tq
+ tq
+
+ sum |> should equal 0
+ }
+
+module Iter2Immutable =
+ [)>]
+ let ``TaskSeq-iter2 visits all paired elements in order`` variant = task {
+ let one = Gen.getSeqImmutable variant
+ let two = Gen.getSeqImmutable variant
+ let results = System.Collections.Generic.List()
+ do! TaskSeq.iter2 (fun a b -> results.Add(a, b)) one two
+ results.Count |> should equal 10
+
+ results
+ |> Seq.forall (fun (a, b) -> a = b)
+ |> should be True
+
+ results
+ |> Seq.map fst
+ |> Seq.toArray
+ |> should equal [| 1..10 |]
+ }
+
+ [)>]
+ let ``TaskSeq-iter2Async visits all paired elements in order`` variant = task {
+ let one = Gen.getSeqImmutable variant
+ let two = Gen.getSeqImmutable variant
+ let results = System.Collections.Generic.List()
+
+ do!
+ TaskSeq.iter2Async
+ (fun a b ->
+ results.Add(a, b)
+ Task.fromResult ())
+ one
+ two
+
+ results.Count |> should equal 10
+
+ results
+ |> Seq.forall (fun (a, b) -> a = b)
+ |> should be True
+
+ results
+ |> Seq.map fst
+ |> Seq.toArray
+ |> should equal [| 1..10 |]
+ }
+
+ []
+ let ``TaskSeq-iter2 truncates to shorter sequence when first is shorter`` () = task {
+ let short = taskSeq { yield! [ 1..3 ] }
+ let long = taskSeq { yield! [ 1..10 ] }
+ let mutable count = 0
+ do! TaskSeq.iter2 (fun _ _ -> count <- count + 1) short long
+ count |> should equal 3
+ }
+
+ []
+ let ``TaskSeq-iter2 truncates to shorter sequence when second is shorter`` () = task {
+ let long = taskSeq { yield! [ 1..10 ] }
+ let short = taskSeq { yield! [ 1..3 ] }
+ let mutable count = 0
+ do! TaskSeq.iter2 (fun _ _ -> count <- count + 1) long short
+ count |> should equal 3
+ }
+
+ []
+ let ``TaskSeq-iter2 can combine different element types`` () = task {
+ let ints = taskSeq { yield! [ 1; 2; 3 ] }
+ let strs = taskSeq { yield! [ "a"; "b"; "c" ] }
+ let results = System.Collections.Generic.List()
+ do! TaskSeq.iter2 (fun n s -> results.Add(n, s)) ints strs
+ results.Count |> should equal 3
+
+ results
+ |> Seq.toList
+ |> should equal [ (1, "a"); (2, "b"); (3, "c") ]
+ }
+
+module Map2EmptySeq =
+ []
+ let ``Null source is invalid for map2`` () =
+ assertNullArg
+ <| fun () -> TaskSeq.map2 (fun _ _ -> ()) null TaskSeq.empty
+
+ assertNullArg
+ <| fun () -> TaskSeq.map2 (fun _ _ -> ()) TaskSeq.empty null
+
+ []
+ let ``Null source is invalid for map2Async`` () =
+ assertNullArg
+ <| fun () -> TaskSeq.map2Async (fun _ _ -> Task.fromResult ()) null TaskSeq.empty
+
+ assertNullArg
+ <| fun () -> TaskSeq.map2Async (fun _ _ -> Task.fromResult ()) TaskSeq.empty null
+
+ [)>]
+ let ``TaskSeq-map2 returns empty on two empty sequences`` variant =
+ TaskSeq.map2 (fun a b -> a + b) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant)
+ |> verifyEmpty
+
+ [)>]
+ let ``TaskSeq-map2 returns empty when first sequence is empty`` variant =
+ TaskSeq.map2 (fun a b -> a + b) (Gen.getEmptyVariant variant) (taskSeq { yield! [ 1..10 ] })
+ |> verifyEmpty
+
+ [)>]
+ let ``TaskSeq-map2 returns empty when second sequence is empty`` variant =
+ TaskSeq.map2 (fun a b -> a + b) (taskSeq { yield! [ 1..10 ] }) (Gen.getEmptyVariant variant)
+ |> verifyEmpty
+
+ [)>]
+ let ``TaskSeq-map2Async returns empty on two empty sequences`` variant =
+ TaskSeq.map2Async (fun a b -> Task.fromResult (a + b)) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant)
+ |> verifyEmpty
+
+module Map2Immutable =
+ [)>]
+ let ``TaskSeq-map2 maps all paired elements in order`` variant = task {
+ let one = Gen.getSeqImmutable variant
+ let two = Gen.getSeqImmutable variant
+
+ let! result =
+ TaskSeq.map2 (fun a b -> a + b) one two
+ |> TaskSeq.toArrayAsync
+
+ result |> should haveLength 10
+
+ result
+ |> should equal (Array.init 10 (fun i -> (i + 1) * 2))
+ }
+
+ [)>]
+ let ``TaskSeq-map2Async maps all paired elements in order`` variant = task {
+ let one = Gen.getSeqImmutable variant
+ let two = Gen.getSeqImmutable variant
+
+ let! result =
+ TaskSeq.map2Async (fun a b -> Task.fromResult (a + b)) one two
+ |> TaskSeq.toArrayAsync
+
+ result |> should haveLength 10
+
+ result
+ |> should equal (Array.init 10 (fun i -> (i + 1) * 2))
+ }
+
+ []
+ let ``TaskSeq-map2 truncates to shorter sequence when first is shorter`` () = task {
+ let short = taskSeq { yield! [ 1..3 ] }
+ let long = taskSeq { yield! [ 10..19 ] }
+
+ let! result =
+ TaskSeq.map2 (fun a b -> a + b) short long
+ |> TaskSeq.toArrayAsync
+
+ result |> should haveLength 3
+ result |> should equal [| 11; 13; 15 |]
+ }
+
+ []
+ let ``TaskSeq-map2 truncates to shorter sequence when second is shorter`` () = task {
+ let long = taskSeq { yield! [ 10..19 ] }
+ let short = taskSeq { yield! [ 1..3 ] }
+
+ let! result =
+ TaskSeq.map2 (fun a b -> a + b) long short
+ |> TaskSeq.toArrayAsync
+
+ result |> should haveLength 3
+ result |> should equal [| 11; 13; 15 |]
+ }
+
+ []
+ let ``TaskSeq-map2 can produce different types`` () = task {
+ let ints = taskSeq { yield! [ 1; 2; 3 ] }
+ let strs = taskSeq { yield! [ "a"; "b"; "c" ] }
+
+ let! result =
+ TaskSeq.map2 (fun n s -> sprintf "%d%s" n s) ints strs
+ |> TaskSeq.toArrayAsync
+
+ result |> should equal [| "1a"; "2b"; "3c" |]
+ }
+
+ []
+ let ``TaskSeq-map2 works with equal-length sequences`` () = task {
+ let s1 = taskSeq { yield! [ 1..5 ] }
+ let s2 = taskSeq { yield! [ 10..14 ] }
+
+ let! result =
+ TaskSeq.map2 (fun a b -> a * b) s1 s2
+ |> TaskSeq.toArrayAsync
+
+ result |> should haveLength 5
+ result |> should equal [| 10; 22; 36; 52; 70 |]
+ }
+
+ []
+ let ``TaskSeq-map2Async can use async work in mapping`` () = task {
+ let s1 = taskSeq { yield! [ 1..3 ] }
+ let s2 = taskSeq { yield! [ 4..6 ] }
+
+ let! result =
+ TaskSeq.map2Async
+ (fun a b -> task {
+ do! Task.Delay(0)
+ return a + b
+ })
+ s1
+ s2
+ |> TaskSeq.toArrayAsync
+
+ result |> should equal [| 5; 7; 9 |]
+ }
+
+module Map2SideEffects =
+ [)>]
+ let ``TaskSeq-map2 works correctly with side-effect sequences`` variant = task {
+ let one = Gen.getSeqWithSideEffect variant
+ let two = Gen.getSeqWithSideEffect variant
+
+ let! result =
+ TaskSeq.map2 (fun a b -> a + b) one two
+ |> TaskSeq.toArrayAsync
+
+ result |> should haveLength 10
+
+ result
+ |> Array.forall (fun x -> x % 2 = 0)
+ |> should be True
+ }
+
+ [)>]
+ let ``TaskSeq-iter2 works correctly with side-effect sequences`` variant = task {
+ let one = Gen.getSeqWithSideEffect variant
+ let two = Gen.getSeqWithSideEffect variant
+ let mutable count = 0
+ do! TaskSeq.iter2 (fun _ _ -> count <- count + 1) one two
+ count |> should equal 10
+ }
diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs
index aced3a5..6a435d7 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeq.fs
+++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs
@@ -369,10 +369,14 @@ type TaskSeq private () =
static member iteri action source = Internal.iter (CountableAction action) source
static member iterAsync action source = Internal.iter (AsyncSimpleAction action) source
static member iteriAsync action source = Internal.iter (AsyncCountableAction action) source
+ static member iter2 action source1 source2 = Internal.iter2 action source1 source2
+ static member iter2Async action source1 source2 = Internal.iter2Async action source1 source2
static member map (mapper: 'T -> 'U) source = Internal.map (SimpleAction mapper) source
static member mapi (mapper: int -> 'T -> 'U) source = Internal.map (CountableAction mapper) source
static member mapAsync mapper source = Internal.map (AsyncSimpleAction mapper) source
static member mapiAsync mapper source = Internal.map (AsyncCountableAction mapper) source
+ static member map2 (mapping: 'T -> 'U -> 'V) source1 source2 = Internal.map2 mapping source1 source2
+ static member map2Async (mapping: 'T -> 'U -> #Task<'V>) source1 source2 = Internal.map2Async mapping source1 source2
static member collect (binder: 'T -> #TaskSeq<'U>) source = Internal.collect binder source
static member collectSeq (binder: 'T -> #seq<'U>) source = Internal.collectSeq binder source
static member collectAsync (binder: 'T -> #Task<#TaskSeq<'U>>) source : TaskSeq<'U> = Internal.collectAsync binder source
diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi
index fcf45cf..abfd9b9 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi
+++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi
@@ -679,6 +679,37 @@ type TaskSeq =
/// Thrown when the input sequence is null.
static member iteriAsync: action: (int -> 'T -> #Task) -> source: TaskSeq<'T> -> Task
+ ///
+ /// Iterates over the two input task sequences simultaneously, applying the function
+ /// to each pair of items. When the sequences have different lengths, the shorter sequence determines how many
+ /// items are iterated; excess items in the longer sequence are ignored.
+ /// This function is non-blocking, but will exhaust both input sequences as soon as the task is evaluated.
+ /// If is asynchronous, consider using .
+ ///
+ ///
+ /// A function to apply to each pair of elements from the two input task sequences.
+ /// The first input task sequence.
+ /// The second input task sequence.
+ /// A .
+ /// Thrown when either input task sequence is null.
+ static member iter2: action: ('T -> 'U -> unit) -> source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> Task
+
+ ///
+ /// Iterates over the two input task sequences simultaneously, applying the asynchronous
+ /// function to each pair of items. When the sequences have different lengths, the shorter sequence determines how many
+ /// items are iterated; excess items in the longer sequence are ignored.
+ /// This function is non-blocking, but will exhaust both input sequences as soon as the task is evaluated.
+ /// If is synchronous, consider using .
+ ///
+ ///
+ /// An asynchronous function to apply to each pair of elements from the two input task sequences.
+ /// The first input task sequence.
+ /// The second input task sequence.
+ /// A .
+ /// Thrown when either input task sequence is null.
+ static member iter2Async:
+ action: ('T -> 'U -> #Task) -> source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> Task
+
///
/// Builds a new task sequence whose elements are the corresponding elements of the input task
/// sequence paired with the integer index (from 0) of each element.
@@ -749,6 +780,39 @@ type TaskSeq =
/// Thrown when the input task sequence is null.
static member mapiAsync: mapper: (int -> 'T -> #Task<'U>) -> source: TaskSeq<'T> -> TaskSeq<'U>
+ ///
+ /// Builds a new task sequence whose elements are the results of applying the
+ /// function to corresponding pairs of elements from the two input task sequences.
+ /// When the sequences have different lengths, the shorter sequence determines how many elements are in
+ /// the result; excess elements in the longer sequence are ignored.
+ /// Does not evaluate the input sequences until requested.
+ /// If is asynchronous, consider using .
+ ///
+ ///
+ /// A function to transform pairs of items from the two input task sequences.
+ /// The first input task sequence.
+ /// The second input task sequence.
+ /// The resulting task sequence of mapped values.
+ /// Thrown when either input task sequence is null.
+ static member map2: mapping: ('T -> 'U -> 'V) -> source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'V>
+
+ ///
+ /// Builds a new task sequence whose elements are the results of applying the asynchronous
+ /// function to corresponding pairs of elements from the two input task sequences.
+ /// When the sequences have different lengths, the shorter sequence determines how many elements are in
+ /// the result; excess elements in the longer sequence are ignored.
+ /// Does not evaluate the input sequences until requested.
+ /// If is synchronous, consider using .
+ ///
+ ///
+ /// An asynchronous function to transform pairs of items from the two input task sequences.
+ /// The first input task sequence.
+ /// The second input task sequence.
+ /// The resulting task sequence of mapped values.
+ /// Thrown when either input task sequence is null.
+ static member map2Async:
+ mapping: ('T -> 'U -> #Task<'V>) -> source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'V>
+
///
/// Builds a new task sequence whose elements are the results of applying the
/// function to each of the elements of the input task sequence in , and concatenating the
diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
index 26b02a6..01aecac 100644
--- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
+++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
@@ -388,6 +388,44 @@ module internal TaskSeqInternal =
go <- step
}
+ let iter2 (action: 'T -> 'U -> unit) (source1: TaskSeq<'T>) (source2: TaskSeq<'U>) =
+ checkNonNull (nameof source1) source1
+ checkNonNull (nameof source2) source2
+
+ task {
+ use e1 = source1.GetAsyncEnumerator CancellationToken.None
+ use e2 = source2.GetAsyncEnumerator CancellationToken.None
+ let mutable go = true
+ let! step1 = e1.MoveNextAsync()
+ let! step2 = e2.MoveNextAsync()
+ go <- step1 && step2
+
+ while go do
+ action e1.Current e2.Current
+ let! step1 = e1.MoveNextAsync()
+ let! step2 = e2.MoveNextAsync()
+ go <- step1 && step2
+ }
+
+ let iter2Async (action: 'T -> 'U -> #Task) (source1: TaskSeq<'T>) (source2: TaskSeq<'U>) =
+ checkNonNull (nameof source1) source1
+ checkNonNull (nameof source2) source2
+
+ task {
+ use e1 = source1.GetAsyncEnumerator CancellationToken.None
+ use e2 = source2.GetAsyncEnumerator CancellationToken.None
+ let mutable go = true
+ let! step1 = e1.MoveNextAsync()
+ let! step2 = e2.MoveNextAsync()
+ go <- step1 && step2
+
+ while go do
+ do! action e1.Current e2.Current
+ let! step1 = e1.MoveNextAsync()
+ let! step2 = e2.MoveNextAsync()
+ go <- step1 && step2
+ }
+
let fold folder initial (source: TaskSeq<_>) =
checkNonNull (nameof source) source
@@ -543,6 +581,45 @@ module internal TaskSeqInternal =
yield result
}
+ let map2 (mapping: 'T -> 'U -> 'V) (source1: TaskSeq<'T>) (source2: TaskSeq<'U>) : TaskSeq<'V> =
+ checkNonNull (nameof source1) source1
+ checkNonNull (nameof source2) source2
+
+ taskSeq {
+ use e1 = source1.GetAsyncEnumerator CancellationToken.None
+ use e2 = source2.GetAsyncEnumerator CancellationToken.None
+ let mutable go = true
+ let! step1 = e1.MoveNextAsync()
+ let! step2 = e2.MoveNextAsync()
+ go <- step1 && step2
+
+ while go do
+ yield mapping e1.Current e2.Current
+ let! step1 = e1.MoveNextAsync()
+ let! step2 = e2.MoveNextAsync()
+ go <- step1 && step2
+ }
+
+ let map2Async (mapping: 'T -> 'U -> #Task<'V>) (source1: TaskSeq<'T>) (source2: TaskSeq<'U>) : TaskSeq<'V> =
+ checkNonNull (nameof source1) source1
+ checkNonNull (nameof source2) source2
+
+ taskSeq {
+ use e1 = source1.GetAsyncEnumerator CancellationToken.None
+ use e2 = source2.GetAsyncEnumerator CancellationToken.None
+ let mutable go = true
+ let! step1 = e1.MoveNextAsync()
+ let! step2 = e2.MoveNextAsync()
+ go <- step1 && step2
+
+ while go do
+ let! result = mapping e1.Current e2.Current
+ yield result
+ let! step1 = e1.MoveNextAsync()
+ let! step2 = e2.MoveNextAsync()
+ go <- step1 && step2
+ }
+
let zip (source1: TaskSeq<_>) (source2: TaskSeq<_>) =
checkNonNull (nameof source1) source1
checkNonNull (nameof source2) source2