Merging into immutable Map

I would like to sort-of-add a new value to an immutable Map. Whereas the normal + function replaces the old key/value pair with the new one, what I’d like to do is add the new key/value pair if the key is not already in the Map, however, if they key is already there, I’d like to merge the old value with the new value according to a given merging function.

I have two questions about doing this.

Q1) isn’t there a function I can use for this? Or do I have to write it myself?
Here is the function I’ve written

  def mergeMap[K,V](m:Map[K,V], key:K, value:V, combine:(V,V)=>V):Map[K,V] = {
    if( m.contains(key))
      m + (key -> combine(m(key), value))
    else
      m + (key -> value)
  }

Q2) How can I write the function so I can call it as follows?

mergeMap(m, 1 -> "hello", (a:String,b:String)=>a+b)

But here is what I’d like to write but can’t, because (K->V) is not accepted as a type name.

  //wrong
  def mergeMap[K,V](m:Map[K,V], kv:(K -> V), combine:(V,V)=>V):Map[K,V] = {
    kv match {
      case (key -> value) =>
        if (m.contains(key))
          m + (key -> combine(m(key), value))
        else
          m + (key -> value)
    }
  }

How about:

	def mergeMap[K, V](m: Map[K, V], kv: (K, V), combine: (V, V) => V): Map[K, V] = {
		kv match {
			case (key, value) =>
				if (m.contains(key))
					m + (key -> combine(m(key), value))
				else
					m + (key -> value)
		}
	}

kv is just a regular tuple, so the type is written (K, V).

The cats library has functions that do exactly this:

scala> import cats.implicits._
import cats.implicits._

scala> Map(1 -> "a") |+| Map(1 -> "b")
res0: scala.collection.immutable.Map[Int,String] = Map(1 -> ab)

But it works by taking the Semigroup[V] that is currently in the implicit scope instead of with an explicitly provided combine function.

@andreak, thanks. It is interesting that your suggestion works but if I try to use case (key -> value) it fails.

	def mergeMap[K, V](m: Map[K, V], kv: (K, V), combine: (V, V) => V): Map[K, V] = {
		kv match {
			case (key -> value) =>
				if (m.contains(key))
					m + (key -> combine(m(key), value))
				else
					m + (key -> value)
		}
	}

In Scala 2.13 it works (cause I added it).

1 Like

Ahh, but it is only a regular tuple in some limited circumstances. For example (K -> V) is not the same as (K,V) when K and V are type names. And also, case (k -> v) is not the same as case (k,v) in a pattern match.

Seems counterintuitive/inconsistent to me.

To expand on my previous message, it seemed inconsistent to me too so I fixed that. However not everyone liked the type syntax, so writing K -> V as a type is still not possible. You can easily add such a type alias to your own code though.

1 Like

Arguably so, but the key insight is that (k -> v) isn’t syntax. Instead, -> is just a plain old function that returns a Tuple2. So (in Scala 2.12), you can only use it in function-like ways.

I assume that @Jasper-M added the necessary unapply() bits to make it work in pattern matches for 2.13, but that’s still not magic: it’s still not a type, just a bit of pretty sugar (in the stdlib, not the language) that you can use as special syntax to assemble and disassemble tuples…

2 Likes

FWIW, you can define both in very few lines in your own code.
In 2.13 you just need to add type ->[K,V] = (K,V) to be able to use -> as a type.
In 2.12, you’ll additionaly need the extractor for pattern matches:

object -> {
    def unapply[K,V](kv:(K,V)): Option[(K,V)] = Some(kv)
} 

How about

  def mergeMap[K,V](m:Map[K,V], key:K, value:V, combine:(V,V)=>V):Map[K,V] = 
    m.updatedWith(key) {
      case Some(v) => Some(combine(v, value))
      case None => Some(value)
    }
1 Like