Você lê o Scaladoc para métodos de coleta "óbvios"? Ou por que a preguiça nem sempre é boa

Se você não sabe como eles diferem


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

e


 someMap.mapValues(foo) 

exceto pela sintaxe, ou você duvida / não sabe a quais conseqüências ruins essa diferença pode levar e onde a identity é esse artigo é para você.


Caso contrário, participe da pesquisa localizada no final do artigo.


Vamos começar com um simples


Vamos tentar estupidamente pegar um exemplo localizado antes do kat e ver o que acontece:


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

Este código deve imprimir


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

E nesse nível, a compreensão do método mapValues é mapValues nos estágios iniciais do aprendizado do Scala: bem, sim, existe esse método, é conveniente alterar valores no Map quando as chaves não são alteradas. E realmente, o que mais há para pensar? Pelo nome do método, tudo é óbvio, o comportamento é claro.


Exemplos mais complicados


Vamos modificar um pouco o nosso exemplo (vou escrever explicitamente tipos para que você não pense que há algum tipo de trouxa com implícitos):


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

E esse código será produzido após o lançamento


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

É bastante lógico e óbvio. "Cara, é hora de chegar ao final do artigo!" - o leitor dirá. Vamos tornar a criação de nossa classe dependente de condições externas e adicionar algumas verificações simples de idiotice:


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

Na saída, obtemos:


 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 

É lógico que os resultados agora não sejam iguais, mas aleatórios. Mas espere, por que a segunda afirmação deu false ? Realmente os valores em resultMapValues mudaram, mas não fizemos nada com eles? Vamos verificar se tudo dentro é igual ao que era:


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

E na saída temos:


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

imagem


Por que isso aconteceu?


Por println altera o valor do Map ?
Chegou a hora de entrar na documentação para o 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. */ 

A primeira linha nos diz o que pensávamos - altera o Map , aplicando a função passada nos argumentos a cada valor. Mas se você ler com muito cuidado e até o fim, verifica-se que não é o Map que é retornado, mas a "visualização do mapa" (visualização). E essa não é uma visualização normal ( View ), que você pode obter usando o método de view e que possui um método de force para cálculo explícito. Uma classe especial (o código é da versão 2.12.7 do Scala, mas para a 2.11 existe quase a mesma coisa):


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

Se você ler este código, verá que nada é armazenado em cache e cada vez que acessar os valores, eles serão recalculados. O que observamos em nosso exemplo.


Se você trabalha com funções puras e tudo é imutável, não notará nenhuma diferença. Bem, talvez o desempenho seja prejudicado. Infelizmente, porém, nem tudo em nosso mundo é limpo e perfeito, e usando esses métodos, você pode entrar no rake (o que aconteceu em um de nossos projetos, caso contrário, este artigo não teria sido).


Obviamente, não somos os primeiros a se deparar com isso. Já em 2011, um grande erro foi aberto nesta ocasião (e, no momento da redação deste documento, está marcado como aberto). Ele também menciona o método filterKeys , que tem exatamente os mesmos problemas, porque é implementado com o mesmo princípio.


Além disso, desde 2015, um ticket está pendurado para adicionar inspeções ao IntelliJ Idea.


O que fazer


A solução mais simples é estupidamente não usar esses métodos, porque pelo nome, o comportamento deles, na minha opinião, é muito óbvio.


Uma opção um pouco melhor é chamar o map(identity) .
identity , se alguém não souber, esta é uma função da biblioteca padrão que simplesmente retorna seu argumento de entrada. Nesse caso, o trabalho principal é realizado pelo método map , que cria explicitamente um Map normal. Vamos verificar apenas no caso de:


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

Na saída, obtemos


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

Está tudo bem :)


Se você ainda deseja deixar a preguiça, é melhor alterar o código para que fique óbvio. Você pode criar uma classe implícita com um método de wrapper para mapValues e filterKeys , que fornece um novo nome que seja compreensível para eles. Ou use explicitamente .view e trabalhe com um iterador de pares.


Além disso, vale a pena adicionar inspeção ao ambiente / regra de desenvolvimento em um analisador estático / em outro lugar, que alerta sobre o uso desses métodos. Porque é melhor gastar um pouco de tempo nisso agora do que pisar no ancinho e colher as consequências por um longo tempo depois.


De que outra forma você pode pisar no rake e como pisamos neles


Além do caso da dependência de condições externas, que observamos nos exemplos acima, existem outras opções.


Por exemplo, um valor mutável (observe, aqui, superficialmente, tudo é "imutável"):


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

Quando someMap2 novamente, obtivemos um resultado engraçado.


Os problemas que podem surgir quando os mapValues e as filterKeys são usados ​​de maneira mapValues podem ser adicionados à degradação do desempenho, aumento do consumo de memória e / ou aumento da carga no GC, mas isso depende mais de casos específicos e pode não ser tão crítico.


Você também deve adicionar o método toSeq do iterador ao cofrinho de rakes semelhantes, que retorna um Stream lento.


mapValues acidente. Foi usado em um método que, usando reflexão, criou um conjunto de manipuladores a partir da configuração: as chaves eram os identificadores dos manipuladores e o valor eram suas configurações, que foram então convertidas nos próprios manipuladores (a instância da classe foi criada). Como os manipuladores consistiam apenas em funções puras, tudo funcionava sem problemas, e notavelmente não afetava o desempenho (depois de fazer o rake, fizemos as medições).


Mas uma vez eu tive que criar um semáforo em um dos manipuladores para que apenas um manipulador desempenhasse uma função pesada, cujo resultado é armazenado em cache e usado por outros manipuladores. E então os problemas começaram no ambiente de teste - o código válido que funcionava bem localmente começou a falhar devido a problemas com o semáforo. Obviamente, o primeiro pensamento com a inoperabilidade da nova funcionalidade é que os problemas estão associados a ela mesma. Ficamos com isso por um longo tempo até chegarmos à conclusão "algum jogo, por que diferentes instâncias de manipuladores são usadas?" e foi apenas no rastreamento da pilha que eles nos encontraram para mapValues o mapValues .


Se você estiver trabalhando com o Apache Spark, poderá encontrar um problema semelhante quando descobrir repentinamente que, para algum trecho de código elementar com .mapValues é possível 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
Mas o map(identity) resolve o problema e, geralmente, não há motivação / tempo para aprofundar.


Conclusão


Os erros podem estar ocultos nos lugares mais inesperados - mesmo nos métodos que parecem 100% óbvios. Especificamente, esse problema, na minha opinião, está associado a um nome de método ruim e a um tipo de retorno insuficientemente rigoroso.


Obviamente, é importante estudar a documentação de todos os métodos usados ​​na biblioteca padrão, mas nem sempre é óbvio e, francamente, nem sempre há motivação suficiente para ler sobre as "coisas óbvias".


A computação preguiçosa sozinha é uma piada bacana, e o artigo não os encoraja a abandonar. No entanto, quando a preguiça não é óbvia - isso pode levar a problemas.


Para ser mapValues , o problema com mapValues já apareceu em Habré na tradução, mas, pessoalmente, aquele artigo que eu coloquei muito mal na minha cabeça, porque já havia muitas coisas básicas / conhecidas e não estava completamente claro qual poderia ser o perigo de usar essas funções:


O método filterKeys quebra a tabela de origem sem copiar nenhum elemento. Não há nada errado com isso, mas você dificilmente espera esse comportamento das filterKeys

Ou seja, há uma observação sobre comportamento inesperado e, ao mesmo tempo, você também pode pisar um pouco no rake, aparentemente, é considerado improvável.


→ Todo o código do artigo está nesta essência

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


All Articles