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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/9003874/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-22 03:49:22  来源:igfitidea点击:

Idiomatic way to update value in a Map based on previous value

scalamapimmutability

提问by ffriend

Let's say I store bank accounts information in an immutable Map:

假设我将银行帐户信息存储在一个不可变的文件中Map

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

and I want to withdraw, say, $50 from Mark's account. I can do it as follows:

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

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

But this code seems ugly to me. Is there better way to write this?

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

采纳答案by Travis Brown

There's no adjustin the MapAPI, unfortunately. I've sometimes used a function like the following (modeled on Haskell's Data.Map.adjust, with a different order of arguments):

不幸的adjust是,MapAPI 中没有。我有时会使用如下函数(以 Haskell 为模型Data.Map.adjust,参数顺序不同):

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

Now adjust(m, "Mark")(_ - 50)does what you want. You could also use the pimp-my-library patternto get the more natural m.adjust("Mark")(_ - 50)syntax, if you really wanted something cleaner.

现在adjust(m, "Mark")(_ - 50)做你想做的。如果你真的想要更简洁的东西,你也可以使用pimp-my-library 模式来获得更自然的m.adjust("Mark")(_ - 50)语法。

(Note that the short version above throws an exception if kisn't in the map, which is different from the Haskell behavior and probably something you'd want to fix in real code.)

(请注意,如果k地图中没有,上面的简短版本会抛出异常,这与 Haskell 行为不同,并且可能是您想要在实际代码中修复的问题。)

回答by Dan Burton

This could be done with lenses. The very idea of a lens is to be able to zoom in on a particular part of an immutable structure, and be able to 1) retrieve the smaller part from a larger structure, or 2) create a new larger structure with a modified smaller part. In this case, what you desire is #2.

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

Firstly, a simple implementation of Lens, stolen from this answer, stolen from scalaz:

首先,一个简单的实现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
}

Next, a smart constructor to create a lens from "larger structure" Map[A,B]to "smaller part" Option[B]. We indicate which "smaller part" we want to look at by providing a particular key. (Inspired by what I remember from Edward Kmett's presentation on Lenses in Scala):

接下来,一个聪明的构造函数来创建一个从“较大结构”Map[A,B]到“较小部分”的镜头Option[B]。我们通过提供一个特定的键来指示我们想要查看哪个“较小的部分”。(灵感来自我记得Edward Kmett 关于 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)
  }
)

Now your code can be written:

现在你的代码可以写成:

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

n.b. I actually changed modfrom the answer I stole it from so that it takes its inputs curried. This helps to avoid extra type annotations. Also notice _.map, because remember, our lens is from Map[A,B]to Option[B]. This means the map will be unchanged if it does not contain the key "Mark". Otherwise, this solution ends up being very similar to the adjustsolution presented by Travis.

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

回答by mucaho

An SO Answerproposes another alternative, using the |+|operator from scalaz

一个SO答案提出了另一种选择,使用|+|从scalaz运营商

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

The |+|operator will sum the values of an existing key, or insert the value under a new key.

|+|运营商将值相加现有密钥的,或在一个新的密钥插入值。

回答by Xavier Guihot

Starting Scala 2.13, Map#updatedWithserves this exact purpose:

开始Scala 2.13Map#updatedWith服务于这个确切的目的:

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

or in a more compact form:

或更紧凑的形式:

map.updatedWith("Mark")(_.map(_ - 50))


Note that (quoting the doc) if the remapping function returns Some(v), the mapping is updated with the new value v. If the remapping function returns None, the mapping is removed (or remains absent if initially absent).

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

def updatedWith[V1 >: V](key: K)(remappingFunction: (Option[V]) => Option[V1]): Map[K, V1]

def updatedWith[V1 >: V](key: K)(remappingFunction: (Option[V]) => Option[V1]): Map[K, V1]

This way, we can elegantly handle cases where the key for which to update the value doesn't exist:

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

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)