Lesen Sie Scaladoc für „offensichtliche“ Erfassungsmethoden? Oder warum Faulheit nicht immer gut ist

Wenn Sie nicht wissen, wie sie sich unterscheiden


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

und


 someMap.mapValues(foo) 

Mit Ausnahme der Syntax oder wenn Sie bezweifeln / nicht wissen, zu welchen schlimmen Konsequenzen dieser Unterschied führen kann und wo die identity , ist dieser Artikel für Sie.


Nehmen Sie andernfalls an der Umfrage am Ende des Artikels teil.


Beginnen wir mit einem einfachen


Versuchen wir dumm, ein Beispiel vor dem Kat zu nehmen und zu sehen, was passiert:


 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}") 

Dieser Code wird voraussichtlich gedruckt


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

Ungefähr auf dieser Ebene mapValues das Verständnis der mapValues Methode in den frühen Phasen des Lernens von Scala: Ja, es gibt eine solche Methode. Es ist praktisch, Werte in Map zu ändern, wenn sich die Schlüssel nicht ändern. Und wirklich, was gibt es sonst noch zu denken? Beim Namen der Methode ist alles offensichtlich, das Verhalten ist klar.


Kompliziertere Beispiele


Lassen Sie uns unser Beispiel ein wenig modifizieren (ich werde explizit Typen schreiben, damit Sie nicht glauben, dass es eine Art Muggel mit Implikationen gibt):


 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}") 

Und solcher Code wird nach dem Start erzeugt


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

Es ist ziemlich logisch und offensichtlich. "Alter, es ist Zeit, dem Artikel auf den Grund zu gehen!" - Der Leser wird sagen. Lassen Sie uns die Schaffung unserer Klasse von äußeren Bedingungen abhängig machen und ein paar einfache Überprüfungen auf Idiotie hinzufügen:


 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}") 

Am Ausgang erhalten wir:


 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 

Es ist logisch, dass die Ergebnisse jetzt nicht gleich, sondern zufällig sind. Aber warte, warum hat die zweite Behauptung false ? Die Werte in resultMapValues haben sich wirklich geändert, aber wir haben nichts damit gemacht? Lassen Sie uns überprüfen, ob alles im Inneren so ist wie es war:


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

Und am Ausgang bekommen wir:


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

Bild


Warum ist das passiert?


Warum println den Wert von Map ?
Es ist Zeit, sich bereits mit der Dokumentation der mapValues Methode zu 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. */ 

Die erste Zeile sagt uns, was wir gedacht haben - sie ändert Map und wendet die in den Argumenten übergebene Funktion auf jeden Wert an. Wenn Sie es jedoch sehr sorgfältig und bis zum Ende lesen, stellt sich heraus, dass nicht die Map zurückgegeben wird, sondern die "Kartenansicht" (Ansicht). Und dies ist keine normale Ansicht ( View ), die Sie mit der view Methode erhalten können und die eine force Methode zur expliziten Berechnung hat. Eine spezielle Klasse (der Code stammt aus Scala Version 2.12.7, aber für 2.11 gibt es fast dasselbe):


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

Wenn Sie diesen Code lesen, sehen Sie, dass nichts zwischengespeichert wird, und jedes Mal, wenn Sie auf die Werte zugreifen, werden sie neu berechnet. Was wir in unserem Beispiel beobachten.


Wenn Sie mit reinen Funktionen arbeiten und alles unveränderlich ist, werden Sie keinen Unterschied bemerken. Nun, vielleicht wird die Leistung nachlassen. Leider ist nicht alles in unserer Welt sauber und perfekt, und mit diesen Methoden können Sie auf den Rechen treten (was in einem unserer Projekte passiert ist, sonst wäre dieser Artikel nicht gewesen).


Natürlich sind wir nicht die Ersten, die darauf stoßen. Bereits 2011 wurde bei dieser Gelegenheit ein schwerwiegender Fehler behoben (und zum Zeitpunkt des Schreibens ist er als offen markiert). Es wird auch die filterKeys Methode erwähnt, die genau dieselben Probleme aufweist, da sie nach demselben Prinzip implementiert ist.


Darüber hinaus hängt seit 2015 ein Ticket , um Inspektionen zu IntelliJ Idea hinzuzufügen.


Was zu tun ist?


Die einfachste Lösung ist, diese Methoden dumm nicht anzuwenden, weil namentlich ist ihr Verhalten meiner Meinung nach sehr offensichtlich.


Eine etwas bessere Option ist das Aufrufen der map(identity) .
identity , wenn jemand nicht weiß, ist dies eine Funktion aus der Standardbibliothek, die einfach ihr Eingabeargument zurückgibt. In diesem Fall wird die Hauptarbeit von der map erledigt, die explizit eine normale Map . Lassen Sie uns nur für den Fall überprüfen:


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

Am Ausgang bekommen wir


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

Alles ist gut :)


Wenn Sie immer noch Faulheit hinterlassen möchten, ist es besser, den Code so zu ändern, dass er offensichtlich ist. Sie können eine implizite Klasse mit einer Wrapper-Methode für mapValues und filterKeys , die einen neuen Namen filterKeys , der für sie verständlich ist. Oder verwenden Sie explizit .view und arbeiten Sie mit einem Iterator von Paaren.


Darüber hinaus lohnt es sich, die Entwicklungsumgebung / -regel in einem statischen Analysegerät / an einer anderen Stelle zu überprüfen, um vor der Verwendung dieser Methoden zu warnen. Weil es besser ist, jetzt ein wenig Zeit damit zu verbringen, als auf den Rechen zu treten und die Konsequenzen für eine lange Zeit später zu harken.


Wie sonst können Sie auf den Rechen treten und wie sind wir auf sie getreten?


Neben dem Fall mit der Abhängigkeit von äußeren Bedingungen, den wir in den obigen Beispielen beobachtet haben, gibt es andere Optionen.


Zum Beispiel ein veränderlicher Wert (beachten Sie, hier ist auf einen oberflächlichen Blick alles "unveränderlich"):


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

Dieser Code erzeugt dieses Ergebnis:


 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) 

Als ich wieder auf someMap2 zugegriffen someMap2 , haben someMap2 ein lustiges Ergebnis someMap2 .


Die Probleme, die auftreten können, wenn mapValues und filterKeys mapValues verwendet werden, können zu Leistungseinbußen, erhöhtem Speicherverbrauch und / oder erhöhter Belastung des GC hinzugefügt werden. Dies hängt jedoch stärker von bestimmten Fällen ab und ist möglicherweise nicht so kritisch.


Sie sollten auch die toSeq Methode des Iterators zum Sparschwein ähnlicher Rechen hinzufügen, wodurch ein fauler Stream .


Wir mapValues versehentlich auf mapValues . Es wurde in einer Methode verwendet, die mithilfe von Reflection eine Reihe von Handlern aus der Konfiguration erstellte: Die Schlüssel waren die Bezeichner der Handler, und der Wert waren ihre Einstellungen, die dann in die Handler selbst konvertiert wurden (Klasseninstanz wurde erstellt). Da die Handler nur aus reinen Funktionen bestanden, funktionierte alles problemlos, hatte aber auch keinen merklichen Einfluss auf die Leistung (nach dem Rechen nahmen wir Messungen vor).


Aber einmal musste ich in einem der Handler ein Semaphor erstellen, damit nur ein Handler eine schwere Funktion ausführt, deren Ergebnis zwischengespeichert und von anderen Handlern verwendet wird. Und dann begannen Probleme in der Testumgebung - gültiger Code, der lokal gut funktionierte, stürzte aufgrund von Problemen mit dem Semaphor ab. Der erste Gedanke bei der Inoperabilität der neuen Funktionalität ist natürlich, dass die Probleme damit verbunden sind. Wir haben uns lange damit beschäftigt, bis wir zu dem Schluss kamen, dass "ein Spiel, warum werden verschiedene Instanzen von Handlern verwendet?" und nur auf der Stapelspur fanden sie, dass wir mapValues .


Wenn Sie mit Apache Spark arbeiten, können Sie auf ein ähnliches Problem .mapValues wenn Sie plötzlich feststellen, dass Sie einen elementaren Code mit .mapValues abfangen können


 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
Aber map(identity) löst das Problem, und normalerweise gibt es keine Motivation / Zeit, tiefer zu graben.


Fazit


Fehler können an den unerwartetsten Stellen lauern - selbst bei Methoden, die zu 100% offensichtlich erscheinen. Insbesondere ist dieses Problem meiner Meinung nach mit einem schlechten Methodennamen und einem unzureichend strengen Rückgabetyp verbunden.


Natürlich ist es wichtig, die Dokumentation für alle in der Standardbibliothek verwendeten Methoden zu studieren, aber es ist nicht immer offensichtlich und ehrlich gesagt gibt es nicht immer genug Motivation, um über die "offensichtlichen Dinge" zu lesen.


Lazy Computing allein ist ein cooler Witz, und der Artikel ermutigt sie in keiner Weise, aufzugeben. Wenn jedoch Faulheit nicht offensichtlich ist, kann dies zu Problemen führen.


mapValues erschien das Problem mit mapValues bereits bei Habré in der Übersetzung, aber persönlich war dieser Artikel, den ich sehr schlecht in meinen Kopf gesetzt hatte, weil Es gab viele bereits bekannte / grundlegende Dinge und es war nicht ganz klar, wie gefährlich die Verwendung dieser Funktionen sein könnte:


Die filterKeys-Methode umschließt die Quelltabelle, ohne Elemente zu kopieren. Daran ist nichts auszusetzen, aber Sie erwarten dieses Verhalten kaum von filterKeys

Das heißt, es gibt eine Bemerkung über unerwartetes Verhalten, und gleichzeitig können Sie auch ein wenig auf den Rechen treten, anscheinend wird dies als unwahrscheinlich angesehen.


→ Der gesamte Code aus dem Artikel befindet sich in dieser Liste

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


All Articles