【问题标题】:Idiomatic way to update value in a Map based on previous value根据先前值更新 Map 中的值的惯用方法
【发布时间】:2012-02-18 16:56:05
【问题描述】:

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

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

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

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

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

【问题讨论】:

    标签: scala map immutability


    【解决方案1】:

    很遗憾,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) 语法。

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

    【讨论】:

    • 这仅适用于地图中存在密钥的情况。如果您想忽略丢失的密钥,请考虑 map.get(k).fold(map)(b => map.updated(k, f(b))),或者如果您希望能够在缺少密钥时设置密钥,请考虑使用具有 f: Option[B] => B 的方法。
    • @adamnfish: adjust(m, "Mark")(_.getOrElse(0) - 50)
    • 非常有用 - 我希望 adjust 在标准库中! (可能是@adamnfish 的折叠变体..)
    【解决方案2】:

    这可以通过镜头来完成。镜头的真正想法是能够放大不可变结构的特定部分,并且能够 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))
    

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

    【讨论】:

      【解决方案3】:

      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)
      

      或更紧凑的形式:

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

      请注意(引用doc)如果重映射函数返回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)
      

      【讨论】:

      • 这似乎是给定 scala 2.13+ 中最正确的解决方案
      【解决方案4】:

      SO Answer 提出了另一种选择,使用来自 scalaz 的 |+| 运算符

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-05-20
        • 1970-01-01
        • 1970-01-01
        • 2011-01-29
        • 2022-10-13
        相关资源
        最近更新 更多