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

ffr*_*end 43 scala map immutability

假设我将银行账户信息存储在一个不可变的Map:

val m = Map("Mark" -> 100, "Jonathan" -> 350, "Bob" -> 65)
Run Code Online (Sandbox Code Playgroud)

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

val m2 = m + ("Mark" -> (m("Mark") - 50))
Run Code Online (Sandbox Code Playgroud)

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

Tra*_*own 36

遗憾的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)))
Run Code Online (Sandbox Code Playgroud)

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

(请注意,如果k不在映射中,则上面的简短版本会抛出异常,这与Haskell行为不同,可能是您想要在实际代码中修复的内容.)

  • 仅当键存在于地图中时,此方法才有效.考虑`map.get(k).fold(map)(b => map.updated(k,f(b)))`如果你想忽略一个缺失的键,或者一个方法有`f:Option [B ] => B`如果您希望能够在缺席时设置密钥. (3认同)

Dan*_*ton 12

这可以用镜头完成.镜头的想法是能够放大不可变结构的特定部分,并且能够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
}
Run Code Online (Sandbox Code Playgroud)

接下来,一个智能构造器,用于创建从"较大结构" 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)
  }
)
Run Code Online (Sandbox Code Playgroud)

现在您的代码可以编写:

val m2 = containsKey("Mark").mod(m)(_.map(_ - 50))
Run Code Online (Sandbox Code Playgroud)

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


muc*_*aho 9

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

val m2 = m |+| Map("Mark" -> -50)
Run Code Online (Sandbox Code Playgroud)

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


Xav*_*hot 9

开始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)
Run Code Online (Sandbox Code Playgroud)

或更紧凑的形式:

map.updatedWith("Mark")(_.map(_ - 50))
Run Code Online (Sandbox Code Playgroud)

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

def updatedWith[V1 >: V](key: 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)
Run Code Online (Sandbox Code Playgroud)