Scala 基于以前的值更新映射中的值的惯用方法

Scala 基于以前的值更新映射中的值的惯用方法,scala,map,immutability,Scala,Map,Immutability,假设我将银行账户信息存储在一个不可变的映射中: val m = Map("Mark" -> 100, "Jonathan" -> 350, "Bob" -> 65) 我想从马克的账户上取50美元。我可以这样做: val m2 = m + ("Mark" -> (m("Mark") - 50)) 但我觉得这个代码很难看。有没有更好的方法写这个 不幸的是,MapAPI中没有adjust。我有时会使用如下函数(以Haskell为模型,参数顺序不同): 现在adjust(m,

假设我将银行账户信息存储在一个不可变的
映射中

val m = Map("Mark" -> 100, "Jonathan" -> 350, "Bob" -> 65)
我想从马克的账户上取50美元。我可以这样做:

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

但我觉得这个代码很难看。有没有更好的方法写这个

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

现在
adjust(m,“Mark”)(-50)
做你想做的事。如果您确实想要更干净的东西,您还可以使用来获得更自然的
m.adjust(“Mark”)(-50)
语法

(请注意,如果
k
不在映射中,上面的简短版本会引发异常,这与Haskell行为不同,可能是您希望在真实代码中修复的情况。)

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

首先,一个简单的
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
}
接下来,一个智能构造器将镜头从“较大结构”映射到“较小部分”
选项[B]
。我们通过提供一个特定的键来指示要查看的“较小部分”。(灵感来源于我的记忆):

现在可以编写代码:

val m2 = containsKey("Mark").mod(m)(_.map(_ - 50))
n、 b.我实际上改变了我偷来的答案的
mod
,这样它就接受了它的输入。这有助于避免额外的类型注释。还要注意
\uu.map
,因为请记住,我们的镜头是从
映射[A,B]
选项[B]
。这意味着如果地图不包含键
“Mark”
,则地图将保持不变。否则,此解决方案与Travis提出的
adjust
解决方案非常相似。

An提出了另一种替代方案,使用scalaz的
+
操作符

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

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

启动
Scala 2.13
,用于此确切目的:

// 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)
或者以更紧凑的形式:

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

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

def更新为[V1>:V](键:K)(重新映射函数:(选项[V])=>选项[V1]):映射[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)

这仅在映射中存在密钥时有效。考虑<代码> map。获取(k)。折叠(map)(b= > map(更新)(k,f(b))< /C>如果你想忽略一个丢失的键,或者一个有<代码> f的选项:选项[b]=b/代码>如果你想在它不存在的时候设置键。@ AdnnFISH:调整(M,马克)((.GETORSER(0)-50))非常有用-我希望<代码>调整< /C> >在标准库中!(可能是@adamnfish的折叠变体…)这似乎是给定scala 2.13中最正确的解决方案+
map.updatedWith("Mark")(_.map(_ - 50))
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)