如何更新嵌套的不可变映射

时间:2016-09-01 18:55:03

标签: scala clojure immutability

我试图找到一种更简洁的方法来更新Scala中的嵌套不可变结构。我想我在Clojure中寻找类似于assoc-in的东西。我不确定有多少类型因素。

例如,在Clojure中,更新" city"嵌套地图的属性我做了:

> (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"}}

我在Scala中有哪些选择?

val person = Map("name" -> "john", "dob" -> "1990-01-01", 
             "home-address" -> Map("city" -> "norfolk", "state" -> "VA"))

3 个答案:

答案 0 :(得分:4)

Scala是一种静态类型语言,因此您可能首先希望通过从任意字符串转移到任意字符串来提高代码的安全性。

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

(是的,java.util.Datebetter alternatives

然后你创建一个这样的更新:

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"))

要避免这种嵌套copy,您可以使用镜头库,例如MonocleQuicklens(还有很多其他镜头库)。

import com.softwaremill.quicklens._
person.modify(_.homeAddress.city).setTo("richmond")

答案 1 :(得分:3)

如另一个答案中所示,您可以利用case classes来获取更清晰的类型化数据对象。但如果您需要的只是更新地图:

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

结果(在工作表中):

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

+上的Map运算符将添加新的键值对,如果已存在则覆盖。但值得注意的是,由于原始值为val,因此您必须将结果分配给新的val,因为您无法更改原始值。

因为在您的示例中,您正在重写嵌套值,因此手动执行此操作会变得更加繁重:

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

这会产生一些数据丢失(嵌套的Y值消失):

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.

因此,强制您复制原始值,然后重新分配:

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)

这产生了预期的'结果,但显然是很多代码:

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))

简而言之,当您更新'时,使用任何不可变数据结构。你真的要复制你想要的所有部分,然后在适当的时候包括更新的值。如果结构复杂,这可能会变得繁重。因此推荐@ 0 ___给出了Monocle

答案 2 :(得分:0)

另外两个答案很好地总结了正确建模问题的重要性,因此我们最终不必处理Map [String,Object]类型的集合。

使用Scala中安静强大的功能流水线和更高阶函数功能,只需在这里添加我的两分钱即可获得强力解决方案。需要使用丑陋的asInstanceOf,因为Map值具有不同的类型,因此Scala将Map签名视为Map [String,Any]。

 val person: Map[String,Any]  = Map("name" -> "john", "dob" -> "1990-01-01", "home-address" -> Map("city" -> "norfolk", "state" -> "VA"))

 val newperson = person.map({case(k,v) => if(k == "home-address") v.asInstanceOf[Map[String,String]].updated("city","Virginia") else k -> v})