【问题标题】:Scala FlatMap provides wrong resultsScala FlatMap 提供了错误的结果
【发布时间】:2023-09-15 22:11:02
【问题描述】:

给定一个文档列表,我想获得至少共享一个令牌的对。 为此,我编写了下面的代码,通过倒排索引来实现。

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 不能正常工作,它只返回两对:(2,3) 和 (1,2)。 我不明白为什么它不起作用,而且 IntelliJ 建议我使用 flatMap 而不是使用 map 然后 flatten。

有人能解释一下问题出在哪里吗?因为想不通,我以前也遇到过这个问题。

谢谢

卢卡

【问题讨论】:

    标签: scala flatten flatmap


    【解决方案1】:

    这是一个很好的例子,它表明如果您在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一个值。 p>

    与第一个版本的不同之处在于,在map 之后,您会获得类似于Iterable[List[(Int, Int)]] 的东西,而不是Map,因此不会丢失/覆盖任何键。

    【讨论】:

    • token 从未被引用。这更有意义:val couples1 = docIdsByToken.values.flatMap{ docs =&gt;。 . .
    • @jwvh 我复制了一段已经编译、测试(至少表面上,通过对输出的视觉检查)并且通过了 OP 提供的所有断言的代码。令牌不应该被引用,它只是作为数字之间的粘合剂。一旦找到数字对,就可以丢弃字符串标记。不过,它应该被替换为和下划线。我保留它是为了尽可能多地保留原始代码。
    • 这是 Scala 当前集合库中最糟糕的“特性”之一。由于映射SetMap,我遇到了很多错误。
    • 你可以看看this question我曾经问过这个问题。
    • @ziggystar 好问题,但出于某种原因,我对解决方法没有印象。老实说,希望有更系统的东西:]