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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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.") |
Expand Down
2 changes: 2 additions & 0 deletions release-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<Compile Include="TaskSeq.IsEmpty.fs" />
<Compile Include="TaskSeq.Item.Tests.fs" />
<Compile Include="TaskSeq.Iter.Tests.fs" />
<Compile Include="TaskSeq.Map2Iter2.Tests.fs" />
<Compile Include="TaskSeq.Last.Tests.fs" />
<Compile Include="TaskSeq.Length.Tests.fs" />
<Compile Include="TaskSeq.Map.Tests.fs" />
Expand Down
310 changes: 310 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Map2Iter2.Tests.fs
Original file line number Diff line number Diff line change
@@ -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 =
[<Fact>]
let ``Null source is invalid for iter2`` () =
assertNullArg
<| fun () -> TaskSeq.iter2 (fun _ _ -> ()) null TaskSeq.empty

assertNullArg
<| fun () -> TaskSeq.iter2 (fun _ _ -> ()) TaskSeq.empty null

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

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

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

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

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
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 =
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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<int * int>()
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 |]
}

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

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

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

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

[<Fact>]
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<int * string>()
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 =
[<Fact>]
let ``Null source is invalid for map2`` () =
assertNullArg
<| fun () -> TaskSeq.map2 (fun _ _ -> ()) null TaskSeq.empty<int>

assertNullArg
<| fun () -> TaskSeq.map2 (fun _ _ -> ()) TaskSeq.empty<int> null

[<Fact>]
let ``Null source is invalid for map2Async`` () =
assertNullArg
<| fun () -> TaskSeq.map2Async (fun _ _ -> Task.fromResult ()) null TaskSeq.empty<int>

assertNullArg
<| fun () -> TaskSeq.map2Async (fun _ _ -> Task.fromResult ()) TaskSeq.empty<int> null

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-map2 returns empty on two empty sequences`` variant =
TaskSeq.map2 (fun a b -> a + b) (Gen.getEmptyVariant variant) (Gen.getEmptyVariant variant)
|> verifyEmpty

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

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

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

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

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

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

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

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

[<Fact>]
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 =
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
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
}

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