假设我有一个字符串,“你好”,我想生成一个字符频率图:
Map[Char,Int] = Map(h -> 1, e -> 1, o -> 1, l -> 2)
我可以迭代地执行此操作:
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)
}
通过在REPL中乱搞,我发现我可以做一些更简洁的事情,而不是使用可变集合:
> 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)
但是我不知道groupBy()的性能特征,也不知道传递给map的块中发生了什么(就像p恰当的那样)。
如何使用Scala中的功能范例以惯用方式执行此操作?
对于背景,我是第一次从Ruby来到Scala。在Ruby中,我会使用inject
,但我不确定在Scala中执行此操作的并行方法是:
counts = str.each_byte.inject(Hash.new(0)){ |h, c| h[c] += 1; h}
答案 0 :(得分:35)
p
是什么意思? groupBy
采用将元素映射到K
类型的键的函数。在某个集合Coll
上调用时,它会返回Map[K, Coll]
,其中包含从键K
到映射到同一键的所有元素的映射。
因此,在您的情况下,str.groupBy(_.toChar)
会生成从键k
(这是一个字符)到包含所有元素(字符)c
的字符串的映射映射,以便{ {1}}。
你明白了:
k == c.toChar
Map(e -> "e", h -> "h", l -> "ll", o -> "o")
是一组可重复的键和值。在这种情况下,每对都是一个字符和一串元素。在Map
上调用map
操作涉及映射这些对 - Map
是一对p
是一个字符,p._1
是关联字符串(您可以像上面那样拨打p._2
。
以上是如何使用length
和groupBy
惯用法。或者,您可以在字符串长度上使用不可变映射和递归来计算频率,或使用不可变映射和map
。
最好benchmark看看差异。 这里有几个microbenchmark用于高度重复的字符串(~3GHz iMac,JDK7,Scala 2.10.0每晚):
foldLeft
结果:
势在必行: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)}
}
}
组合器:$ 103 57 53 58 53 53 53 53 53 53
折叠:$ 72 51 63 56 53 52 52 54 53 53
请注意,更改命令式版本以使用$ 163 62 71 62 57 57 57 58 57 57
:
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
}
电话,显然非常慢:
put
:withDefaultValue
结论:在这种情况下,角色的装箱和拆箱足够高,因此难以观察到这些方法之间的性能差异。
编辑:
更新:您可能希望使用ScalaMeter inline benchmarking代替$ 133 87 109 106 101 100 101 100 101 101
特征。
答案 1 :(得分:25)
扩展Axel的答案。
您的groupBy
解决方案已经正常运行。它只是微小的修正,可以使它更清洁:
str.groupBy(_.toChar).mapValues(_.size)
Scala替代inject
的替代方法是foldLeft
,foldRight
,reduce
,reduceOption
取决于您使用它的方式。您在Ruby中使用inject
的方式不起作用,因为您的解决方案基于变异h
,而在功能世界中,可变性是“禁忌”。以下是您在Scala中以接近inject
但功能样式的方式执行解决方案的方法:
str.foldLeft( Map[Char, Int]() ){ (m, c) => m + (c -> (m.getOrElse(c, 0) + 1)) }
显然groupBy
看起来好多了。
答案 2 :(得分:11)
您在ruby上的示例几乎可以使用foldLeft
和不可变Map
直接转换为Scala。
以下是可能的解决方案之一:
str.foldLeft(Map[Char, Int]() withDefaultValue 0){(h, c) => h.updated(c, h(c)+1)}
实际上,如果你对局部可变性感到满意,你可以做这样的事情:
def charFrequencies(str: String): collection.Map[Char, Int] = {
val hash = collection.mutable.HashMap.empty[Char, Int] withDefaultValue 0
str foreach { hash(_) += 1 }
hash
}
表达式hash(_) += 1
将被移至c => hash(c) = hash(c) + 1
,然后移至c => hash.update(c, hash.apply(c) + 1)
此解决方案应该比功能解决方案更有效,因为它不会创建中间集合。此外,因为方法返回不可变collection.Map[Char, Int]
,结果将被视为不可变(只要没有人会对它执行不安全的向下转换)。
答案 3 :(得分:2)
从Scala 2.13
开始,我们可以使用groupMapReduce方法,该方法(顾名思义)等效于groupBy
,后跟mapValues
和缩减步骤:< / p>
"hello".groupMapReduce(identity)(_ => 1)(_ + _)
// immutable.Map[Char,Int] = Map(e -> 1, h -> 1, l -> 2, o -> 1)
此:
group
个字符(组 MapReduce的组部分)
map
将每个分组值出现的次数设为1(组 Map Reduce的映射部分)
reduce
在一组值(_ + _
)中的值相加(减少groupMap Reduce 的一部分)。
这是等效于performed in one pass的版本,通过以下字符组成:
"hello".groupBy(identity).mapValues(_.map(_ => 1).reduce(_+_))