Groovy Cookbook

How to merge two maps in Groovy?

One of the most popular map-related operation in any programming language is merging two (or more) maps. In this short blog post, I explain how to do it in the Groovy programming language, starting from the simplest + operation, up to more advanced use cases of merging nested maps and using runtime metaprogramming to add a merge method to the Map interface. Enjoy reading and learning!

Merge maps using + operator

The easiest way to merge two maps in Groovy is to use + operator. This method is straightforward - it creates a new map from the left-hand-side and right-hand-side maps. The below example illustrates it clearly.

def a = [a: 1, b: 3]
def b = [a: 2, c: 4]

def c = a + b

assert c == [a: 2, b: 3, c: 4]

However, this method has one limitation - it doesn’t perform a so-called "deep merge" operation. Any nested map won’t get merged - it will only override the left-hand-side map entry with the one from the right-hand-side map. Just like in the example shown below.

def a = [a: 1, b: 3, z: [a: 10, b: 20]]
def b = [a: 2, c: 4, z: [c: 30]]

def c = a + b

assert c == [a: 2, b: 3, c: 4, z: [c: 30]]

You can see that the final map contains the z key. However, the value it stores comes from the right-hand-side map only.

Merge maps with nested maps

It can be fixed by implementing custom merge(lhs,rhs) function.

def a = [a: 1, b: 3, z: [a: 10, b: 20]]
def b = [a: 2, c: 4, z: [c: 30]]

def merge(Map lhs, Map rhs) {
    return rhs.inject(lhs.clone()) { map, entry ->
        if (map[entry.key] instanceof Map && entry.value instanceof Map) {
            map[entry.key] = merge(map[entry.key], entry.value)
        } else {
            map[entry.key] = entry.value
        }
        return map
    }
}

def d = merge(a, b)

assert d == [a: 2, b: 3, c: 4, z: [a: 10, b: 20, c: 30]]

It starts with the inject function, which is an equivalent (or close equivalent) of the reduce or fold functions from other popular programming languages. It takes two parameters - the initial value (the lhs.clone() in our case), and the two-argument closure that receives the current accumulator (the map variable from our example) and the current map entry from the iteration.

We use lhs.clone() to avoid creating any side effects. If we use just lhs, we would modify the state of the first map passed to the merge function.

In the next step, we need to check what is the type of the specific map key. If the same key stores a map in both maps, we call a merge function to create a new value as a result of merging two nested maps. Otherwise, we just copy the value from the right-hand-side map.

It looks like we are ready to go, but what if maps we are merging contain e.g. a list of numbers?

def a = [a: 1, b: 3, z: [a: 10, b: 20], y: [1,2,3,4]]
def b = [a: 2, c: 4, z: [c: 30], y: [5,6,7]]

def merge(Map lhs, Map rhs) {
    return rhs.inject(lhs.clone()) { map, entry ->
        if (map[entry.key] instanceof Map && entry.value instanceof Map) {
            map[entry.key] = merge(map[entry.key], entry.value)
        } else {
            map[entry.key] = entry.value
        }
        return map
    }
}

def d = merge(a, b)

assert d == [a: 2, b: 3, c: 4, z: [a: 10, b: 20, c: 30], y: [5,6,7]]

Our current implementation does not support merging nested lists. Let’s fix it.

Merging maps with nested lists

The solution to this problem is simple. We need to add one more condition.

def a = [a: 1, b: 3, z: [a: 10, b: 20], y: [1,2,3,4]]
def b = [a: 2, c: 4, z: [c: 30], y: [5,6,7]]

def merge(Map lhs, Map rhs) {
    return rhs.inject(lhs.clone()) { map, entry ->
        if (map[entry.key] instanceof Map && entry.value instanceof Map) {
            map[entry.key] = merge(map[entry.key], entry.value)
        } else if (map[entry.key] instanceof Collection && entry.value instanceof Collection) {
            map[entry.key] += entry.value
        } else {
            map[entry.key] = entry.value
        }
        return map
    }
}

def d = merge(a, b)

assert d == [a: 2, b: 3, c: 4, z: [a: 10, b: 20, c: 30], y: [1,2,3,4,5,6,7]]

It works!

Adding merge method to the Map interface

Groovy supports runtime and compile time metaprogramming. We can use it to add merge(Map m) method to the Map interface. Let’s re-use the merge function we’ve already implemented.

def a = [a: 1, b: 3, z: [a: 10, b: 20], y: [1,2,3,4]]
def b = [a: 2, c: 4, z: [c: 30], y: [5,6,7]]

def merge(Map lhs, Map rhs) {
    return rhs.inject(lhs.clone()) { map, entry ->
        if (map[entry.key] instanceof Map && entry.value instanceof Map) {
            map[entry.key] = merge(map[entry.key], entry.value)
        } else if (map[entry.key] instanceof Collection && entry.value instanceof Collection) {
            map[entry.key] += entry.value
        } else {
            map[entry.key] = entry.value
        }
        return map
    }
}

Map.metaClass.merge << { Map rhs -> merge(delegate, rhs) } (1)

def e = a.merge(b) (2)

assert e == [a: 2, b: 3, c: 4, z: [a: 10, b: 20, c: 30], y: [1,2,3,4,5,6,7]]

In the final example, we use runtime metaprogramming to create merge(Map rhs) method in the Map interface . This way we can simply call a.merge(b) to create a new map using deep merge operation.

Bonus: using Map.withDefault method

The last tip comes from the Reddit user /u/-jp- - thanks for the contribution! He posted a comment with an alternative solution that uses combination of Map.withDefault method with a method pointer operator that creates a closure from the Map.get method. It produces a slightly different solution than the one posted above. However, it’s very clever and creative way to use Groovy, so I want to share it with you as well. Here’s the original comment.

Depending on what semantics you want you can also use withDefault. This will create a view of map1 that gets missing references from map2:

final map1 = [x: 1, y: 2]
final map2 = [z: 3]
final merged = map1.withDefault(map2.&get)
println "$merged.x, $merged.y, $merged.z"

Did you like this article?

Consider buying me a coffee

0 Comments