¿Lees Scaladoc para métodos de recolección "obvios"? O por qué la pereza no siempre es buena

Si no sabes cómo difieren


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

y


 someMap.mapValues(foo) 

a excepción de la sintaxis, o si duda / no sabe qué consecuencias negativas puede acarrear esta diferencia y dónde se identity , este artículo es para usted.


De lo contrario, participe en la encuesta que se encuentra al final del artículo.


Comencemos con un simple


Intentemos estúpidamente tomar un ejemplo ubicado antes del kat y ver qué sucede:


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

Se espera que este código se imprima


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

Y aproximadamente a este nivel, la comprensión del método mapValues se mapValues en las primeras etapas del aprendizaje de Scala: bueno, sí, existe un método así, es conveniente cambiar los valores en Map cuando las teclas no cambian. Y realmente, ¿qué más hay para pensar? Por el nombre del método, todo es obvio, el comportamiento es claro.


Ejemplos más complicados


Modifiquemos un poco nuestro ejemplo (escribiré explícitamente tipos para que no pienses que hay algún tipo de muggle con implicidades):


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

Y dicho código se producirá después del lanzamiento


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

Es bastante lógico y obvio. "¡Amigo, es hora de llegar al final del artículo!" - dirá el lector. Hagamos que la creación de nuestra clase dependa de condiciones externas y agreguemos un par de simples comprobaciones de idiotez:


 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 la salida obtenemos:


 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 lógico que los resultados ahora no sean iguales, sino aleatorios. Pero espera, ¿por qué la segunda afirmación dio false ? ¿Realmente los valores en resultMapValues han cambiado, pero no hicimos nada con ellos? Vamos a ver si todo lo que hay dentro es igual que antes:


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

Y a la salida obtenemos:


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

imagen


¿Por qué sucedió esto?


¿ println qué println cambia el valor de Map ?
Ya es hora de entrar en la documentación del método 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 primera línea nos dice lo que pensamos: cambia Map , aplicando la función pasada en los argumentos a cada valor. Pero si lo lee con cuidado y hasta el final, resulta que no se devuelve el Map , sino la "vista de mapa" (ver). Y esta no es una vista normal ( View ), que puede obtener usando el método de view y que tiene un método de force para el cálculo explícito. Una clase especial (el código es de Scala versión 2.12.7, pero para 2.11 hay casi lo mismo):


 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 lee este código, verá que no hay nada en la memoria caché, y cada vez que acceda a los valores, se volverán a calcular. Lo que observamos en nuestro ejemplo.


Si trabaja con funciones puras y todo es inmutable, no notará ninguna diferencia. Bueno, tal vez el rendimiento se agotará. Pero, desafortunadamente, no todo en nuestro mundo es limpio y perfecto, y utilizando estos métodos, puede pisar el rastrillo (lo que sucedió en uno de nuestros proyectos, de lo contrario, este artículo no lo habría sido).


Por supuesto, no somos los primeros en encontrar esto. Ya en 2011, se abrió un error importante en esta ocasión (y al momento de escribir, está marcado como abierto). También menciona el método filterKeys , que tiene exactamente los mismos problemas, porque se implementa con el mismo principio.


Además, desde 2015, se ha colgado un boleto para agregar inspecciones a IntelliJ Idea.


Que hacer


La solución más simple es no usar estúpidamente estos métodos, porque Por su nombre, su comportamiento, en mi opinión, es muy obvio.


Una opción un poco mejor es llamar al map(identity) .
identity , si alguien no lo sabe, esta es una función de la biblioteca estándar que simplemente devuelve su argumento de entrada. En este caso, el trabajo principal se realiza mediante el método de map , que crea explícitamente un Map normal. Vamos a ver por si acaso:


 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 la salida obtenemos


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

Todo esta bien :)


Si aún quiere dejar la pereza, es mejor cambiar el código para que sea obvio. Puede crear una clase implícita con un método de contenedor para mapValues y filterKeys , que les da un nuevo nombre que les es comprensible. O use explícitamente .view y trabaje con un iterador de pares.


Además, vale la pena agregar una inspección al entorno / regla de desarrollo en un analizador estático / en otro lugar, que advierte sobre el uso de estos métodos. Porque es mejor pasar un poco de tiempo ahora que pisar el rastrillo y rastrear las consecuencias por mucho tiempo más tarde.


¿De qué otra manera puedes pisar el rastrillo y cómo los pisamos?


Además del caso con la dependencia de condiciones externas, que observamos en los ejemplos anteriores, hay otras opciones.


Por ejemplo, un valor mutable (nota, aquí en una mirada superficial todo es "inmutable"):


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

Este código producirá este resultado:


 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) 

Cuando volví a acceder a someMap2 , obtuvimos un resultado divertido.


A los problemas que pueden surgir cuando se mapValues y filterKeys , puede agregar una disminución en el rendimiento, un aumento en el consumo de memoria y / o un aumento en la carga en el GC, pero esto depende más de casos específicos y puede no ser tan crítico.


También debe agregar el método toSeq del iterador a la alcancía de rastrillos similares, que devuelve un Stream lento.


mapValues en mapValues accidente. Se usó en un método que, usando la reflexión, creó un conjunto de manejadores a partir de la configuración: las claves eran los identificadores de los manejadores, y el valor era su configuración, que luego se convirtió en los mismos manejadores (se creó la instancia de clase). Dado que los controladores consistían solo en funciones puras, todo funcionó sin problemas, incluso notablemente no afectó el rendimiento (después de tomar el rastrillo, tomamos medidas).


Pero una vez que tuve que hacer un semáforo en uno de los controladores para que solo un controlador realizara una función pesada, el resultado del cual es almacenado en caché y utilizado por otros controladores. Y luego comenzaron los problemas en el entorno de prueba: el código válido que funcionaba bien localmente comenzó a fallar debido a problemas con el semáforo. Por supuesto, lo primero que se piensa con la inoperancia de la nueva funcionalidad es que los problemas están asociados con ella misma. Estuvimos jugando con esto durante mucho tiempo hasta que llegamos a la conclusión de "algún juego, ¿por qué se usan diferentes instancias de controladores?" y fue solo en el seguimiento de la pila que nos encontraron para subir mapValues .


Si está trabajando con Apache Spark, puede tropezar con un problema similar cuando de repente encuentra que para un código elemental con .mapValues puede detectar


 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
Pero el map(identity) resuelve el problema, y ​​generalmente no hay motivación / tiempo para profundizar.


Conclusión


Los errores pueden acechar en los lugares más inesperados, incluso en aquellos métodos que parecen 100% obvios. Específicamente, este problema, en mi opinión, está asociado con un nombre de método deficiente y un tipo de retorno insuficientemente estricto.


Por supuesto, es importante estudiar la documentación de todos los métodos utilizados en la biblioteca estándar, pero no siempre es obvio y, francamente, no siempre hay suficiente motivación para leer sobre las "cosas obvias".


La computación perezosa por sí sola es una broma genial, y el artículo de ninguna manera los alienta a abandonar. Sin embargo, cuando la pereza no es obvia, esto puede generar problemas.


Para ser justos, el problema con mapValues ya apareció en Habré en la traducción, pero personalmente, ese artículo que había puesto muy mal en mi cabeza, porque había muchas cosas ya conocidas / básicas y no estaba completamente claro cuál podría ser el peligro de usar estas funciones:


El método filterKeys envuelve la tabla de origen sin copiar ningún elemento. No hay nada de malo en eso, pero apenas espera este comportamiento de filterKeys

Es decir, hay un comentario sobre el comportamiento inesperado, y que al mismo tiempo también puede pisar un poco el rastrillo, aparentemente, se considera poco probable.


→ Todo el código del artículo está en esta esencia

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


All Articles