Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to update a nested immutable map

I'm trying to find a cleaner way to update nested immutable structures in Scala. I think I'm looking for something similar to assoc-in in Clojure. I'm not sure how much types factor into this.

For example, in Clojure, to update the "city" attribute of a nested map I'd do:

> (def person {:name "john", :dob "1990-01-01", :home-address {:city "norfolk", :state "VA"}})
#'user/person
> (assoc-in person [:home-address :city] "richmond")
{:name "john", :dob "1990-01-01", :home-address {:state "VA", :city "richmond"}}

What are my options in Scala?

val person = Map("name" -> "john", "dob" -> "1990-01-01", 
             "home-address" -> Map("city" -> "norfolk", "state" -> "VA"))
like image 285
bihzad Avatar asked Jul 09 '26 23:07

bihzad


2 Answers

As indicated in the other answer, you can leverage case classes to get cleaner, typed data objects. But in case what you need is simply to update a map:

val m = Map("A" -> 1, "B" -> 2)
val m2 = m + ("A" -> 3)

The result (in a worksheet):

m: scala.collection.immutable.Map[String,Int] = Map(A -> 1, B -> 2)
m2: scala.collection.immutable.Map[String,Int] = Map(A -> 3, B -> 2)

The + operator on a Map will add the new key-value pair, overwriting if it already exists. Notably, though, because the original value is a val, you have to assign the result to a new val, because you cannot change the original.

Because, in your example, you're rewriting a nested value, doing this manually becomes somewhat more onerous:

val m = Map("A" -> 1, "B" -> Map("X" -> 2, "Y" -> 4))
val m2 = m + ("B" -> Map("X" -> 3))

This yields some loss-of-data (the nested Y value disappears):

m: scala.collection.immutable.Map[String,Any] = Map(A -> 1, B -> Map(X -> 2, Y -> 4))
m2: scala.collection.immutable.Map[String,Any] = Map(A -> 1, B -> Map(X -> 3))  // Note that 'Y' has gone away.

Thus, forcing you to copy the original value and then re-assign it back:

val m = Map("A" -> 1, "B" -> Map("X" -> 2, "Y" -> 4))
val b = m.get("B") match {
  case Some(b: Map[String, Any]) => b + ("X" -> 3)  // Will update `X` while keeping other key-value pairs
  case None => Map("X" -> 3)
}
val m2 = m + ("B" -> b)

This yields the 'expected' result, but is obviously a lot of code:

m: scala.collection.immutable.Map[String,Any] = Map(A -> 1, B -> Map(X -> 2, Y -> 4))
b: scala.collection.immutable.Map[String,Any] = Map(X -> 3, Y -> 4)
m2: scala.collection.immutable.Map[String,Any] = Map(A -> 1, B -> Map(X -> 3, Y -> 4))

In short, with any immutable data structure when you 'update' it you're really copying all the pieces you want and then including updated values where appropriate. If the structure is complicated this can get onerous. Hence the recommendation that @0___ gave with, say, Monocle.

like image 161
Nathaniel Ford Avatar answered Jul 11 '26 14:07

Nathaniel Ford


Scala is a statically typed language, so you may first want to increase the safety of your code by moving away from any-string-to-any-string.

case class Address(city: String, state: String)
case class Person(name: String, dob: java.util.Date, homeAddress: Address)

(Yes, there are better alternatives for java.util.Date).

Then you create an update like this:

val person = Person(name = "john", dob = new java.util.Date(90, 0, 1),
  homeAddress = Address(city = "norfolk", state = "VA"))

person.copy(homeAddress = person.homeAddress.copy(city = "richmond"))

To avoid this nested copy, you would use a lens library, like Monocle or Quicklens (there are many others).

import com.softwaremill.quicklens._
person.modify(_.homeAddress.city).setTo("richmond")
like image 24
0__ Avatar answered Jul 11 '26 15:07

0__



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!