【问题标题】:Scala 2.8 breakOutScala 2.8 突破
【发布时间】:2010-12-15 11:59:41
【问题描述】:

在Scala2.8中,scala.collection.package.scala中有一个对象:

def breakOut[From, T, To](implicit b : CanBuildFrom[Nothing, T, To]) =
    new CanBuildFrom[From, T, To] {
        def apply(from: From) = b.apply() ; def apply() = b.apply()
 }

我被告知这会导致:

> import scala.collection.breakOut
> val map : Map[Int,String] = List("London", "Paris").map(x => (x.length, x))(breakOut)

map: Map[Int,String] = Map(6 -> London, 5 -> Paris)

这里发生了什么?为什么breakOut 被称为作为我的List 的参数?

【问题讨论】:

  • 简单的答案是,这不是List 的论据,而是map。
  • String from v.2.13 collection.breakOut 不再存在,使用 .view 和 .to(Collection) 代替。 docs.scala-lang.org/overviews/core/…

标签: scala scala-2.8 scala-collections


【解决方案1】:

答案在map的定义上找到:

def map[B, That](f : (A) => B)(implicit bf : CanBuildFrom[Repr, B, That]) : That 

注意它有两个参数。第一个是你的函数,第二个是隐式的。如果您不提供该隐式,Scala 将选择最具体可用的一个。

关于breakOut

那么,breakOut 的目的是什么?考虑为问题给出的示例,您获取一个字符串列表,将每个字符串转换为一个元组(Int, String),然后从中生成一个Map。最明显的方法是生成一个中介 List[(Int, String)] 集合,然后对其进行转换。

鉴于map 使用Builder 来生成结果集合,难道不能跳过中介List 并将结果直接收集到Map 中吗?显然,是的,是的。然而,为此,我们需要将正确的CanBuildFrom 传递给map,而这正是breakOut 所做的。

那么,让我们看看breakOut的定义:

def breakOut[From, T, To](implicit b : CanBuildFrom[Nothing, T, To]) =
  new CanBuildFrom[From, T, To] {
    def apply(from: From) = b.apply() ; def apply() = b.apply()
  }

注意breakOut 是参数化的,它返回一个CanBuildFrom 的实例。碰巧的是,From、T 和To 类型已经被推断出来,因为我们知道map 期待CanBuildFrom[List[String], (Int, String), Map[Int, String]]。因此:

From = List[String]
T = (Int, String)
To = Map[Int, String]

最后,让我们检查一下breakOut 本身接收到的隐式。它的类型为CanBuildFrom[Nothing,T,To]。我们已经知道所有这些类型,因此我们可以确定我们需要一个隐式类型CanBuildFrom[Nothing,(Int,String),Map[Int,String]]。但是有这样的定义吗?

我们来看看CanBuildFrom的定义:

trait CanBuildFrom[-From, -Elem, +To] 
extends AnyRef

所以CanBuildFrom 在它的第一个类型参数上是逆变的。因为Nothing 是一个底层类(即它是所有事物的子类),这意味着可以使用any 类来代替Nothing。

由于存在这样的构建器,Scala 可以使用它来生成所需的输出。

关于建设者

Scala 集合库中的许多方法包括获取原始集合,以某种方式对其进行处理(在 map 的情况下,转换每个元素),然后将结果存储在新集合中。

为了最大限度地重用代码,这种结果存储是通过 builder (scala.collection.mutable.Builder) 完成的,它基本上支持两种操作:添加元素和返回结果集合。此结果集合的类型将取决于构建器的类型。因此,List 构建器将返回 List,Map 构建器将返回 Map,依此类推。 map 方法的实现不需要关心结果的类型:构建器会处理它。

另一方面,这意味着map 需要以某种方式接收此构建器。设计 Scala 2.8 Collections 时面临的问题是如何选择最好的构建器。例如,如果我要写Map('a' -> 1).map(_.swap),我希望得到一个Map(1 -> 'a') 回复。另一方面,Map('a' -> 1).map(_._1) 不能返回 Map(它返回 Iterable)。

通过这个CanBuildFrom 隐式执行从已知类型的表达式中产生最佳Builder 的魔力。

关于CanBuildFrom

为了更好地解释发生了什么,我将举一个示例,其中被映射的集合是 Map 而不是 List。我稍后会回到List。现在,考虑以下两个表达式:

Map(1 -> "one", 2 -> "two") map Function.tupled(_ -> _.length)
Map(1 -> "one", 2 -> "two") map (_._2)

第一个返回Map,第二个返回Iterable。回归合身系列的魔力是CanBuildFrom 的工作。让我们再次考虑map的定义来理解它。

方法map继承自TraversableLike。它在B 和That 上进行参数化,并利用类型参数A 和Repr 对类进行参数化。让我们一起看看这两个定义:

TraversableLike 类定义为:

trait TraversableLike[+A, +Repr] 
extends HasNewBuilder[A, Repr] with AnyRef

def map[B, That](f : (A) => B)(implicit bf : CanBuildFrom[Repr, B, That]) : That 

要了解A 和Repr 的来源,让我们考虑Map 本身的定义:

trait Map[A, +B] 
extends Iterable[(A, B)] with Map[A, B] with MapLike[A, B, Map[A, B]]

因为TraversableLike 被所有扩展Map、A 和Repr 的特征继承,可以从它们中的任何一个继承。不过,最后一个获得了偏好。因此,根据不可变 Map 的定义以及将其连接到 TraversableLike 的所有特征,我们有:

trait Map[A, +B] 
extends Iterable[(A, B)] with Map[A, B] with MapLike[A, B, Map[A, B]]

trait MapLike[A, +B, +This <: MapLike[A, B, This] with Map[A, B]] 
extends MapLike[A, B, This]

trait MapLike[A, +B, +This <: MapLike[A, B, This] with Map[A, B]] 
extends PartialFunction[A, B] with IterableLike[(A, B), This] with Subtractable[A, This]

trait IterableLike[+A, +Repr] 
extends Equals with TraversableLike[A, Repr]

trait TraversableLike[+A, +Repr] 
extends HasNewBuilder[A, Repr] with AnyRef

如果您将Map[Int, String] 的类型参数一直传递下去,我们发现传递给TraversableLike 的类型,因此map 使用的类型是:

A = (Int,String)
Repr = Map[Int, String]

回到示例,第一个映射接收((Int, String)) =&gt; (Int, Int) 类型的函数,第二个映射接收((Int, String)) =&gt; String 类型的函数。我使用双括号来强调它是一个正在接收的元组,因为这是我们看到的A 的类型。

有了这些信息,让我们考虑一下其他类型。

map Function.tupled(_ -> _.length):
B = (Int, Int)

map (_._2):
B = String

我们可以看到第一个map返回的类型是Map[Int,Int],第二个是Iterable[String]。查看map的定义,不难看出这些是That的值。但它们来自哪里?

如果我们查看所涉及类的伴随对象内部,我们会看到一些提供它们的隐式声明。在对象Map:

implicit def  canBuildFrom [A, B] : CanBuildFrom[Map, (A, B), Map[A, B]]  

在对象Iterable 上,其类由Map 扩展:

implicit def  canBuildFrom [A] : CanBuildFrom[Iterable, A, Iterable[A]]  

这些定义为参数化CanBuildFrom 提供工厂。

Scala 将选择最具体的可用隐式。在第一种情况下,它是第一个CanBuildFrom。第二种情况,由于第一种不匹配,所以选择了第二种CanBuildFrom。

回到问题

让我们看看问题的代码,List's 和map's 的定义(再次)看看类型是如何推断的:

val map : Map[Int,String] = List("London", "Paris").map(x => (x.length, x))(breakOut)

sealed abstract class List[+A] 
extends LinearSeq[A] with Product with GenericTraversableTemplate[A, List] with LinearSeqLike[A, List[A]]

trait LinearSeqLike[+A, +Repr <: LinearSeqLike[A, Repr]] 
extends SeqLike[A, Repr]

trait SeqLike[+A, +Repr] 
extends IterableLike[A, Repr]

trait IterableLike[+A, +Repr] 
extends Equals with TraversableLike[A, Repr]

trait TraversableLike[+A, +Repr] 
extends HasNewBuilder[A, Repr] with AnyRef

def map[B, That](f : (A) => B)(implicit bf : CanBuildFrom[Repr, B, That]) : That 

List("London", "Paris")的类型是List[String],所以TraversableLike上定义的A和Repr的类型是:

A = String
Repr = List[String]

(x =&gt; (x.length, x)) 的类型是(String) =&gt; (Int, String),所以B 的类型是:

B = (Int, String)

最后一个未知类型,That 是 map 的结果类型,我们也已经有了:

val map : Map[Int,String] =

所以,

That = Map[Int, String]

这意味着breakOut 必须返回CanBuildFrom[List[String], (Int, String), Map[Int, String]] 的类型或子类型。

【讨论】:

  • Daniel,我可以通过你的答案中的类型卑躬屈膝,但是一旦我走到最后,我觉得我还没有获得任何高层次的理解。什么是突破? “breakOut”这个名字是从哪里来的(我要摆脱什么)?为什么在这种情况下需要它来获取地图?肯定有一些方法来简要回答这些问题吗? (即使为了掌握每一个细节,冗长的打字仍然是必要的)
  • @Seth 这是一个有效的担忧,但我不确定我是否能胜任这项任务。可以在这里找到它的起源:article.gmane.org/gmane.comp.lang.scala.internals/1812/…。我会考虑的,但是,现在,我想不出太多改进它的方法。
  • 有没有办法避免指定 Map[Int, String] 的整个结果类型,而是能够编写类似:'val map = List("London", "Paris").地图(x => (x.length, x))(breakOut[...Map])'
  • @SethTisue 从我对这个解释的阅读来看,breakOut 似乎对于“突破”您的构建器需要从 List[String] 构建的要求是必要的。编译器需要 CanBuildFrom[List[String], (Int,String), Map[Int,String]],您无法提供。 breakOut 函数通过将 CanBuildFrom 中的第一个类型参数设置为 Nothing 来破坏它。现在你只需要提供一个 CanBuildFrom[Nothing, (Int,String), Map[Int,String]]。这很简单,因为它是由 Map 类提供的。
  • @Mark 当我发现breakOut 时,我看到它解决的问题是monad 坚持映射(通过bind/flatMap)到它们自己的类型的方式。它允许使用一个 monad 将映射链“突破”成不同的 monad 类型。不过,我不知道 Adriaan Moors(作者)是不是这么想的!
【解决方案2】:

我想以丹尼尔的回答为基础。它非常彻底,但正如 cmets 中所述,它没有解释突破的作用。

取自 Re: Support for explicit Builders (2009-10-23),这是我认为 breakout 的作用:

它给编译器一个关于隐式选择哪个 Builder 的建议(本质上它允许编译器选择它认为最适合情况的工厂。)

例如,请参阅以下内容:

scala> import scala.collection.generic._
import scala.collection.generic._

scala> import scala.collection._
import scala.collection._

scala> import scala.collection.mutable._
import scala.collection.mutable._

scala>

scala> def breakOut[From, T, To](implicit b : CanBuildFrom[Nothing, T, To]) =
     |    new CanBuildFrom[From, T, To] {
     |       def apply(from: From) = b.apply() ; def apply() = b.apply()
     |    }
breakOut: [From, T, To]
     |    (implicit b: scala.collection.generic.CanBuildFrom[Nothing,T,To])
     |    java.lang.Object with
     |    scala.collection.generic.CanBuildFrom[From,T,To]

scala> val l = List(1, 2, 3)
l: List[Int] = List(1, 2, 3)

scala> val imp = l.map(_ + 1)(breakOut)
imp: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 3, 4)

scala> val arr: Array[Int] = l.map(_ + 1)(breakOut)
imp: Array[Int] = Array(2, 3, 4)

scala> val stream: Stream[Int] = l.map(_ + 1)(breakOut)
stream: Stream[Int] = Stream(2, ?)

scala> val seq: Seq[Int] = l.map(_ + 1)(breakOut)
seq: scala.collection.mutable.Seq[Int] = ArrayBuffer(2, 3, 4)

scala> val set: Set[Int] = l.map(_ + 1)(breakOut)
seq: scala.collection.mutable.Set[Int] = Set(2, 4, 3)

scala> val hashSet: HashSet[Int] = l.map(_ + 1)(breakOut)
seq: scala.collection.mutable.HashSet[Int] = Set(2, 4, 3)

您可以看到编译器隐式选择返回类型以最匹配预期类型。根据您声明接收变量的方式,您会得到不同的结果。

以下是指定构建器的等效方法。请注意,在这种情况下,编译器将根据构建器的类型推断出预期的类型:

scala> def buildWith[From, T, To](b : Builder[T, To]) =
     |    new CanBuildFrom[From, T, To] {
     |      def apply(from: From) = b ; def apply() = b
     |    }
buildWith: [From, T, To]
     |    (b: scala.collection.mutable.Builder[T,To])
     |    java.lang.Object with
     |    scala.collection.generic.CanBuildFrom[From,T,To]

scala> val a = l.map(_ + 1)(buildWith(Array.newBuilder[Int]))
a: Array[Int] = Array(2, 3, 4)

【讨论】:

  • 我想知道为什么它被命名为“breakOut”?我在想convert 或buildADifferentTypeOfCollection(但更短)之类的东西可能更容易记住。
【解决方案3】:

Daniel Sobral 的回答很棒,应该与 Architecture of Scala Collections 一起阅读(Scala 编程的第 25 章)。

我只是想详细说明一下为什么叫breakOut:

为什么叫breakOut?

因为我们想突破一种类型并进入另一种类型:

破出什么类型变成什么类型​​?我们以Seq上的map函数为例:

Seq.map[B, That](f: (A) -> B)(implicit bf: CanBuildFrom[Seq[A], B, That]): That

如果我们想直接通过对序列元素的映射来构建 Map,例如:

val x: Map[String, Int] = Seq("A", "BB", "CCC").map(s => (s, s.length))

编译器会抱怨:

error: type mismatch;
found   : Seq[(String, Int)]
required: Map[String,Int]

原因是 Seq 只知道如何构建另一个 Seq(即有一个隐式的 CanBuildFrom[Seq[_], B, Seq[B]] 构建器工厂可用,但是从 Seq 到 Map 没有 NO 构建器工厂)。

为了编译,我们需要以某种方式breakOut的类型要求,并且能够构造一个生成器为map生成映射要使用的函数。

正如 Daniel 所解释的,breakOut 具有以下签名:

def breakOut[From, T, To](implicit b: CanBuildFrom[Nothing, T, To]): CanBuildFrom[From, T, To] =
    // can't just return b because the argument to apply could be cast to From in b
    new CanBuildFrom[From, T, To] {
      def apply(from: From) = b.apply()
      def apply()           = b.apply()
    }

Nothing 是所有类的子类,因此任何构建器工厂都可以代替implicit b: CanBuildFrom[Nothing, T, To]。如果我们使用breakOut函数提供隐式参数:

val x: Map[String, Int] = Seq("A", "BB", "CCC").map(s => (s, s.length))(collection.breakOut)

它会编译,因为breakOut 能够提供所需的CanBuildFrom[Seq[(String, Int)], (String, Int), Map[String, Int]] 类型,而编译器能够找到CanBuildFrom[Map[_, _], (A, B), Map[A, B]] 类型的隐式构建器工厂来代替CanBuildFrom[Nothing, T, To],用于breakOut用于创建实际的构建器。

请注意,CanBuildFrom[Map[_, _], (A, B), Map[A, B]] 是在 Map 中定义的,并且只是启动一个使用底层 Map 的 MapBuilder。

希望这能解决问题。

【讨论】:

    【解决方案4】:

    一个简单的例子来理解breakOut做了什么:

    scala> import collection.breakOut
    import collection.breakOut
    
    scala> val set = Set(1, 2, 3, 4)
    set: scala.collection.immutable.Set[Int] = Set(1, 2, 3, 4)
    
    scala> set.map(_ % 2)
    res0: scala.collection.immutable.Set[Int] = Set(1, 0)
    
    scala> val seq:Seq[Int] = set.map(_ % 2)(breakOut)
    seq: Seq[Int] = Vector(1, 0, 1, 0) // map created a Seq[Int] instead of the default Set[Int]
    

    【讨论】:

    • 感谢您的示例!此外,val seq:Seq[Int] = set.map(_ % 2).toVector 不会为您提供重复值,因为 Set 是为 map 保留的。
    • @MatthewPickering 正确! set.map(_ % 2) 首先创建一个Set(1, 0),然后将其转换为Vector(1, 0)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-01-30
    • 1970-01-01
    • 2011-04-12
    • 2011-03-17
    • 2011-02-20
    • 2011-02-13
    相关资源
    最近更新 更多