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

bksxznpy  于 2023-08-05  发布在  Scala
关注(0)|答案(4)|浏览(143)

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

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

字符串
我想从马克的账户里取50美元我可以这样做:

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


但这段代码看起来很丑。有没有更好的方式来写这个?

7lrncoxx

7lrncoxx1#

不幸的是,Map API中没有adjust。我有时会使用如下函数(以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)语法。
(Note如果k不在map中,上面的简短版本会抛出一个异常,这与Haskell的行为不同,可能是你想在真实的代码中修复的。

f1tvaqid

f1tvaqid2#

Scala 2.13开始,Map#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)

字符串
或者以更紧凑的形式:

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


请注意(引用文档)如果重Map函数返回Some(v),则Map将更新为新值v。如果重Map函数返回None,则Map被移除(或者如果最初不存在,则保持不存在)。
def updatedWith[V1 >:V](键:K)(重新Map功能:(选项[V])=>选项[VI]):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)

nom7f22z

nom7f22z3#

这可以用 lenses 来实现。透镜的理念是能够放大不可变结构的特定部分,并且能够1)从较大结构中检索较小部分,或者2)用修改的较小部分创建新的较大结构。在这种情况下,你想要的是#2。
首先,一个简单的Lens的实现,它是从this answer,从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))


注意:我实际上改变了mod从答案,我偷了它,使它采取其输入curried。这有助于避免额外的类型注解。还要注意_.map,因为记住,我们的透镜是从Map[A,B]Option[B]。这意味着如果Map不包含键"Mark",则它将保持不变。否则,这个解决方案最终将非常类似于Travis提出的adjust解决方案。

yrefmtwq

yrefmtwq4#

SO Answer提出了另一种替代方案,使用scalaz中的|+|运算符

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

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

相关问题