1

Given a Map[Int, Set[Int]], how can I modify a single value of the Map, generating a new one in the process, for example:

val x = Map(1 -> Set(1,2,3))
x(1) + 5 // This creates a new Set, but not a new Map

val y = x(1) change { x => x + 5 }
// The previous functionality is what I'm looking for
// z: Set[Int]] = List(Set(1, 2, 3, 5))
Robin Green
  • 32,079
  • 16
  • 104
  • 187
fresskoma
  • 25,481
  • 10
  • 85
  • 128

7 Answers7

5

As Robin Green suggests, lenses are made for this job. In fact, you want a partial lens, since a map is a partial function of key -> value.

Scalaz 7 includes the mapVPLens function to make a partial lens (PLens) to the value at a chosen key:

import scalaz.PLens._
val x = Map(1 -> Set(1,2,3))

mapVPLens(1) mod ((_: Set[Int]) + 5, x) // Map(1 -> Set(1, 2, 3, 5))

Modifying the value at a non-existent key will have no effect:

mapVPLens(9) mod ((_: Set[Int]) + 5, x) // Map(1 -> Set(1,2,3))
Ben James
  • 121,135
  • 26
  • 193
  • 155
4

In scala 2.10:

implicit class ChangeableMap[K,V]( val m: Map[K,V] ) extends AnyVal {
  def change( k: K )( transform: V => V ): Map[K,V] = {
    m.get( k ).map{ v => m + (k-> transform(v)) }.getOrElse( m )
  }
}

Some test:

scala>val x = Map(1 -> Set(1,2,3), 2 -> Set(4,5))
x: scala.collection.immutable.Map[Int,scala.collection.immutable.Set[Int]] = Map(1 -> Set(1, 2, 3), 2 -> Set(4, 5))
scala> x.change(1) { x => x + 5 }
res1: Map[Int,scala.collection.immutable.Set[Int]] = Map(1 -> Set(1, 2, 3, 5), 2 -> Set(4, 5))

If you're in scala 2.9, this will do:

class ChangeableMap[K,V]( m: Map[K,V] ) {
  def change( k: K )( transform: V => V ): Map[K,V] = {
    m.get( k ).map{ v => m + (k-> transform(v)) }.getOrElse( m )
  }
}
implicit def toChangeableMap[K,V]( m: Map[K,V] ) = new ChangeableMap[K,V]( m )
Régis Jean-Gilles
  • 32,541
  • 5
  • 83
  • 97
3

Use lenses!

However, Scalaz 6, which defines lenses, doesn't have a specific pre-made lens for your situation, which means slightly more work for you - though if your Map is in turn contained in another object, it does have (well-hidden) support for that situation. And Scalaz 7 will have a lens for standalone Maps.

Also, lenses are just pairs of functions, requiring no language support, so you could just roll your own.

Robin Green
  • 32,079
  • 16
  • 104
  • 187
1

A very idiomatic way of solving this problem would be the following (thanks Viktor Klang):

val x = Map(1 -> Set(1,2,3), 2 -> Set(1), 3 -> Set(5))
x.map { case (1, v) => (1, v + 5); case x => x }
// res0: Map(1 -> Set(1, 2, 3, 5))

Or nicely packed into a class as well as an implicit:

class ChangeableMap[K,V](map:Map[K,V]) {
    def change(index:K)(f:V => V) = map.map {
        case (`index`, v:V) => (index, f(v))
        case x => x
    }
}

object ChangeableMap {
    implicit def fromMap[K,V](map:Map[K,V]) = new ChangeableMap(map)
}

With the previous declaration, the following will work:

x.change(1) { x => x + 5 }
x.change(1) { _ + 5 }
// res1:  Map(1 -> Set(1, 2, 3, 5))

Note that this is probably not the fastest solution, given that Scala will (probably, haven't confirmed) iterate over the entire map!

A possibly faster implementation would be the following (though I have not verified if it is actually faster):

class ChangeableMap[K,V](map:Map[K,V]) {
    def change(index:K)(f:V => V) = map.get(index) match {
        case Some(x) => map + ((index, f(x)))
        case None => map
    }
}
fresskoma
  • 25,481
  • 10
  • 85
  • 128
  • Only the last version works as expected. The other implementations rely (wrongly) on the fact that a PartialFunction **is** a function (note that `map` expects a function, not a `PartialFunction`) without handling the default case. Net effect, you get a `MatchError` if your map has more than one element (or the only element is not the one you expected). By example, try it with `val x = Map(1 -> Set(1,2,3), 2 -> Set(4,5))`. – Régis Jean-Gilles Jan 21 '13 at 21:29
  • Actually even the last version does not work as it does not handle he case when the map does **not** contain the key (in your the `index` parameter) at all. – Régis Jean-Gilles Jan 21 '13 at 21:32
  • Thanks @RégisJean-Gilles, I definitely did not test thouroughly enough :) I will fix the answer ASAP :) – fresskoma Jan 21 '13 at 22:01
  • Updated my answer. It should also work in the cases @RégisJean-Gilles mentioned. Thanks for your feedback, and sorry for my initial sloppyness :) – fresskoma Jan 21 '13 at 23:17
  • Can you explain "rely (wrongly) on the **fact** that `PartialFunction` **is a** `Function`?" (Some emphasis mine...) – Randall Schulz Jan 23 '13 at 02:58
  • 1
    I think what he means is that `map` expects a function which is defined for all possible input values (limited by its signature, of course), in other words, if I have a Map[Int], `map` would expect a function that is defined for the entire domain of `Int`. However, given that [`PartialFunction` is a subtype of `Function`](http://stackoverflow.com/questions/930698/why-is-partialfunction-function-in-scala), you can also pass a `PartialFunction` to `map`, in which case you receive an error if the Map contains a value for which the `PartialFunction` is not defined. – fresskoma Jan 23 '13 at 07:19
  • Yes, that's exactly what I meant. Thanks x3ro. – Régis Jean-Gilles Jan 26 '13 at 00:53
1

Here's one from our codebase.

/**
 * Alters a value in a map.
 *
 * modifyMap :: Map k v -> k -> (Maybe v -> Maybe v) -> Map k v
 * See Haskell's Data.Map.alter
 *
 * @param m   the map to modify
 * @param key the key to modify the value of
 * @param mod a function that takes the existing value (if any) and returns an optional new value
 *
 * @return the modified map
 */
def modifyMap[K,V](m: Map[K,V], key: K)
                  (mod: (Option[V] ⇒ Option[V])): Map[K,V] = {
  mod(m.get(key)) match {
    case Some(newVal) ⇒ m + (key → newVal)
    case None ⇒ m - key
  }
}

And here's how you use it:

modifyMap(myMap, "someKey") { 
  case Some(someVal) => 
    // present
    if (condition) 
      Some(valueDerivedFrom(someVal)) // provide a new mapping for someKey
    else 
      None // someKey will now be unset
  case None => 
    // wasn't present
    if (condition)
      Some(newValue) // provide a new value for someKey
    else
      None // leave someKey unset
}
Alex Cruise
  • 7,939
  • 1
  • 27
  • 40
1

I think the easiest way would be using scala.collection.mutable.Map.

import scala.collection.mutable.Map

val m = Map(1 -> Set(1,2,3))
m.update(1, m(1) + 5)
// now the Map looks like this: Map(1 -> Set(1,2,3,5))

If you get an immutable Map, you can simply convert it to a mutable one by using:

val n: collection.mutale.Map(m.toSeq: _*)

This also works the other way around, if you need to return an immutable Map.

tgr
  • 3,557
  • 4
  • 33
  • 63
  • While this is certainly an easy solution, I fear that it might be even more inefficient than my "idiomatic" approach :D – fresskoma Jan 22 '13 at 10:16
0

As mentioned before you can use Partial Lens for this sort of problem, scalaz and Monocle implements it. Here is how you would do it with Monocle:

import monocle.syntax.taversal._ // to use |->>
import monocle.syntax.at._       // to use at

val x = Map(1 -> Set(1,2,3))

x |->> at(1) modify(_ + 5) == Map(1 -> Set(1,2,3,5))  
Julien Truffaut
  • 477
  • 4
  • 10