web-dev-qa-db-ja.com

以前の値に基づいてマップの値を更新する慣用的な方法

銀行口座情報を不変のMapに保存するとします。

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

マークのアカウントから$ 50を引き出したいと思います。私は次のようにそれを行うことができます:

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

しかし、このコードは私には醜いようです。これを書くより良い方法はありますか?

44
ffriend

残念ながら、adjust AP​​IにはMapはありません。私は時々次のような関数を使用しました(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の動作とは異なり、おそらく実際のコードで修正したいものです。)

36
Travis Brown

これはlensesで実行できます。レンズのまさにそのアイデアは、不変の構造の特定の部分にズームインして、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
}

次に、「大きな構造」Map[A,B]から「小さな部分」Option[B]にレンズを作成するためのスマートコンストラクター。特定のキーを提供することにより、どの「小さい部分」を見たいかを示します。 (私が覚えていることに触発 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ソリューションと非常によく似たものになります。

12
Dan Burton

SO Answer は、scalazの|+|演算子を使用して、別の代替案を提案します

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

|+|演算子は、既存のキーの値を合計するか、新しいキーの下に値を挿入します。

9
mucaho

_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))
_

(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)
_
3
Xavier Guihot