在Scala中如何将一个元组列表展平?

7

我本以为元组列表可以很容易地被展开:

scala> val p = "abcde".toList
p: List[Char] = List(a, b, c, d, e)

scala> val q = "pqrst".toList
q: List[Char] = List(p, q, r, s, t)

scala> val pq = p zip q
pq: List[(Char, Char)] = List((a,p), (b,q), (c,r), (d,s), (e,t))

scala> pq.flatten

但事实上,发生了这样的情况:

<console>:15: error: No implicit view available from (Char, Char) => scala.collection.GenTraversableOnce[B].
       pq.flatten
          ^

我能够使用以下技术完成工作:

scala> (for (x <- pq) yield List(x._1, x._2)).flatten
res1: List[Char] = List(a, p, b, q, c, r, d, s, e, t)

但是我不理解这个错误信息。而且我的备选方案似乎很复杂和低效。

那个错误信息是什么意思,为什么我不能简单地压缩元组列表?

3个回答

19
如果无法找到隐式转换,您可以明确提供它。
pq.flatten {case (a,b) => List(a,b)}

如果在代码中多次执行此操作,则可以通过使其隐式化来节省一些样板。
scala> import scala.language.implicitConversions
import scala.language.implicitConversions

scala> implicit def flatTup[T](t:(T,T)): List[T]= t match {case (a,b)=>List(a,b)}
flatTup: [T](t: (T, T))List[T]

scala> pq.flatten
res179: List[Char] = List(a, p, b, q, c, r, d, s, e, t)

3
当源类型和目标类型都很常见时,请不要使用隐式转换。如果将其与自动元组混合使用,就会得到各种奇怪的结果。有一个接受字符串列表的方法吗?突然间foo("a", "b")可以工作,但foo("a", "b", "c")不能。依此类推... - Travis Brown
2
明白了。隐式参数本质上有点太“神秘”,在这种非正式的情况下最好避免使用。 - jwvh

5
为什么你和jwvh找到的解决方案是必需的。

如Scala库所述,Tuple2(它被翻译成(,))是:

2个元素的元组;一个Product2的范式表示。

并进一步解释:

Product2是由2个组件构成的笛卡尔积。

...这就意味着Tuple2[T1,T2]表示:

其组件分别是两个集合(分别是T1T2中的所有元素)的成员对的全体集合。

List[T]则表示有序的T元素集合。

实际上,这意味着没有绝对的方法来将任何可能的Tuple2[T1,T2]转换为List[T],因为T1T2可能是不同的。例如,考虑以下元组:

val tuple = ("hi", 5)

这个元组该如何展开?5 应该转换为 String 吗?或者只是展开成 List[Any]?虽然这两种解决方案都可以使用,但它们都是在“绕过类型系统”,所以设计上不会在Tuple API中编码。
所有这些归结为一个事实,即此情况下没有默认的隐式视图,您必须自己提供其中一个,正如jwvh和您已经发现的那样。

所以基本上@jwvh提供的转换确保元组成员都具有相同的类型(T,T)。这将允许它们被展开(a,b)=>列表(a,b)。很好的解释(: - Rafael Saraiva
1
@mdm:将Tuple2[T1,T2]转换为List[T]确实不明显,但是另一方面,Tuple2[T,T]-> List[T]相当简单。我认为Scala没有提供它的好理由。如果我可以展开List[Option[T]],那么我应该也能够展开List[(T,T)]List[(T,T,T)]等。 - Dima
1
@Dima,为什么要使用已经存在的完美好用的Seq(等等)?我认为使用情况有限(在某些情况下,这将是非常错误的)。以存储坐标的典型用法(x, y)为例。xy仍然不是同一件事,而flatten将是一个非常不合适的操作。元组不仅仅是可能不同类型的序列,其中的索引也非常重要,并且提供了一些忽略它的操作是不明智的(当然,这都是“大多数意见”,但在评论中这是可以的)。 - The Archetypal Paul
1
@dima,Option[T]是一个Monad,在这种情况下,flatten实际上就是在运用它。每个Option[T]都是一个Monad,并且行为在类型内是一致的:对任何T进行flatten都会有相同的行为。另一方面,Tuple[T1,T2]不是一个Monad,并且它没有一致的行为可供提供。特别是针对Tuple[T,T]List[T]之间的交互,你可以将其展开,但这并不能证明需要在语言级别上进行特殊处理,我个人认为。 - mdm
3
@dima,此外,您可以将 Tuple[T,T] 定义为一种特定的单子实例,但是为什么语言设计者选择将 List((5,7),(6,8)) 展平为 List(5,7,6,8) 而不是例如 List(5,6)?或者是 List(7,8)?您所描述的行为只有在将 Tuple 用作 List 时才显而易见,这是您不能指望类型系统默认提供的。 - mdm
显示剩余11条评论

1
我们最近需要做这个。在说明我们的解决方案之前,让我简要介绍一下使用情况。
用例
给定一个项目池(我将其称为类型T),我们希望对池中的每个项目进行评估,并与池中的所有其他项目进行比较。这些比较的结果是一个失败的评估集合,我们将其表示为元组,即左侧项目和右侧项目的评估:(T,T)。
完成这些评估后,将Set[(T,T)]扁平化为另一个Set[T]变得非常有用,以突出显示未通过任何比较的所有项目。
解决方案
我们的解决方案是折叠:
val flattenedSet =
    set.foldLeft(Set[T]())
                { case (acc, (x, y)) => acc + x + y }

这从一个空集开始(作为foldLeft的初始参数)作为累加器

然后,对于消耗的每个元素Set [(T,T)](在这里命名为set),将传递折叠函数:

  1. 累加器的最后一个值(acc),和
  2. (T,T)元组用于该元素,case将其拆分为xy

我们的折叠函数然后返回acc + x + y,它返回一个包含累加器中所有元素以及xy的集合。该结果作为累加器传递到下一次迭代 - 因此,它积累了每个元组内的所有值。

为什么不使用List

我特别欣赏这个解决方案,因为它避免了在执行平铺操作时创建中间List - 相反,它直接在构建新的Set [T]时解构每个元组。

我们也可以改变评估代码,返回包含每个失败评估中左侧和右侧项目的List[T],然后flatten就会正常工作。但是我们认为元组更准确地表示了我们在评估中所追求的内容——具体而言是一个项目与另一个项目相比较,而不是一个开放式类型,可能代表任意数量的项目。

你能解释一下这是如何工作的吗?它是如何解决问题的? - Richard Wеrеzaк
@RichardWеrеzaк 我在答案中添加了解释。 - zigg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接