Skip to content

Commit f0b5cdd

Browse files
committed
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.
1 parent f8da10d commit f0b5cdd

9 files changed

Lines changed: 73 additions & 0 deletions

File tree

docs/release-notes/.FSharp.Core/11.0.100.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@
44
* 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))
55
* Move `Async.StartChild` to the "Starting Async Computations" docs category alongside `Async.StartChildAsTask`. ([Issue #19667](https://github.com/dotnet/fsharp/issues/19667))
66
* Add `InlineIfLambda` to `Array.init` ([PR #19869](https://github.com/dotnet/fsharp/pull/19869))
7+
8+
### Added
9+
10+
* 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))

src/FSharp.Core/.#map.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bbatsov@Bozhidars-Mac-mini.local.89569:1773847061

src/FSharp.Core/map.fs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,15 @@ module Map =
12191219
let foldBack<'Key, 'T, 'State when 'Key: comparison> folder (table: Map<'Key, 'T>) (state: 'State) =
12201220
MapTree.foldBack folder table.Tree state
12211221

1222+
[<CompiledName("Merge")>]
1223+
let merge resolve (table1: Map<_, _>) (table2: Map<_, _>) =
1224+
(table1, table2)
1225+
||> fold (fun acc key value2 ->
1226+
acc
1227+
|> change key (function
1228+
| Some value1 -> Some(resolve key value1 value2)
1229+
| None -> Some value2))
1230+
12221231
[<CompiledName("ToSeq")>]
12231232
let toSeq (table: Map<_, _>) =
12241233
table |> Seq.map (fun kvp -> kvp.Key, kvp.Value)

src/FSharp.Core/map.fsi

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,29 @@ module Map =
663663
[<CompiledName("Map")>]
664664
val map: mapping: ('Key -> 'T -> 'U) -> table: Map<'Key, 'T> -> Map<'Key, 'U>
665665

666+
/// <summary>Builds a new map that contains the bindings of the two given maps. When a key occurs in
667+
/// both maps, the given function is used to combine the two values into one.</summary>
668+
///
669+
/// <param name="resolve">The function used to combine the values for keys that occur in both maps. It is
670+
/// passed the key and the corresponding values from the first and second map respectively.</param>
671+
/// <param name="table1">The first input map.</param>
672+
/// <param name="table2">The second input map.</param>
673+
///
674+
/// <returns>The merged map.</returns>
675+
///
676+
/// <remarks>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.</remarks>
677+
///
678+
/// <example id="merge-1">
679+
/// <code lang="fsharp">
680+
/// let sample1 = Map [ (1, 1); (2, 2) ]
681+
/// let sample2 = Map [ (2, 20); (3, 30) ]
682+
///
683+
/// (sample1, sample2) ||> Map.merge (fun _key v1 v2 -> v1 + v2) // evaluates to map [(1, 1); (2, 22); (3, 30)]
684+
/// </code>
685+
/// </example>
686+
[<CompiledName("Merge")>]
687+
val merge: resolve: ('Key -> 'T -> 'T -> 'T) -> table1: Map<'Key, 'T> -> table2: Map<'Key, 'T> -> Map<'Key, 'T>
688+
666689
/// <summary>Tests if an element is in the domain of the map.</summary>
667690
///
668691
/// <param name="key">The input key.</param>

tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.debug.bsl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2
436436
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])
437437
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Empty[TKey,T]()
438438
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])
439+
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])
439440
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfArray[TKey,T](System.Tuple`2[TKey,T][])
440441
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfList[TKey,T](Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[TKey,T]])
441442
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfSeq[TKey,T](System.Collections.Generic.IEnumerable`1[System.Tuple`2[TKey,T]])

tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard20.release.bsl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2
436436
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])
437437
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Empty[TKey,T]()
438438
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])
439+
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])
439440
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfArray[TKey,T](System.Tuple`2[TKey,T][])
440441
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfList[TKey,T](Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[TKey,T]])
441442
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfSeq[TKey,T](System.Collections.Generic.IEnumerable`1[System.Tuple`2[TKey,T]])

tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.debug.bsl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2
438438
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])
439439
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Empty[TKey,T]()
440440
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])
441+
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])
441442
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfArray[TKey,T](System.Tuple`2[TKey,T][])
442443
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfList[TKey,T](Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[TKey,T]])
443444
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfSeq[TKey,T](System.Collections.Generic.IEnumerable`1[System.Tuple`2[TKey,T]])

tests/FSharp.Core.UnitTests/FSharp.Core.SurfaceArea.netstandard21.release.bsl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2
438438
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])
439439
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] Empty[TKey,T]()
440440
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])
441+
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])
441442
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfArray[TKey,T](System.Tuple`2[TKey,T][])
442443
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfList[TKey,T](Microsoft.FSharp.Collections.FSharpList`1[System.Tuple`2[TKey,T]])
443444
Microsoft.FSharp.Collections.MapModule: Microsoft.FSharp.Collections.FSharpMap`2[TKey,T] OfSeq[TKey,T](System.Collections.Generic.IEnumerable`1[System.Tuple`2[TKey,T]])

tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Collections/MapModule.fs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,38 @@ type MapModule() =
406406

407407
()
408408

409+
[<Fact>]
410+
member _.Merge() =
411+
412+
// value keys, with both overlapping and disjoint keys
413+
let valueKeyMap1 = Map.ofSeq [(1, 1); (2, 2); (3, 3)]
414+
let valueKeyMap2 = Map.ofSeq [(2, 20); (3, 30); (4, 40)]
415+
let resultValueMap = (valueKeyMap1, valueKeyMap2) ||> Map.merge (fun _ v1 v2 -> v1 + v2)
416+
Assert.AreEqual([(1, 1); (2, 22); (3, 33); (4, 40)] |> Map.ofList, resultValueMap)
417+
418+
// the resolver is passed the key and the values from the first and second map respectively
419+
let left = Map.ofList [("a", "L")]
420+
let right = Map.ofList [("a", "R")]
421+
let resultOrder = (left, right) ||> Map.merge (fun k v1 v2 -> k + v1 + v2)
422+
Assert.AreEqual(Map.ofList [("a", "aLR")], resultOrder)
423+
424+
// reference keys
425+
let refMap1 = Map.ofSeq [for c in ["."; ".."; "..."] do yield (c, c.Length)]
426+
let refMap2 = Map.ofSeq [for c in [".."; "..."; "...."] do yield (c, c.Length * 10)]
427+
let resultRefMap = (refMap1, refMap2) ||> Map.merge (fun _ v1 v2 -> v1 + v2)
428+
Assert.AreEqual([(".", 1); ("..", 22); ("...", 33); ("....", 40)] |> Map.ofList, resultRefMap)
429+
430+
// merging with an empty map returns the other map unchanged
431+
let oeleMap = Map.ofSeq [(1, "one")]
432+
let eptMap = Map.empty<int, string>
433+
Assert.AreEqual(oeleMap, (oeleMap, eptMap) ||> Map.merge (fun _ v1 v2 -> v1 + v2))
434+
Assert.AreEqual(oeleMap, (eptMap, oeleMap) ||> Map.merge (fun _ v1 v2 -> v1 + v2))
435+
436+
// both empty
437+
Assert.AreEqual(eptMap, (eptMap, eptMap) ||> Map.merge (fun _ v1 v2 -> v1 + v2))
438+
439+
()
440+
409441
[<Fact>]
410442
member _.Contains() =
411443
// value keys

0 commit comments

Comments
 (0)