scala - tipos - ¿Cómo debo evitar capturar involuntariamente el alcance local en los literales de funciones?
scala tutorial (1)
Preguntaré esto con un ejemplo de Scala, pero bien podría ser que esto afecte a otros idiomas que permiten estilos imperativos y funcionales híbridos.
Aquí hay un pequeño ejemplo ( ACTUALIZADO , ver a continuación):
def method: Iterator[Int] {
// construct some large intermediate value
val huge = (1 to 1000000).toList
val small = List.fill(5)(scala.util.Random.nextInt)
// accidentally use huge in a literal
small.iterator filterNot ( huge contains _ )
}
Ahora iterator.filterNot
funciona perezosamente, ¡lo cual es genial! Como resultado, esperaríamos que el iterador devuelto no consuma mucha memoria (de hecho, O (1)). Tristemente, sin embargo, hemos cometido un terrible error: dado que filterNot
es flojo, guarda una referencia a la función literal que huge contains _
.
Por lo tanto, si bien pensamos que el método requeriría una gran cantidad de memoria mientras se estaba ejecutando, y que esa memoria podría liberarse inmediatamente después de la terminación del método, de hecho esa memoria está atascada hasta que olvidemos el Iterator
devuelto.
(¡Acabo de cometer un error así, que tardó mucho tiempo en rastrear! Puedes ver cosas como estas mirando montones de montones ...)
¿Cuáles son las mejores prácticas para evitar este problema?
Parece que la única solución es verificar cuidadosamente los literales de funciones que sobreviven al final del alcance y que capturaron variables intermedias. Esto es un poco incómodo si está construyendo una colección no estricta y planea regresarla. ¿Alguien puede pensar en algunos trucos agradables, específicos de Scala o de otro tipo, que eviten este problema y me permitan escribir un buen código?
ACTUALIZACIÓN: el ejemplo que había dado anteriormente era estúpido, como lo demuestra la respuesta de huynhjl a continuación. Había sido:
def method: Iterator[Int] {
val huge = (1 to 1000000).toList // construct some large intermediate value
val n = huge.last // do some calculation based on it
(1 to n).iterator map (_ + 1) // return some small value
}
De hecho, ahora que entiendo un poco mejor cómo funcionan estas cosas, ¡no estoy tan preocupado!
¿Estás seguro de que no estás simplificando demasiado el caso de prueba? Esto es lo que corro:
object Clos {
def method: Iterator[Int] = {
val huge = (1 to 2000000).toList
val n = huge.last
(1 to n).iterator map (_ + 1)
}
def gc() { println("GC!!"); Runtime.getRuntime.gc }
def main(args:Array[String]) {
val list = List(method, method, method)
list.foreach(m => println(m.next))
gc()
list.foreach(m => println(m.next))
list.foreach(m => println(m.next))
}
}
Si te entiendo correctamente, porque main
está usando los iteradores incluso después de la llamada a gc()
, la JVM estaría reteniendo los objetos huge
.
Así es como lo manejo:
JAVA_OPTS="-verbose:gc" scala -cp classes Clos
Esto es lo que imprime hacia el final:
[Full GC 57077K->57077K(60916K), 0.3340941 secs]
[Full GC 60852K->60851K(65088K), 0.3653304 secs]
2
2
2
GC!!
[Full GC 62959K->247K(65088K), 0.0610994 secs]
3
3
3
4
4
4
Así que me parece que los objetos huge
fueron recuperados ...