From 84c93dd77d47452fe70efd53fdc2b30698f09787 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Thu, 25 Jun 2026 14:34:12 +0300 Subject: [PATCH] Add Map.merge function to FSharp.Core Combines two maps into one, using a caller-supplied function to resolve the values of keys present in both maps. The right-biased "last one wins" merge from the original proposal is just a special case of the combiner form (Map.merge (fun _ _ v2 -> v2)). Implements fsharp/fslang-suggestions#560. --- docs/release-notes/.FSharp.Core/11.0.100.md | 4 +++ src/FSharp.Core/map.fs | 9 ++++++ src/FSharp.Core/map.fsi | 23 +++++++++++++ ...p.Core.SurfaceArea.netstandard20.debug.bsl | 1 + ...Core.SurfaceArea.netstandard20.release.bsl | 1 + ...p.Core.SurfaceArea.netstandard21.debug.bsl | 1 + ...Core.SurfaceArea.netstandard21.release.bsl | 1 + .../Microsoft.FSharp.Collections/MapModule.fs | 32 +++++++++++++++++++ 8 files changed, 72 insertions(+) diff --git a/docs/release-notes/.FSharp.Core/11.0.100.md b/docs/release-notes/.FSharp.Core/11.0.100.md index 3349ac75260..6438157033b 100644 --- a/docs/release-notes/.FSharp.Core/11.0.100.md +++ b/docs/release-notes/.FSharp.Core/11.0.100.md @@ -4,3 +4,7 @@ * Fix `Array.exists2` documentation examples to use equal-length arrays; the previous examples would throw `ArgumentException` at runtime instead of returning the documented `false`/`true` values. ([PR #19672](https://github.com/dotnet/fsharp/pull/19672)) * Move `Async.StartChild` to the "Starting Async Computations" docs category alongside `Async.StartChildAsTask`. ([Issue #19667](https://github.com/dotnet/fsharp/issues/19667)) * Add `InlineIfLambda` to `Array.init` ([PR #19869](https://github.com/dotnet/fsharp/pull/19869)) + +### Added + +* Added `Map.merge`, which combines two maps into one, using a function to resolve the values of keys present in both. ([Suggestion #560](https://github.com/fsharp/fslang-suggestions/issues/560), [PR #19994](https://github.com/dotnet/fsharp/pull/19994)) diff --git a/src/FSharp.Core/map.fs b/src/FSharp.Core/map.fs index 434ccd08e76..296cdd17578 100644 --- a/src/FSharp.Core/map.fs +++ b/src/FSharp.Core/map.fs @@ -1219,6 +1219,15 @@ module Map = let foldBack<'Key, 'T, 'State when 'Key: comparison> folder (table: Map<'Key, 'T>) (state: 'State) = MapTree.foldBack folder table.Tree state + [] + let merge resolve (table1: Map<_, _>) (table2: Map<_, _>) = + (table1, table2) + ||> fold (fun acc key value2 -> + acc + |> change key (function + | Some value1 -> Some(resolve key value1 value2) + | None -> Some value2)) + [] let toSeq (table: Map<_, _>) = table |> Seq.map (fun kvp -> kvp.Key, kvp.Value) diff --git a/src/FSharp.Core/map.fsi b/src/FSharp.Core/map.fsi index 76cf899442a..156d7bc2689 100644 --- a/src/FSharp.Core/map.fsi +++ b/src/FSharp.Core/map.fsi @@ -663,6 +663,29 @@ module Map = [] val map: mapping: ('Key -> 'T -> 'U) -> table: Map<'Key, 'T> -> Map<'Key, 'U> + /// Builds a new map that contains the bindings of the two given maps. When a key occurs in + /// both maps, the given function is used to combine the two values into one. + /// + /// The function used to combine the values for keys that occur in both maps. It is + /// passed the key and the corresponding values from the first and second map respectively. + /// The first input map. + /// The second input map. + /// + /// The merged map. + /// + /// This is an O(m log n) operation, where m is the size of the second map and n is the size of the merged map. + /// + /// + /// + /// let sample1 = Map [ (1, 1); (2, 2) ] + /// let sample2 = Map [ (2, 20); (3, 30) ] + /// + /// (sample1, sample2) ||> Map.merge (fun _key v1 v2 -> v1 + v2) // evaluates to map [(1, 1); (2, 22); (3, 30)] + /// + /// + [] + val merge: resolve: ('Key -> 'T -> 'T -> 'T) -> table1: Map<'Key, 'T> -> table2: Map<'Key, 'T> -> Map<'Key, 'T> + /// Tests if an element is in the domain of the map. /// /// The input key. diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl index 5b6cc0bce4e..015442dd1f5 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl @@ -436,6 +436,7 @@ Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2 Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Change[TKey,T](TKey, Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.FSharpOption`1[T],Microsoft.FSharp.Core.FSharpOption`1[T]], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Empty[TKey,T]() Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Filter[TKey,T](Microsoft.FSharp.Core.FSharpFunc`2[TKey,Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean]], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T]) +Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Merge[TKey,T](Microsoft.FSharp.Core.FSharpFunc`2[TKey,Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpFunc`2[T,T]]], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfArray[TKey,T](System.Tuple`2[TKey,T][]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfList[TKey,T](Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[TKey,T]]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfSeq[TKey,T](System.Collections.Generic.IEnumerable`1[System.Tuple`2[TKey,T]]) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl index 217d4b7c837..f0f53849681 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl @@ -436,6 +436,7 @@ Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2 Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Change[TKey,T](TKey, Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.FSharpOption`1[T],Microsoft.FSharp.Core.FSharpOption`1[T]], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Empty[TKey,T]() Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Filter[TKey,T](Microsoft.FSharp.Core.FSharpFunc`2[TKey,Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean]], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T]) +Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Merge[TKey,T](Microsoft.FSharp.Core.FSharpFunc`2[TKey,Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpFunc`2[T,T]]], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfArray[TKey,T](System.Tuple`2[TKey,T][]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfList[TKey,T](Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[TKey,T]]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfSeq[TKey,T](System.Collections.Generic.IEnumerable`1[System.Tuple`2[TKey,T]]) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl index 43defdb622e..f7f23a2a939 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl @@ -438,6 +438,7 @@ Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2 Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Change[TKey,T](TKey, Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.FSharpOption`1[T],Microsoft.FSharp.Core.FSharpOption`1[T]], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Empty[TKey,T]() Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Filter[TKey,T](Microsoft.FSharp.Core.FSharpFunc`2[TKey,Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean]], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T]) +Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Merge[TKey,T](Microsoft.FSharp.Core.FSharpFunc`2[TKey,Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpFunc`2[T,T]]], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfArray[TKey,T](System.Tuple`2[TKey,T][]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfList[TKey,T](Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[TKey,T]]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfSeq[TKey,T](System.Collections.Generic.IEnumerable`1[System.Tuple`2[TKey,T]]) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl index ed913ea04d3..805dda1ea8a 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl +++ b/tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl @@ -438,6 +438,7 @@ Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2 Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Change[TKey,T](TKey, Microsoft.FSharp.Core.FSharpFunc`2[Microsoft.FSharp.Core.FSharpOption`1[T],Microsoft.FSharp.Core.FSharpOption`1[T]], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Empty[TKey,T]() Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Filter[TKey,T](Microsoft.FSharp.Core.FSharpFunc`2[TKey,Microsoft.FSharp.Core.FSharpFunc`2[T,System.Boolean]], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T]) +Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Merge[TKey,T](Microsoft.FSharp.Core.FSharpFunc`2[TKey,Microsoft.FSharp.Core.FSharpFunc`2[T,Microsoft.FSharp.Core.FSharpFunc`2[T,T]]], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T], Microsoft.FSharp.Collections.FSharpMap`2[TKey,T]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfArray[TKey,T](System.Tuple`2[TKey,T][]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfList[TKey,T](Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[TKey,T]]) Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfSeq[TKey,T](System.Collections.Generic.IEnumerable`1[System.Tuple`2[TKey,T]]) diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/MapModule.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/MapModule.fs index fec6af0757c..af456b425fd 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/MapModule.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/MapModule.fs @@ -406,6 +406,38 @@ type MapModule() = () + [] + member _.Merge() = + + // value keys, with both overlapping and disjoint keys + let valueKeyMap1 = Map.ofSeq [(1, 1); (2, 2); (3, 3)] + let valueKeyMap2 = Map.ofSeq [(2, 20); (3, 30); (4, 40)] + let resultValueMap = (valueKeyMap1, valueKeyMap2) ||> Map.merge (fun _ v1 v2 -> v1 + v2) + Assert.AreEqual([(1, 1); (2, 22); (3, 33); (4, 40)] |> Map.ofList, resultValueMap) + + // the resolver is passed the key and the values from the first and second map respectively + let left = Map.ofList [("a", "L")] + let right = Map.ofList [("a", "R")] + let resultOrder = (left, right) ||> Map.merge (fun k v1 v2 -> k + v1 + v2) + Assert.AreEqual(Map.ofList [("a", "aLR")], resultOrder) + + // reference keys + let refMap1 = Map.ofSeq [for c in ["."; ".."; "..."] do yield (c, c.Length)] + let refMap2 = Map.ofSeq [for c in [".."; "..."; "...."] do yield (c, c.Length * 10)] + let resultRefMap = (refMap1, refMap2) ||> Map.merge (fun _ v1 v2 -> v1 + v2) + Assert.AreEqual([(".", 1); ("..", 22); ("...", 33); ("....", 40)] |> Map.ofList, resultRefMap) + + // merging with an empty map returns the other map unchanged + let oeleMap = Map.ofSeq [(1, "one")] + let eptMap = Map.empty + Assert.AreEqual(oeleMap, (oeleMap, eptMap) ||> Map.merge (fun _ v1 v2 -> v1 + v2)) + Assert.AreEqual(oeleMap, (eptMap, oeleMap) ||> Map.merge (fun _ v1 v2 -> v1 + v2)) + + // both empty + Assert.AreEqual(eptMap, (eptMap, eptMap) ||> Map.merge (fun _ v1 v2 -> v1 + v2)) + + () + [] member _.Contains() = // value keys