基于先前值更新Map中的值的惯用方法

时间:2012-01-25 13:57:11

标签: scala map immutability

假设我将银行帐户信息存储在不可变Map

val m = Map("Mark" -> 100, "Jonathan" -> 350, "Bob" -> 65)

我想从Mark的账户中提取50美元。我可以这样做:

val m2 = m + ("Mark" -> (m("Mark") - 50))

但这段代码对我来说似乎很难看。有没有更好的方法来写这个?

4 个答案:

答案 0 :(得分:33)

遗憾的是,adjust API中没有Map。我有时使用类似下面的函数(以Haskell的Data.Map.adjust为模型,具有不同的参数顺序):

def adjust[A, B](m: Map[A, B], k: A)(f: B => B) = m.updated(k, f(m(k)))

现在adjust(m, "Mark")(_ - 50)可以满足您的需求。如果你真的想要更干净的东西,你也可以使用pimp-my-library pattern来获得更自然的m.adjust("Mark")(_ - 50)语法。

(请注意,如果k不在地图中,则上面的简短版本会引发异常,这与Haskell行为不同,可能是您想在实际代码中修复的内容。)

答案 1 :(得分:12)

这可以通过镜头来完成。镜头的想法是能够放大不可变结构的特定部分,并且能够1)从较大的结构中检索较小的部分,或者2)创建具有修改的较小部分的新的较大的结构。在这种情况下,你想要的是#2。

首先,从this answer偷来的Lens的简单实施,从scalaz中窃取:

case class Lens[A,B](get: A => B, set: (A,B) => A) extends Function1[A,B] with Immutable {
  def apply(whole: A): B   = get(whole)
  def updated(whole: A, part: B): A = set(whole, part) // like on immutable maps
  def mod(a: A)(f: B => B) = set(a, f(this(a)))
  def compose[C](that: Lens[C,A]) = Lens[C,B](
    c => this(that(c)),
    (c, b) => that.mod(c)(set(_, b))
  )
  def andThen[C](that: Lens[B,C]) = that compose this
}

接下来,一个智能构造函数,用于创建从“更大结构”Map[A,B]到“更小部分”Option[B]的镜头。我们通过提供特定密钥来指出我们想要查看的“较小部分”。 (灵感来自我记得的Edward Kmett's presentation on Lenses in Scala):

def containsKey[A,B](k: A) = Lens[Map[A,B], Option[B]](
  get = (m:Map[A,B]) => m.get(k),
  set = (m:Map[A,B], opt: Option[B]) => opt match {
    case None => m - k
    case Some(v) => m + (k -> v)
  }
)

现在可以编写代码:

val m2 = containsKey("Mark").mod(m)(_.map(_ - 50))

n.b。我实际上从我偷了它的答案中改变了mod,以便它的输入成为咖喱。这有助于避免额外的类型注释。另请注意_.map,因为请记住,我们的镜头是Map[A,B]Option[B]。这意味着如果地图不包含密钥"Mark",则地图将保持不变。否则,此解决方案最终与Travis提供的adjust解决方案非常相似。

答案 2 :(得分:9)

SO Answer使用scalaz中的|+|运算符

提出了另一种选择
val m2 = m |+| Map("Mark" -> -50)

|+|运算符将对现有键的值求和,或在新键下插入值。

答案 3 :(得分:0)

Scala 2.13开始,Map#updatedWith达到了这个确切目的:

// val map = Map("Mark" -> 100, "Jonathan" -> 350, "Bob" -> 65)
map.updatedWith("Mark")(_.map(_ - 50))
// collection.immutable.Map[String,Int] = Map("Mark" -> 50, "Jonathan" -> 350, "Bob" -> 65)

或更明确:

map.updatedWith("Mark")({ case money: Option[Int] => money.map(_ - 50) })

请注意(如果引用doc,则重新映射函数返回Some(v)时,将使用新值v更新映射。如果重新映射函数返回None,则映射将被删除(如果最初不存在,则保持不存在。)

  

defupdatedWith [V1>:V](键:K)(remappingFunction:(Option [V])=> Option [V1]):Map [K,V1]

这样,我们可以优雅地处理其中不存在要更新值的键的情况:

Map("Jonathan" -> 350, "Bob" -> 65)
  .updatedWith("Mark")({ case None => Some(0) case Some(v) => Some(v - 50) })
// Map("Jonathan" -> 350, "Bob" -> 65, "Mark" -> 0)
Map("Mark" -> 100, "Jonathan" -> 350, "Bob" -> 65)
  .updatedWith("Mark")({ case None => Some(0) case Some(v) => Some(v - 50) })
// Map("Mark" -> 50, "Jonathan" -> 350, "Bob" -> 65)

Map("Jonathan" -> 350, "Bob" -> 65)
  .updatedWith("Mark")({ case None => None case Some(v) => Some(v - 50) })
// Map("Jonathan" -> 350, "Bob" -> 65)