string - initialize - scala map of maps
Generando un mapa de frecuencia para una cadena en Scala (4)
1) ¿Qué significa p
?
groupBy
toma una función que mapea elementos a una clave de tipo K
Cuando se invoca en alguna colección Coll
, devuelve un Map[K, Coll]
que contiene las asignaciones de las claves K
a todos los elementos que se asignaron a la misma clave.
Entonces, en su caso, str.groupBy(_.toChar)
produce una asignación de mapa de una clave k
(que es un carácter) a una cadena con todos los elementos (caracteres) c
tal que k == c.toChar
. Usted obtiene esto:
Map(e -> "e", h -> "h", l -> "ll", o -> "o")
Un Map
es un iterable de pares de claves y valores. En este caso, cada par es un carácter y una cadena de elementos. Llamar a la operación de map
en un Map
implica mapear en estos pares - p
es un par donde p._1
es un carácter, y p._2
es la cadena asociada (a la que puede llamar length
, como hizo anteriormente).
2) ¿Cómo hacer esto idiomáticamente?
Lo anterior es cómo hacerlo de forma idiomática: usando groupBy
y map
. Alternativamente, puede usar un mapa inmutable y una recursión en la longitud de la cadena para calcular las frecuencias, o un mapa inmutable y un foldLeft
.
3) Característica de rendimiento
Lo mejor es benchmark para ver las diferencias. Aquí hay un par de microbenchmark para una cadena altamente repetitiva (~ 3GHz iMac, JDK7, Scala 2.10.0 todas las noches):
object Imperative extends testing.Benchmark {
val str = "abc" * 750000
def run() {
var counts = new scala.collection.mutable.HashMap[Char,Int]
var i = 0
val until = str.length
while (i < until) {
var c = str(i)
if (counts.contains(c))
counts.put(c, counts(c) + 1)
else
counts.put(c, 1)
i += 1
}
//println(f)
}
}
object Combinators extends testing.Benchmark {
val str = "abc" * 750000
def run() {
val f = str.groupBy(_.toChar).map(p => (p._1, p._2.length))
}
}
object Fold extends testing.Benchmark {
val str = "abc" * 750000
def run() {
val f = str.foldLeft(Map[Char, Int]() withDefaultValue 0){(h, c) => h.updated(c, h(c)+1)}
}
}
Resultados:
Imperativo:
$ 103 57 53 58 53 53 53 53 53 53
Combinadores:
$ 72 51 63 56 53 52 52 54 53 53
Fold:
$ 163 62 71 62 57 57 57 58 57 57
Tenga en cuenta que al cambiar la versión imperativa para usar con withDefaultValue
:
var counts = new scala.collection.mutable.HashMap[Char,Int].withDefaultValue(0)
var i = 0
val until = str.length
while (i < until) {
var c = str(i)
counts.put(c, counts(c) + 1)
i += 1
}
Aparentemente es terriblemente lento debido a que reenvía cada llamada de put
:
-
withDefaultValue
:$ 133 87 109 106 101 100 101 100 101 101
Conclusión: el boxeo y el desajuste de los personajes en este caso es lo suficientemente alto como para que las diferencias en el rendimiento entre estos enfoques sean difíciles de observar.
EDITAR:
Actualización: es posible que desee utilizar la evaluación comparativa en línea ScalaMeter en lugar del rasgo de Benchmark
.
Digamos que tengo una cadena, "hola", y quiero generar un mapa de frecuencia de caracteres:
Map[Char,Int] = Map(h -> 1, e -> 1, o -> 1, l -> 2)
Yo podría hacer esto iterativamente
val str = "hello"
var counts = new scala.collection.mutable.HashMap[Char,Int]
for (i <- str) {
if (counts.contains(i))
counts.put(i, counts(i) + 1)
else
counts.put(i, 1)
}
Al jugar en el REPL, descubrí que puedo hacer algo un poco más conciso y no usar una colección mutable:
> str.groupBy(_.toChar).map{ p => (p._1, p._2.length)}
scala.collection.immutable.Map[Char,Int] = Map(h -> 1, e -> 1, o -> 1, l -> 2)
Pero no conozco las características de rendimiento de groupBy () ni lo que está pasando en el bloque que se pasa al mapa (como qué es exactamente, p).
¿Cómo hago esto idiomáticamente usando los paradigmas funcionales en Scala?
Para los antecedentes, acabo de llegar a Scala por primera vez desde Ruby. En Ruby, usaría inject
pero no estoy seguro de cuál es la forma paralela de hacerlo en Scala:
counts = str.each_byte.inject(Hash.new(0)){ |h, c| h[c] += 1; h}
Al iniciar Scala 2.13
, podemos usar el método groupMapReduce que es (como su nombre lo indica) un equivalente de groupBy
seguido de mapValues
y un paso de reducción:
"hello".groupMapReduce(identity)(_ => 1)(_ + _) // immutable.Map[Char,Int] = Map(e -> 1, h -> 1, l -> 2, o -> 1)
Esta:
Caracteres del grupo (parte del grupo MapReduce del grupo )
map
s cada ocurrencia de valor agrupado a 1 (parte del mapa del grupo Mapa Reducir)reduce
los valores de s dentro de un grupo de valores (_ + _
) sumándolos (reduzca parte de groupMap Reduce ).
Esta es una versión equivalente realizada en una sola pasada a través de la secuencia de caracteres de:
"hello".groupBy(identity).mapValues(_.map(_ => 1).reduce(_+_))
Extendiendo la respuesta de Axel.
Tu solución groupBy
ya es funcional. Solo hay una pequeña corrección que podría hacerla más limpia:
str.groupBy(_.toChar).mapValues(_.size)
La alternativa de Scala para inject
es foldLeft
, foldRight
, reduce
, reduceOption
dependiendo de cómo lo use. La forma en que usó inject
en Ruby no es funcional, ya que su solución se basa en la mutación h
y en la mutabilidad del mundo funcional es un "no-no". Así es como haría la solución cerca de su inject
pero con un estilo funcional en Scala:
str.foldLeft( Map[Char, Int]() ){ (m, c) => m + (c -> (m.getOrElse(c, 0) + 1)) }
Obviamente groupBy
ve mucho mejor.
Su ejemplo en ruby se puede traducir casi directamente a Scala usando foldLeft
Map
foldLeft
e inmutable.
Aquí está una de las posibles soluciones:
str.foldLeft(Map[Char, Int]() withDefaultValue 0){(h, c) => h.updated(c, h(c)+1)}
En realidad, si estás de acuerdo con la mutabilidad local, puedes hacer algo como esto:
def charFrequencies(str: String): collection.Map[Char, Int] = {
val hash = collection.mutable.HashMap.empty[Char, Int] withDefaultValue 0
str foreach { hash(_) += 1 }
hash
}
La expresión hash(_) += 1
se desugará a c => hash(c) = hash(c) + 1
y luego a c => hash.update(c, hash.apply(c) + 1)
Esta solución debería ser más eficiente que las funcionales, ya que no crea colecciones intermedias. Además, debido a que el método devuelve una collection.Map[Char, Int]
inmutable collection.Map[Char, Int]
, el resultado se tratará como inmutable (siempre y cuando nadie realice un downcasting inseguro en él).