Lisez-vous Scaladoc pour les méthodes de collecte «évidentes»? Ou pourquoi la paresse n'est pas toujours bonne

Si vous ne savez pas en quoi ils diffèrent


someMap.map{ case (k, v) => k -> foo(v)} 

et


 someMap.mapValues(foo) 

à l'exception de la syntaxe, ou si vous doutez / ne savez pas quelles sont les conséquences négatives de cette différence, et d'où vient l' identity , cet article est pour vous.


Sinon, participez à l'enquête située à la fin de l'article.


Commençons par un simple


Essayons bêtement de prendre un exemple situé avant le kat et voyons ce qui se passe:


 val someMap = Map("key1" -> "value1", "key2" -> "value2") def foo(value: String): String = value + "_changed" val resultMap = someMap.map{case (k,v) => k -> foo(v)} val resultMapValues = someMap.mapValues(foo) println(s"resultMap: $resultMap") println(s"resultMapValues: $resultMapValues") println(s"equality: ${resultMap == resultMapValues}") 

Ce code devrait s'imprimer


 resultMap: Map(key1 -> value1_changed, key2 -> value2_changed) resultMapValues: Map(key1 -> value1_changed, key2 -> value2_changed) equality: true 

Et à peu près à ce niveau, la compréhension de la méthode mapValues est mapValues dans les premières étapes de l'apprentissage de Scala: eh bien, oui, il existe une telle méthode, il est pratique de changer les valeurs dans Map lorsque les touches ne changent pas. Et vraiment, que penser d'autre? Par le nom de la méthode, tout est évident, le comportement est clair.


Exemples plus compliqués


Modifions un peu notre exemple (j'écrirai explicitement des types pour que vous ne pensiez pas qu'il y ait une sorte de moldu avec implicites):


 case class ValueHolder(value: String) val someMap: Map[String, String] = Map("key1" -> "value1", "key2" -> "value2") def foo(value: String): ValueHolder = ValueHolder(value) val resultMap: Map[String, ValueHolder] = someMap.map{case (k,v) => k -> foo(v)} val resultMapValues: Map[String, ValueHolder] = someMap.mapValues(foo) println(s"resultMap: $resultMap") println(s"resultMapValues: $resultMapValues") println(s"equality: ${resultMap == resultMapValues}") 

Et ce code produira après le lancement


 resultMap: Map(key1 -> ValueHolder(value1), key2 -> ValueHolder(value2)) resultMapValues: Map(key1 -> ValueHolder(value1), key2 -> ValueHolder(value2)) equality: true 

C'est assez logique et évident. "Mec, il est temps d'aller au bas de l'article!" - dira le lecteur. Faisons dépendre la création de notre classe de conditions externes et ajoutons quelques vérifications simples pour l'idiotie:


 case class ValueHolder(value: String, seed: Int) def foo(value: String): ValueHolder = ValueHolder(value, Random.nextInt()) ... println(s"simple assert for resultMap: ${resultMap.head == resultMap.head}") println(s"simple assert for resultMapValues: ${resultMapValues.head == resultMapValues.head}") 

En sortie on obtient:


 resultMap: Map(key1 -> ValueHolder(value1,1189482436), key2 -> ValueHolder(value2,-702760039)) resultMapValues: Map(key1 -> ValueHolder(value1,-1354493526), key2 -> ValueHolder(value2,-379389312)) equality: false simple assert for resultMap: true simple assert for resultMapValues: false 

Il est logique que les résultats ne soient plus égaux, mais aléatoires. Mais attendez, pourquoi la deuxième affirmation est-elle false ? Vraiment, les valeurs de resultMapValues ont changé, mais nous n'avons rien fait avec elles? Vérifions si tout à l'intérieur est le même:


 println(s"resultMapValues: $resultMapValues") println(s"resultMapValues: $resultMapValues") 

Et en sortie on obtient:


 resultMapValues: Map(key1 -> ValueHolder(value1,1771067356), key2 -> ValueHolder(value2,2034115276)) resultMapValues: Map(key1 -> ValueHolder(value1,-625731649), key2 -> ValueHolder(value2,-1815306407)) 

image


Pourquoi est-ce arrivé?


Pourquoi println change-t-il la valeur de Map ?
Il est temps d'entrer dans la documentation de la méthode mapValues :


  /** Transforms this map by applying a function to every retrieved value. * @param f the function used to transform values of this map. * @return a map view which maps every key of this map * to `f(this(key))`. The resulting map wraps the original map without copying any elements. */ 

La première ligne nous dit ce que nous pensions - cela change Map , en appliquant la fonction passée dans les arguments à chaque valeur. Mais si vous le lisez très attentivement et jusqu'au bout, il s'avère que ce n'est pas Map qui est retourné, mais "map view" (voir). Et ce n'est pas une vue normale ( View ), que vous pouvez obtenir en utilisant la méthode view et qui a une méthode de force pour un calcul explicite. Une classe spéciale (le code est de Scala version 2.12.7, mais pour 2.11 il y a presque la même chose):


 protected class MappedValues[W](f: V => W) extends AbstractMap[K, W] with DefaultMap[K, W] { override def foreach[U](g: ((K, W)) => U): Unit = for ((k, v) <- self) g((k, f(v))) def iterator = for ((k, v) <- self.iterator) yield (k, f(v)) override def size = self.size override def contains(key: K) = self.contains(key) def get(key: K) = self.get(key).map(f) } 

Si vous lisez ce code, vous verrez que rien n'est mis en cache et chaque fois que vous accédez aux valeurs, elles seront recalculées. Ce que nous observons dans notre exemple.


Si vous travaillez avec des fonctions pures et que tout est immuable, vous ne remarquerez aucune différence. Eh bien, peut-être que les performances diminueront. Mais, malheureusement, tout dans notre monde n'est pas propre et parfait, et en utilisant ces méthodes, vous pouvez marcher sur le râteau (ce qui s'est produit dans l'un de nos projets, sinon cet article n'aurait pas été le cas).


Bien sûr, nous ne sommes pas les premiers à tomber là-dessus. Déjà en 2011, un bug majeur a été ouvert à cette occasion (et au moment de la rédaction, il est marqué comme ouvert). Il mentionne également la méthode filterKeys , qui a exactement les mêmes problèmes, car elle est implémentée sur le même principe.


De plus, depuis 2015, un ticket a été suspendu pour ajouter des inspections à IntelliJ Idea.


Que faire


La solution la plus simple est de ne pas utiliser bêtement ces méthodes, car par leur nom, leur comportement, à mon avis, est très peu évident.


Une option légèrement meilleure consiste à appeler map(identity) .
identity , si quelqu'un ne le sait pas, il s'agit d'une fonction de la bibliothèque standard qui renvoie simplement son argument d'entrée. Dans ce cas, le travail principal est effectué par la méthode map , qui crée explicitement une Map normale. Vérifions juste au cas où:


 val resultMapValues: Map[String, ValueHolder] = someMap.mapValues(foo).map(identity) println(s"resultMapValues: $resultMapValues") println(s"simple assert for resultMapValues: ${resultMapValues.head == resultMapValues.head}") println(s"resultMapValues: $resultMapValues") println(s"resultMapValues: $resultMapValues") 

En sortie on obtient


 resultMapValues: Map(key1 -> ValueHolder(value1,333546604), key2 -> ValueHolder(value2,228749608)) simple assert for resultMapValues: true resultMapValues: Map(key1 -> ValueHolder(value1,333546604), key2 -> ValueHolder(value2,228749608)) resultMapValues: Map(key1 -> ValueHolder(value1,333546604), key2 -> ValueHolder(value2,228749608)) 

Tout va bien :)


Si vous voulez toujours quitter la paresse, il vaut mieux changer le code pour qu'il soit évident. Vous pouvez créer une classe implicite avec une méthode wrapper pour mapValues et filterKeys , qui donne un nouveau nom qui leur est compréhensible. Ou utilisez explicitement .view et travaillez avec un itérateur de paires.


De plus, il vaut la peine d'ajouter une inspection à l'environnement / règle de développement dans un analyseur statique / ailleurs, ce qui met en garde contre l'utilisation de ces méthodes. Parce qu'il vaut mieux y consacrer un peu de temps maintenant que de monter sur le râteau et d'en tirer les conséquences longtemps plus tard.


Sinon, comment pouvez-vous marcher sur le râteau et comment nous avons marché dessus


En plus du cas de la dépendance aux conditions externes, que nous avons observé dans les exemples ci-dessus, il existe d'autres options.


Par exemple, une valeur mutable (attention, ici d'un coup d'œil superficiel tout est "immuable"):


 val someMap1 = Map("key1" -> new AtomicInteger(0), "key2" -> new AtomicInteger(0)) val someMap2 = Map("key1" -> new AtomicInteger(0), "key2" -> new AtomicInteger(0)) def increment(value: AtomicInteger): Int = value.incrementAndGet() val resultMap: Map[String, Int] = someMap1.map { case (k, v) => k -> increment(v) } val resultMapValues: Map[String, Int] = someMap2.mapValues(increment) println(s"resultMap (1): $resultMap") println(s"resultMapValues (1): $resultMapValues") println(s"resultMap (2): $resultMap") println(s"resultMapValues (2): $resultMapValues") 

Ce code produira ce résultat:


 resultMap (1): Map(key1 -> 1, key2 -> 1) resultMapValues (1): Map(key1 -> 1, key2 -> 1) resultMap (2): Map(key1 -> 1, key2 -> 1) resultMapValues (2): Map(key1 -> 2, key2 -> 2) 

Lorsque j'ai accédé à nouveau à someMap2 , someMap2 obtenu un résultat amusant.


Aux problèmes qui peuvent survenir lorsque les mapValues et filterKeys sont utilisées de manière irréfléchie, filterKeys pouvez ajouter une diminution des performances, une augmentation de la consommation de mémoire et / ou une augmentation de la charge sur le GC, mais cela dépend plus de cas spécifiques et peut ne pas être aussi critique.


Vous devez également ajouter la méthode toSeq de l'itérateur à la tirelire de râteaux similaires, qui renvoie un Stream paresseux.


Nous mapValues sur mapValues accident. Il a été utilisé dans une méthode qui, à l'aide de la réflexion, a créé un ensemble de gestionnaires à partir de la configuration: les clés étaient les identifiants des gestionnaires, et la valeur était leurs paramètres, qui ont ensuite été convertis en gestionnaires eux-mêmes (une instance de classe a été créée). Étant donné que les gestionnaires ne comprenaient que des fonctions pures, tout fonctionnait sans problème, même de manière notable, n'affectait pas les performances (après avoir pris le râteau, nous avons pris des mesures).


Mais une fois, j'ai dû créer un sémaphore dans l'un des gestionnaires afin qu'un seul gestionnaire exécute une fonction lourde, dont le résultat est mis en cache et utilisé par d'autres gestionnaires. Et puis des problèmes ont commencé sur l'environnement de test - un code valide qui fonctionnait bien localement a commencé à planter en raison de problèmes avec le sémaphore. Bien sûr, la première pensée de l'inopérabilité de la nouvelle fonctionnalité est que les problèmes lui sont associés. Nous avons longuement réfléchi à cela jusqu'à ce que nous arrivions à la conclusion "un jeu, pourquoi différentes instances de gestionnaires sont-elles utilisées?" et ce n'est que sur la trace de la pile qu'ils nous ont trouvés pour monter des mapValues .


Si vous travaillez avec Apache Spark, vous pouvez tomber sur un problème similaire lorsque vous trouvez soudain que pour un morceau de code élémentaire avec .mapValues vous pouvez attraper


 java.io.NotSerializableException: scala.collection.immutable.MapLike$$anon$2 

https://stackoverflow.com/questions/32900862/map-can-not-be-serializable-in-scala
https://issues.scala-lang.org/browse/SI-7005
Mais la map(identity) résout le problème, et généralement il n'y a pas de motivation / temps pour creuser plus profondément.


Conclusion


Les erreurs peuvent se cacher dans les endroits les plus inattendus, même dans les méthodes qui semblent évidentes à 100%. Plus précisément, ce problème, à mon avis, est associé à un nom de méthode médiocre et à un type de retour insuffisamment strict.


Bien sûr, il est important d'étudier la documentation de toutes les méthodes utilisées dans la bibliothèque standard, mais ce n'est pas toujours évident et, franchement, il n'y a pas toujours assez de motivation pour lire sur les «choses évidentes».


L'informatique paresseuse seule est une plaisanterie cool, et l'article ne les encourage en aucune façon à abandonner. Cependant, lorsque la paresse n'est pas évidente - cela peut entraîner des problèmes.


En toute honnêteté, le problème avec mapValues est déjà apparu sur Habré en traduction, mais personnellement, cet article que j'avais très mal mis en tête, car il y avait beaucoup de choses déjà connues / basiques et il n'était pas tout à fait clair quel était le danger d'utiliser ces fonctions:


La méthode filterKeys encapsule la table source sans copier aucun élément. Il n'y a rien de mal à cela, mais vous vous attendez à peine ce comportement de filterKeys

Autrement dit, il y a une remarque sur un comportement inattendu, et qu'en même temps, vous pouvez également marcher un peu sur le râteau, apparemment, il est considéré comme peu probable.


→ Tout le code de l'article est dans ce sens

Source: https://habr.com/ru/post/fr430538/


All Articles