Scala FlatMap 提供了错误的结果
Scala FlatMap provides wrong results
给定一个文档列表,我想获得至少共享一个标记的对。
为此,我编写了下面的代码,通过倒排索引来实现。
object TestFlatMap {
case class Document(id : Int, tokens : List[String])
def main(args: Array[String]): Unit = {
val documents = List(
Document(1, List("A", "B", "C", "D")),
Document(2, List("A", "B", "E", "F", "G")),
Document(3, List("E", "G", "H")),
Document(4, List("A", "L", "M", "N"))
)
val expectedTokensIds = List(("A",1), ("A",2), ("A",4), ("B",1), ("B",2), ("C",1), ("D",1), ("E",2), ("E",3), ("F",2), ("G",2), ("G",3), ("H",3), ("L",4), ("M",4), ("N",4)) //Expected tokens - id tuples
val expectedCouples = Set((1, 2), (1, 4), (2, 3), (2, 4)) //Expected resulting pairs
/**
* For each token returns the id of the documents that contains it
* */
val tokensIds = documents.flatMap{ document =>
document.tokens.map{ token =>
(token, document.id)
}
}
//Check if the tuples are right
assert(tokensIds.length == expectedTokensIds.length && tokensIds.intersect(expectedTokensIds).length == expectedTokensIds.length, "Error: tokens-ids not matches")
//Group the documents by the token
val docIdsByToken = tokensIds.groupBy(_._1).filter(_._2.size > 1)
/**
* For each group of documents generate the pairs
* */
val couples = docIdsByToken.map{ case (token, docs) =>
docs.combinations(2).map{ c =>
val d1 = c.head._2
val d2 = c.last._2
if(d1 < d2){
(d1, d2)
}
else{
(d2, d1)
}
}
}.flatten.toSet
/**
* Same operation, but with flatMap
* For each group of documents generate the pairs
* */
val couples1 = docIdsByToken.flatMap{ case (token, docs) =>
docs.combinations(2).map{ c =>
val d1 = c.head._2
val d2 = c.last._2
if(d1 < d2){
(d1, d2)
}
else{
(d2, d1)
}
}
}.toSet
//The results obtained with flatten pass the test
assert(couples.size == expectedCouples.size && couples.intersect(expectedCouples).size == expectedCouples.size, "Error: couples not matches")
//The results obtained with flatMap do not pass the test: they are wrong
assert(couples1.size == expectedCouples.size && couples1.intersect(expectedCouples).size == expectedCouples.size, "Error: couples1 not matches")
}
问题是应该生成最终结果的 flatMap 不能正常工作,它只有 returns 两对:(2,3) 和 (1,2)。
我不明白为什么它不起作用,而且 IntelliJ 建议我使用 flatMap 而不是使用 map 然后展平。
谁能帮我解释一下问题出在哪里?因为想不通,我以前也遇到过这个问题。
谢谢
卢卡
这是一个很好的例子,证明了如果在 map
/flatMap
/flatten
期间在不同类型的集合之间切换,所有好的 monad 法则不一定都成立。
您必须将 Map
转换为 List
,这样当您构建另一个 Map
作为中间结果时,键就不会被重复覆盖,因为 Map
将覆盖密钥,而不是收集所有对:
val couples1 = docIdsByToken.toList.flatMap{ case (token, docs) =>
docs.combinations(2).map{ c =>
val d1 = c.head._2
val d2 = c.last._2
if(d1 < d2){
(d1, d2)
}
else{
(d2, d1)
}
}
}.toSet
这是一个更短的版本,它演示了同样的问题:
val m = Map("A" -> (2, 1), "B" -> (2, 3))
val s = m.flatMap{ case (k, v) => List(v) }.toSet
println(s)
而不是Set((2, 1), (2, 3))
,它会产生Set((2, 3))
,因为
在 flatMap
之后和 toSet
之前,中间结果 是一个新的 Map
,并且此映射只能包含 一个值 对于 key = 2
.
与第一个版本的不同之处在于,在 map
之后,您会获得类似于 Iterable[List[(Int, Int)]]
的东西,而不是 Map
,因此不能 lose/override键。
给定一个文档列表,我想获得至少共享一个标记的对。 为此,我编写了下面的代码,通过倒排索引来实现。
object TestFlatMap {
case class Document(id : Int, tokens : List[String])
def main(args: Array[String]): Unit = {
val documents = List(
Document(1, List("A", "B", "C", "D")),
Document(2, List("A", "B", "E", "F", "G")),
Document(3, List("E", "G", "H")),
Document(4, List("A", "L", "M", "N"))
)
val expectedTokensIds = List(("A",1), ("A",2), ("A",4), ("B",1), ("B",2), ("C",1), ("D",1), ("E",2), ("E",3), ("F",2), ("G",2), ("G",3), ("H",3), ("L",4), ("M",4), ("N",4)) //Expected tokens - id tuples
val expectedCouples = Set((1, 2), (1, 4), (2, 3), (2, 4)) //Expected resulting pairs
/**
* For each token returns the id of the documents that contains it
* */
val tokensIds = documents.flatMap{ document =>
document.tokens.map{ token =>
(token, document.id)
}
}
//Check if the tuples are right
assert(tokensIds.length == expectedTokensIds.length && tokensIds.intersect(expectedTokensIds).length == expectedTokensIds.length, "Error: tokens-ids not matches")
//Group the documents by the token
val docIdsByToken = tokensIds.groupBy(_._1).filter(_._2.size > 1)
/**
* For each group of documents generate the pairs
* */
val couples = docIdsByToken.map{ case (token, docs) =>
docs.combinations(2).map{ c =>
val d1 = c.head._2
val d2 = c.last._2
if(d1 < d2){
(d1, d2)
}
else{
(d2, d1)
}
}
}.flatten.toSet
/**
* Same operation, but with flatMap
* For each group of documents generate the pairs
* */
val couples1 = docIdsByToken.flatMap{ case (token, docs) =>
docs.combinations(2).map{ c =>
val d1 = c.head._2
val d2 = c.last._2
if(d1 < d2){
(d1, d2)
}
else{
(d2, d1)
}
}
}.toSet
//The results obtained with flatten pass the test
assert(couples.size == expectedCouples.size && couples.intersect(expectedCouples).size == expectedCouples.size, "Error: couples not matches")
//The results obtained with flatMap do not pass the test: they are wrong
assert(couples1.size == expectedCouples.size && couples1.intersect(expectedCouples).size == expectedCouples.size, "Error: couples1 not matches")
}
问题是应该生成最终结果的 flatMap 不能正常工作,它只有 returns 两对:(2,3) 和 (1,2)。 我不明白为什么它不起作用,而且 IntelliJ 建议我使用 flatMap 而不是使用 map 然后展平。
谁能帮我解释一下问题出在哪里?因为想不通,我以前也遇到过这个问题。
谢谢
卢卡
这是一个很好的例子,证明了如果在 map
/flatMap
/flatten
期间在不同类型的集合之间切换,所有好的 monad 法则不一定都成立。
您必须将 Map
转换为 List
,这样当您构建另一个 Map
作为中间结果时,键就不会被重复覆盖,因为 Map
将覆盖密钥,而不是收集所有对:
val couples1 = docIdsByToken.toList.flatMap{ case (token, docs) =>
docs.combinations(2).map{ c =>
val d1 = c.head._2
val d2 = c.last._2
if(d1 < d2){
(d1, d2)
}
else{
(d2, d1)
}
}
}.toSet
这是一个更短的版本,它演示了同样的问题:
val m = Map("A" -> (2, 1), "B" -> (2, 3))
val s = m.flatMap{ case (k, v) => List(v) }.toSet
println(s)
而不是Set((2, 1), (2, 3))
,它会产生Set((2, 3))
,因为
在 flatMap
之后和 toSet
之前,中间结果 是一个新的 Map
,并且此映射只能包含 一个值 对于 key = 2
.
与第一个版本的不同之处在于,在 map
之后,您会获得类似于 Iterable[List[(Int, Int)]]
的东西,而不是 Map
,因此不能 lose/override键。