web-dev-qa-db-fra.com

Manière idiomatique de mettre à jour la valeur dans une carte basée sur la valeur précédente

Supposons que je stocke les informations des comptes bancaires dans un Map immuable:

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

et je veux retirer, disons, 50 $ du compte de Mark. Je peux le faire comme suit:

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

Mais ce code me semble moche. Y a-t-il une meilleure façon d'écrire cela?

44
ffriend

Il n'y a malheureusement pas de adjust dans l'API Map. J'ai parfois utilisé une fonction comme la suivante (sur le modèle de Haskell Data.Map.adjust , avec un ordre d'arguments différent):

def adjust[A, B](m: Map[A, B], k: A)(f: B => B) = m.updated(k, f(m(k)))

Maintenant, adjust(m, "Mark")(_ - 50) fait ce que vous voulez. Vous pouvez également utiliser le modèle pimp-my-library pour obtenir la syntaxe m.adjust("Mark")(_ - 50) la plus naturelle, si vous voulez vraiment quelque chose de plus propre.

(Notez que la version courte ci-dessus lève une exception si k n'est pas dans la carte, ce qui est différent du comportement de Haskell et probablement quelque chose que vous voudriez corriger en code réel.)

36
Travis Brown

Cela pourrait être fait avec des lentilles . L'idée même d'un objectif est de pouvoir zoomer sur une partie particulière d'une structure immuable et de pouvoir 1) récupérer la plus petite partie d'une structure plus grande, ou 2) créer une nouvelle structure plus grande avec une partie plus petite modifiée . Dans ce cas, ce que vous désirez est le n ° 2.

Tout d'abord, une implémentation simple de Lens, volé à cette réponse , volé à 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
}

Ensuite, un constructeur intelligent pour créer une lentille à partir d'une "structure plus grande" Map[A,B] à "partie plus petite" Option[B]. Nous indiquons quelle "partie plus petite" nous voulons examiner en fournissant une clé particulière. (Inspiré de ce dont je me souviens Présentation d'Edward Kmett sur les lentilles en 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)
  }
)

Maintenant, votre code peut être écrit:

val m2 = containsKey("Mark").mod(m)(_.map(_ - 50))

n.b. J'ai en fait changé mod de la réponse dont je l'ai volé pour qu'il prenne ses entrées au curry. Cela permet d'éviter les annotations de type supplémentaires. Notez également _.map, car rappelez-vous, notre objectif est de Map[A,B] à Option[B]. Cela signifie que la carte sera inchangée si elle ne contient pas la clé "Mark". Sinon, cette solution finit par être très similaire à la solution adjust présentée par Travis.

12
Dan Burton

Une réponse SO propose une autre alternative, en utilisant l'opérateur |+| De scalaz

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

L'opérateur |+| Additionnera les valeurs d'une clé existante ou insérera la valeur sous une nouvelle clé.

9
mucaho

Le démarrage de Scala 2.13, Map#updatedWith sert exactement ce but:

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

ou sous une forme plus compacte:

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

Notez que (en citant doc ) si la fonction de remappage renvoie Some(v), le mappage est mis à jour avec la nouvelle valeur v. Si la fonction de remappage renvoie None, le mappage est supprimé (ou reste absent s'il est initialement absent).

def updatedWith [V1>: V] (key: K) (remappingFunction: (Option [V]) => Option [V1]): Map [K, V1]

De cette façon, nous pouvons gérer avec élégance les cas où la clé pour laquelle mettre à jour la valeur n'existe pas:

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