From bd7074a45c692bcf536f2c22b761d546532fc712 Mon Sep 17 00:00:00 2001 From: gusty <1261319+gusty@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:10:06 +0100 Subject: [PATCH 1/3] + zip3 --- src/FSharpPlus/Control/Functor.fs | 39 +++++++++++++++++++++++++++++-- src/FSharpPlus/Operators.fs | 9 +++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/FSharpPlus/Control/Functor.fs b/src/FSharpPlus/Control/Functor.fs index e81052f55..18241bca0 100644 --- a/src/FSharpPlus/Control/Functor.fs +++ b/src/FSharpPlus/Control/Functor.fs @@ -222,8 +222,6 @@ type Zip = static member Zip ((x: Async<'T> , y: Async<'U> , _output: Async<'T*'U> ), _mthd: Zip) = Async.zip x y #if !FABLE_COMPILER static member Zip ((x: Task<'T> , y: Task<'U> , _output: Task<'T*'U> ), _mthd: Zip) = Task.zip x y - #endif - #if !FABLE_COMPILER static member Zip ((x: ValueTask<'T> , y: ValueTask<'U> , _output: ValueTask<'T*'U> ), _mthd: Zip) = ValueTask.zip x y #endif @@ -239,6 +237,43 @@ type Zip with static member inline Zip ((_: ^t when ^t : null and ^t: struct, _: ^u when ^u : null and ^u: struct, _output: ^r when ^r : null and ^r: struct), _mthd: Default1) = id static member inline Zip ((x: '``ZipFunctor<'T1>`` , y: '``ZipFunctor<'T2>`` , _output: '``ZipFunctor<'T1 * 'T2>`` ), _mthd: Default1) = Zip.InvokeOnInstance x y : '``ZipFunctor<'T1 * 'T2>`` + +type Zip3 = + inherit Default1 + static member Zip3 ((x: IEnumerator<'T1> , y: IEnumerator<'T2> , z: IEnumerator<'T3> , _output: IEnumerator<'T1 * 'T2 * 'T3> ), _mthd: Zip3) = Enumerator.zip3 x y z + static member Zip3 ((x: seq<'T> , y: seq<'U> , z: seq<'V> , _output: seq<'T*'U*'V> ), _mthd: Zip3) = Seq.zip3 x y z + static member Zip3 ((x: IDictionary<'K, 'T> , y: IDictionary<'K, 'U> , z: IDictionary<'K, 'V> , _output: IDictionary<'K, 'T * 'U * 'V> ), _mthd: Zip3) = Dict.zip3 x y z + static member Zip3 ((x: IReadOnlyDictionary<'K, 'T>, y: IReadOnlyDictionary<'K, 'U>, z: IReadOnlyDictionary<'K, 'V>, _output: IReadOnlyDictionary<'K, 'T * 'U * 'V>), _mthd: Zip3) = IReadOnlyDictionary.zip3 x y z + static member Zip3 ((x: Dictionary<'K, 'T> , y: Dictionary<'K, 'U> , z: Dictionary<'K, 'V> , _output: Dictionary<'K, 'T * 'U * 'V> ), _mthd: Zip3) = Dictionary.zip3 x y z + static member Zip3 ((x: Map<'K, 'T> , y: Map<'K, 'U> , z: Map<'K, 'V> , _output: Map<'K, 'T * 'U * 'V> ), _mthd: Zip3) = Map.zip3 x y z + static member Zip3 ((f: 'R -> 'T1 , g: 'R -> 'T2 , h: 'R -> 'T3 , _output: 'R -> 'T1 * 'T2 * 'T3 ), _mthd: Zip3) = fun r -> (f r, g r, h r) + static member Zip3 ((f: Func<'R, 'T1> , g: Func<'R, 'T2> , h: Func<'R, 'T3> , _output: Func<'R, 'T1 * 'T2 * 'T3> ), _mthd: Zip3) = Func<_,_> (fun r -> (f.Invoke r, g.Invoke r, h.Invoke r)) + static member Zip3 ((x: list<'T1> , y: list<'T2> , z: list<'T3> , _output: list<'T1 * 'T2 * 'T3> ), _mthd: Zip3) = List.zip3Shortest x y z + static member Zip3 ((x: 'T1 [] , y: 'T2 [] , z: 'T3 [] , _output: ('T1 * 'T2 * 'T3) [] ), _mthd: Zip3) = Array.zip3Shortest x y z + static member Zip3 ((x: ResizeArray<'T1> , y: ResizeArray<'T2> , z: ResizeArray<'T3> , _output: ResizeArray<'T1 * 'T2 * 'T3> ), _mthd: Zip3) = ResizeArray.zip3Shortest x y z + static member Zip3 ((x: option<'T1> , y: option<'T2> , z: option<'T3> , _output: option<'T1 * 'T2 * 'T3> ), _mthd: Zip3) = Option.zip3 x y z + static member Zip3 ((x: voption<'T1> , y: voption<'T2> , z: voption<'T3> , _output: voption<'T1 * 'T2 * 'T3> ), _mthd: Zip3) = ValueOption.zip3 x y z + static member inline Zip3 ((x: Result<'T, 'Error> , y: Result<'U, 'Error> , z: Result<'V, 'Error> , _output: Result<'T * 'U * 'V, 'Error> ), _mthd: Zip3) = Result.apply3With Plus.Invoke (fun a b c -> a, b, c) x y z + static member inline Zip3 ((x: Choice<'T, 'Error> , y: Choice<'U, 'Error> , z: Choice<'V, 'Error> , _output: Choice<'T * 'U * 'V, 'Error> ), _mthd: Zip3) = Choice.apply3With Plus.Invoke (fun a b c -> a, b, c) x y z + static member Zip3 ((x: Async<'T1> , y: Async<'T2> , z: Async<'T3> , _output: Async<'T1 * 'T2 * 'T3> ), _mthd: Zip3) = Async.zip3 x y z + #if !FABLE_COMPILER + static member Zip3 ((x: Task<'T1> , y: Task<'T2> , z: Task<'T3> , _output: Task<'T1 * 'T2 * 'T3> ), _mthd: Zip3) = Task.zip3 x y z + static member Zip3 ((x: ValueTask<'T1> , y: ValueTask<'T2> , z: ValueTask<'T3> , _output: ValueTask<'T1 * 'T2 * 'T3> ), _mthd: Zip3) = ValueTask.zip3 x y z + #endif + + static member inline Invoke (source1: '``ZipFunctor<'T1>``) (source2: '``ZipFunctor<'T2>``) (source3: '``ZipFunctor<'T3>``) = + let inline call_5 (a: ^a, b: ^b, c: ^c, d: ^d, e: ^e) = ((^a or ^b or ^c or ^d or ^e) : (static member Zip3 : (_*_*_*_)*_ -> _) (b, c, d, e), a) + let inline call (a: 'a, b: 'b, c: 'c, d: 'd) = call_5 (a, b, c, d, Unchecked.defaultof<'r>) : 'r + call (Unchecked.defaultof, source1, source2, source3) : '``ZipFunctor<'T1 * 'T2 * 'T3>`` + + static member inline InvokeOnInstance (source1: '``ZipFunctor<'T1>``) (source2: '``ZipFunctor<'T2>``) (source3: '``ZipFunctor<'T3>``) : '``ZipFunctor<'T1 * 'T2 * 'T3>`` = + ((^``ZipFunctor<'T1>`` or ^``ZipFunctor<'T2>`` or ^``ZipFunctor<'T3>`` or ^``ZipFunctor<'T1 * 'T2 * 'T3>``) : (static member Zip3 : _*_*_ -> _) source1, source2, source3) + +type Zip3 with + static member inline Zip3 ((_: ^t when ^t : null and ^t: struct, _: ^u when ^u : null and ^u: struct, _: ^v when ^v : null and ^v: struct, _output: ^r when ^r : null and ^r: struct), _mthd: Default1) = id + static member inline Zip3 ((x: '``ZipFunctor<'T1>`` , y: '``ZipFunctor<'T2>`` , z: '``ZipFunctor<'T3>`` , _output: '``ZipFunctor<'T1 * 'T2 * 'T3>``), _mthd: Default1) = Zip3.InvokeOnInstance x y z : '``ZipFunctor<'T1 * 'T2 * 'T3>`` + + #endif #if !FABLE_COMPILER diff --git a/src/FSharpPlus/Operators.fs b/src/FSharpPlus/Operators.fs index 44130d98f..137185bc5 100644 --- a/src/FSharpPlus/Operators.fs +++ b/src/FSharpPlus/Operators.fs @@ -187,6 +187,15 @@ module Operators = /// Functor let inline zip (source1: '``ZipFunctor<'T1>``) (source2: '``ZipFunctor<'T2>``) : '``ZipFunctor<'T1 * 'T2>`` = Zip.Invoke source1 source2 + /// + /// Zips (tuple) three functors. + /// + /// + /// For collections, if one collection is shorter, excess elements are discarded from the right end of the longer collection. + /// + /// Functor + let inline zip3 (source1: '``ZipFunctor<'T1>``) (source2: '``ZipFunctor<'T2>``) (source3: '``ZipFunctor<'T3>``) : '``ZipFunctor<'T1 * 'T2 * 'T3>`` = Zip3.Invoke source1 source2 source3 + // Applicative ------------------------------------------------------------ /// From aa976eafea60c15b031fd8fac0660d0a36046a17 Mon Sep 17 00:00:00 2001 From: gusty <1261319+gusty@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:42:52 +0100 Subject: [PATCH 2/3] + tests --- tests/FSharpPlus.Tests/Applicatives.fs | 57 +++++++++++++++++++++++ tests/FSharpPlus.Tests/General.fs | 56 ----------------------- tests/FSharpPlus.Tests/Helpers.fs | 63 +++++++++++++++++++++++++- 3 files changed, 119 insertions(+), 57 deletions(-) diff --git a/tests/FSharpPlus.Tests/Applicatives.fs b/tests/FSharpPlus.Tests/Applicatives.fs index 4cb1473ab..c00421d14 100644 --- a/tests/FSharpPlus.Tests/Applicatives.fs +++ b/tests/FSharpPlus.Tests/Applicatives.fs @@ -2,6 +2,7 @@ open System open System.Collections.ObjectModel +open System.Threading.Tasks open FSharpPlus open FSharpPlus.Data open NUnit.Framework @@ -33,3 +34,59 @@ module Applicatives = let arr3 = (+) Compose (async { return [|1;2;3|] } ) <.> Compose (async { return [|10;20;30|] }) CollectionAssert.AreEqual ([|11; 22; 33|], arr3 |> Compose.run |> Async.RunSynchronously) + + + [] + let zip3Test () = + + SideEffects.reset () + let _a = zip3 (seq [1;2;3]) (seq [1. .. 3. ]) (seq ['a';'b';'c']) + Assert.AreEqual (list.Empty, SideEffects.get ()) + + let _b = zip3 (dict [1,'1' ; 2,'2' ; 4,'4']) (dict [1,'1' ; 2,'2' ; 3,'3']) (dict [1,'a' ; 2,'b' ; 3,'c']) + let _c = zip3 [ 1;2;3 ] [ 1. .. 3. ] ['a';'b';'c'] + let _d = zip3 [|1;2;3|] [|1. .. 3.|] [|'a';'b';'c'|] + let _e = zip3 (async {return 1}) (async {return '2'}) (async {return 3.}) + let _f = zip3 (Task.FromResult 1) (Task.FromResult '2') (Task.FromResult 3.) + let _g = zip3 (Some 1) (Some '2') (Some 3.) + let _h = zip3 (Ok 1) (if true then Ok '2' else Error "No") (Ok 3.) + + let _fa a = zip3 a (seq [1. .. 3. ]) (seq ['a';'b';'c']) + let _fb a = zip3 a [ 1. .. 3. ] ['a';'b';'c'] + let _fc a = zip3 a [|1. .. 3.|] [|'a';'b';'c'|] + let _fd a = zip3 a (async {return '2'}) (async {return 3.}) + let _fe a = zip3 a (Task.FromResult '2') (Task.FromResult 3.) + + let _ga b = zip3 (seq [1;2;3]) b (seq ['a';'b';'c']) + let _gb b = zip3 [ 1;2;3 ] b ['a';'b';'c'] + let _gc b = zip3 [|1;2;3|] b [|'a';'b';'c'|] + let _gd b = zip3 (async {return 1}) b (async {return 3.}) + let _ge b = zip3 (Task.FromResult 1) b (Task.FromResult 3.) + + let _ha c = zip3 (seq [1;2;3]) (seq [1. .. 3. ]) c + let _hb c = zip3 [ 1;2;3 ] [ 1. .. 3. ] c + let _hc c = zip3 [|1;2;3|] [|1. .. 3.|] c + let _hd c = zip3 (async {return 1}) (async {return '2'}) c + let _he c = zip3 (Task.FromResult 1) (Task.FromResult '2') c + + let _ia : _ -> _ -> _ -> _ seq = zip3 + let _ib : _ -> _ -> _ -> _ list = zip3 + let _ic : _ -> _ -> _ -> _ [] = zip3 + let _id : _ -> _ -> _ -> Async<_> = zip3 + let _ie : _ -> _ -> _ -> Task<_> = zip3 + + () + + [] + let genericZip3Shortest () = + let a = zip3 [|1; 2; 3|] [|"a"; "b"|] [|10.; 20.; 30.|] + CollectionAssert.AreEqual ([|1,"a",10.; 2,"b",20.|], a) + + let l = zip3 [1; 2] ["a"; "b"; "c"] [10.; 20.; 30.] + CollectionAssert.AreEqual ([1,"a",10.; 2,"b",20.], l) + + let e = zip3 (ResizeArray [1; 2]) (ResizeArray ["a"; "b"; "c"]) (ResizeArray [10.; 20.]) + CollectionAssert.AreEqual (ResizeArray [1,"a",10.; 2,"b",20.], e) + + let nel = zip3 (NonEmptyList.ofList [1; 2]) (NonEmptyList.ofList ["a"; "b"; "c"]) (NonEmptyList.ofList [10.; 20.; 30.]) + CollectionAssert.AreEqual (NonEmptyList.ofList [1,"a",10.; 2,"b",20.], nel) \ No newline at end of file diff --git a/tests/FSharpPlus.Tests/General.fs b/tests/FSharpPlus.Tests/General.fs index 62da5a027..f2169df26 100644 --- a/tests/FSharpPlus.Tests/General.fs +++ b/tests/FSharpPlus.Tests/General.fs @@ -67,62 +67,6 @@ type WrappedListC<'s> = WrappedListC of 's list with static member Zero = WrappedListC List.empty static member Sum (lst: seq>) = Seq.head lst -type WrappedListD<'s> = WrappedListD of 's list with - interface Collections.Generic.IEnumerable<'s> with member x.GetEnumerator () = (let (WrappedListD x) = x in x :> _ seq).GetEnumerator () - interface Collections.IEnumerable with member x.GetEnumerator () = (let (WrappedListD x) = x in x :> _ seq).GetEnumerator () :> Collections.IEnumerator - static member Return (x) = SideEffects.add "Using WrappedListD's Return"; WrappedListD [x] - static member (>>=) ((WrappedListD x):WrappedListD<'T>, f) = SideEffects.add "Using WrappedListD's Bind"; WrappedListD (List.collect (f >> (fun (WrappedListD x) -> x)) x) - static member inline FoldMap (WrappedListD x, f) = - SideEffects.add "Using optimized foldMap" - Seq.fold (fun x y -> x ++ (f y)) zero x - static member Zip (WrappedListD x, WrappedListD y) = SideEffects.add "Using WrappedListD's zip"; WrappedListD (List.zip x y) - static member Exists (x, f) = - SideEffects.add "Using WrappedListD's Exists" - let (WrappedListD lst) = x - List.exists f lst - static member Pick (x, f) = - SideEffects.add "Using WrappedListD's Pick" - let (WrappedListD lst) = x - List.pick f lst - static member Min x = - SideEffects.add "Using WrappedListD's Min" - let (WrappedListD lst) = x - List.min lst - static member MaxBy (x, f) = - SideEffects.add "Using WrappedListD's MaxBy" - let (WrappedListD lst) = x - List.maxBy f lst - static member MapIndexed (WrappedListD x, f) = - SideEffects.add "Using WrappedListD's MapIndexed" - WrappedListD (List.mapi f x) - static member ChooseIndexed (WrappedListD x, f) = - SideEffects.add "Using WrappedListD's ChooseIndexed" - WrappedListD (List.choosei f x) - static member Lift3 (f, WrappedListD x, WrappedListD y, WrappedListD z) = - SideEffects.add "Using WrappedListD's Lift3" - WrappedListD (List.lift3 f x y z) - static member IterateIndexed (WrappedListD x, f) = - SideEffects.add "Using WrappedListD's IterateIndexed" - List.iteri f x - static member inline FoldIndexed (WrappedListD x, f, z) = - SideEffects.add "Using WrappedListD's FoldIndexed" - foldi f z x - static member inline TraverseIndexed (WrappedListD x, f) = - SideEffects.add "Using WrappedListD's TraverseIndexed" - WrappedListD (traversei f x : ^r) - static member FindIndex (WrappedListD x, y) = - SideEffects.add "Using WrappedListD's FindIndex" - findIndex y x - static member FindSliceIndex (WrappedListD x, WrappedListD y) = - SideEffects.add "Using WrappedListD's FindSliceIndex" - findSliceIndex y x - static member FindLastSliceIndex (WrappedListD x, WrappedListD y) = - SideEffects.add "Using WrappedListD's FindLastSliceIndex" - findLastSliceIndex y x - member this.Length = - SideEffects.add "Using WrappedListD's Length" - let (WrappedListD lst) = this - List.length lst type WrappedListE<'s> = WrappedListE of 's list with static member Return x = WrappedListE [x] static member (>>=) (WrappedListE x: WrappedListE<'T>, f) = WrappedListE (List.collect (f >> (fun (WrappedListE x) -> x)) x) diff --git a/tests/FSharpPlus.Tests/Helpers.fs b/tests/FSharpPlus.Tests/Helpers.fs index 0666f6486..acec4c797 100644 --- a/tests/FSharpPlus.Tests/Helpers.fs +++ b/tests/FSharpPlus.Tests/Helpers.fs @@ -1,7 +1,10 @@ module FSharpPlus.Tests.Helpers -open NUnit.Framework +open System open System.Collections +open NUnit.Framework +open FSharpPlus +open FSharpPlus.Control let areEqual (x:'t) (y:'t) = Assert.AreEqual (x, y) let areStEqual x y = Assert.IsTrue( (x = y), sprintf "Expected %A to be structurally equal to %A" x y) @@ -14,3 +17,61 @@ module SideEffects = let add x = effects.Add (x) let get () = effects |> Seq.toList let are lst = areEquivalent lst (get ()) + + +type WrappedListD<'s> = WrappedListD of 's list with + interface Collections.Generic.IEnumerable<'s> with member x.GetEnumerator () = (let (WrappedListD x) = x in x :> _ seq).GetEnumerator () + interface Collections.IEnumerable with member x.GetEnumerator () = (let (WrappedListD x) = x in x :> _ seq).GetEnumerator () :> Collections.IEnumerator + static member Return (x) = SideEffects.add "Using WrappedListD's Return"; WrappedListD [x] + static member (>>=) ((WrappedListD x):WrappedListD<'T>, f) = SideEffects.add "Using WrappedListD's Bind"; WrappedListD (List.collect (f >> (fun (WrappedListD x) -> x)) x) + static member inline FoldMap (WrappedListD x, f) = + SideEffects.add "Using optimized foldMap" + Seq.fold (fun x y -> x ++ (f y)) zero x + static member Zip (WrappedListD x, WrappedListD y) = SideEffects.add "Using WrappedListD's zip"; WrappedListD (List.zip x y) + static member Exists (x, f) = + SideEffects.add "Using WrappedListD's Exists" + let (WrappedListD lst) = x + List.exists f lst + static member Pick (x, f) = + SideEffects.add "Using WrappedListD's Pick" + let (WrappedListD lst) = x + List.pick f lst + static member Min x = + SideEffects.add "Using WrappedListD's Min" + let (WrappedListD lst) = x + List.min lst + static member MaxBy (x, f) = + SideEffects.add "Using WrappedListD's MaxBy" + let (WrappedListD lst) = x + List.maxBy f lst + static member MapIndexed (WrappedListD x, f) = + SideEffects.add "Using WrappedListD's MapIndexed" + WrappedListD (List.mapi f x) + static member ChooseIndexed (WrappedListD x, f) = + SideEffects.add "Using WrappedListD's ChooseIndexed" + WrappedListD (List.choosei f x) + static member Lift3 (f, WrappedListD x, WrappedListD y, WrappedListD z) = + SideEffects.add "Using WrappedListD's Lift3" + WrappedListD (List.lift3 f x y z) + static member IterateIndexed (WrappedListD x, f) = + SideEffects.add "Using WrappedListD's IterateIndexed" + List.iteri f x + static member inline FoldIndexed (WrappedListD x, f, z) = + SideEffects.add "Using WrappedListD's FoldIndexed" + foldi f z x + static member inline TraverseIndexed (WrappedListD x, f) = + SideEffects.add "Using WrappedListD's TraverseIndexed" + WrappedListD (traversei f x : ^r) + static member FindIndex (WrappedListD x, y) = + SideEffects.add "Using WrappedListD's FindIndex" + findIndex y x + static member FindSliceIndex (WrappedListD x, WrappedListD y) = + SideEffects.add "Using WrappedListD's FindSliceIndex" + findSliceIndex y x + static member FindLastSliceIndex (WrappedListD x, WrappedListD y) = + SideEffects.add "Using WrappedListD's FindLastSliceIndex" + findLastSliceIndex y x + member this.Length = + SideEffects.add "Using WrappedListD's Length" + let (WrappedListD lst) = this + List.length lst \ No newline at end of file From 666ac9906d09b158eecdfa1b90bf77c6c9b39e09 Mon Sep 17 00:00:00 2001 From: gusty <1261319+gusty@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:36:34 +0100 Subject: [PATCH 3/3] Add zip3Shortest to NonEmptyList --- src/FSharpPlus/Data/NonEmptyList.fs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/FSharpPlus/Data/NonEmptyList.fs b/src/FSharpPlus/Data/NonEmptyList.fs index d41ac144d..fd2c61c7d 100644 --- a/src/FSharpPlus/Data/NonEmptyList.fs +++ b/src/FSharpPlus/Data/NonEmptyList.fs @@ -147,6 +147,16 @@ module NonEmptyList = /// List with corresponding pairs of input lists. let zipShortest (list1: NonEmptyList<'T>) (list2: NonEmptyList<'U>) = { Head = (list1.Head, list2.Head); Tail = List.zipShortest list1.Tail list2.Tail } + + /// + /// Zip safely three lists. If one list is shorter, excess elements are discarded from the right end of the longer list. + /// + /// First input list. + /// Second input list. + /// Third input list. + /// List with corresponding triplets of input lists. + let zip3Shortest (list1: NonEmptyList<'T1>) (list2: NonEmptyList<'T2>) (list3: NonEmptyList<'T3>) = + { Head = (list1.Head, list2.Head, list3.Head); Tail = List.zip3Shortest list1.Tail list2.Tail list3.Tail } /// Adds an element to the beginning of the given list /// The element to add @@ -1013,6 +1023,9 @@ type NonEmptyList<'t> with [] static member Zip (x, y) = NonEmptyList.zipShortest x y + + [] + static member Zip3 (x, y, z) = NonEmptyList.zip3Shortest x y z static member (>>=) ({Head = x; Tail = xs}, f: _->NonEmptyList<'b>) = let {Head = y; Tail = ys} = f x