在 scala 中理解 Lists/Sets

for comprehension on Lists/Sets in scala

这可能是个幼稚的问题。我有一个案例 class 'Book',定义如下:

case class Book(title : String, authors : List[String])

在我的main方法中,我定义了一些Book记录如下:

val books = List(
  Book(title = "Book1", authors = List("Author1", "Author2")),
  Book(title = "Book2", authors = List("Author3", "Author4")),
  Book(title = "Book3", authors = List("Author2", "Author5")),
  Book(title = "Book4", authors = List("Author6", "Author3")),
  Book(title = "Book5", authors = List("Author7", "Author8")),
  Book(title = "Book6", authors = List("Author5", "Author9"))
)

我正在编写一个查询来检索创作了不止一本书的作者的姓名,我的查询如下:

val authorsWithMoreThanTwoBooks =
      (for {

        b1 <- books
        b2 <- books
        if b1.title != b2.title
        a1 <- b1.authors
        a2 <- b2.authors
        if a1 == a2
      } yield a1)

println(authorsWithMoreThanTwoBooks)

这会打印 List(Author2, Author3, Author2, Author5, Author3, Author5)(作者的名字出现两次,这是意料之中的,因为每对书都会被拿走两次,比如 (b1,b2) 和 (b2,b1))。

当然我可以使用 distinct 来解决这个问题,但另一种方法是不在列表中而是在 Set 中创建记录:

val books = Set(
  Book(title = "Book1", authors = List("Author1", "Author2")),
  Book(title = "Book2", authors = List("Author3", "Author4")),
  Book(title = "Book3", authors = List("Author2", "Author5")),
  Book(title = "Book4", authors = List("Author6", "Author3")),
  Book(title = "Book5", authors = List("Author7", "Author8")),
  Book(title = "Book6", authors = List("Author5", "Author9"))
)

for表达式和println后的输出:Set(Author5, Author2, Author3)

为什么会发生这种行为?为什么 Set 上的 for 表达式会生成另一个 Set 而不是 List?如果需要,是否有可能获得具有重复值的相关作者的 List

在 Scala 中,for 表达式实际上被编译器翻译成高阶函数的组合:

  • using a for 产生的东西被翻译成 mapflatMapwithFilter

  • 使用不产生任何结果的 for 被翻译成 withFilterforeach

如果您对此不熟悉,也许您应该在某处查找(here's 一种选择)。

所以,你的构造

for {
  b1 <- books
  b2 <- books 
  if b1.title != b2.title
  a1 <- b1.authors
  a2 <- b2.authors 
  if a1 == a2
} yield a1

翻译成

books.flatMap(b1 => books
  .filter(b2 => b1.title != b2.title)
  .flatMap(b2 => b1.authors
  .flatMap(a1 => b2.authors
  .filter(a2 => a1 == a2)
  .map(a2 => a1))))

平面映射两个 Sets 生成一个 SetLists 的平面映射 Set 也会生成 Set。实际上这比看起来要多得多(它可以追溯到范畴论,因为 Set 在这种情况下是一个单子,而 flatMap 是它的自然转换之一,但这只是题外话)。

无论如何,我不确定你最后一个问题的意思,但是如果你想将原始 books 集合保留为 Set 并使用 for 表达式,您可以在对书籍进行操作之前简单地调用 .toList 。这样整个 for 表达式都在 List 而不是 Set.

上工作

P.S.

这表明 for 表达式的性质更接近于函数式编程构造,例如 monad 和仿函数(分别使用 flatMap 和 map 进行操作),而不是经典的 for 循环。与典型的命令式编程构造的经典 for 循环不同,Scala 中的 for 表达式完全是函数式构造,因为它们是高阶函数链 mapflatMapfilterforeach。请注意,这意味着您不仅可以将 for 表达式用于集合,还可以用于任何支持这些函数的表达式(例如,您可以将它们与 OptionFuture 一起使用)。这根本不是一个天真的问题,如果你到目前为止还没有意识到这一点,那么了解这一点很重要。当然,您不需要能够在半夜将任何给定的 for 表达式转换为 maps 和 flatMaps 链,但您应该意识到它使用了那些函数 "under the hood"。