web-dev-qa-db-ja.com

Scala 2.8 breakOut

Scala 2.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)

ここで何が起こっていますか? breakOutListに対して引数としてと呼ばれるのはなぜですか?

223
oxbow_lakes

答えは、mapの定義にあります。

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

2つのパラメーターがあることに注意してください。 1つ目は関数で、2つ目は暗黙的です。暗黙的に指定しない場合、Scalaは、利用可能な最も多くspecificを選択します。

breakOutについて

それで、breakOutの目的は何ですか? 「文字列のリストを取得し、各文字列をタプル(Int, String)に変換し、その中からMapを生成する」という質問の例を考えてみましょう。これを行う最も明白な方法は、中間のList[(Int, String)]コレクションを作成し、それを変換します。

mapBuilderを使用して結果のコレクションを生成する場合、中間のListをスキップして結果を直接Mapに収集することはできませんか?明らかに、はい、そうです。ただし、そうするためには、適切なCanBuildFrommapに渡す必要があります。それがまさに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のインスタンスを返すことに注意してください。たまたま、FromCanBuildFrom[List[String], (Int, String), Map[Int, String]]を予期していることがわかっているため、タイプTTo、およびmapはすでに推論されています。したがって:

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の場合、各要素を変換し)、結果を新しいコレクションに格納します。

コードの再利用を最大化するために、この結果の保存はbuilderscala.collection.mutable.Builder)を介して行われます。これは基本的に要素の追加と結果のコレクションを返します。この結果のコレクションのタイプは、ビルダーのタイプによって異なります。したがって、ListビルダーはListを返し、MapビルダーはMapを返します。 mapメソッドの実装は、結果の型を考慮する必要はありません。ビルダーが処理します。

一方、それはmapが何らかの方法でこのビルダーを受け取る必要があることを意味します。 Scala 2.8コレクションを設計する際に直面した問題は、可能な限り最高のビルダーを選択する方法でした。たとえば、Map('a' -> 1).map(_.swap)と書くとしたら、Map(1 -> 'a') back。一方、Map('a' -> 1).map(_._1)Mapを返すことができません(Iterableを返します)。

式の既知のタイプから可能な限り最良のBuilderを生成する魔法は、このCanBuildFrom暗黙的によって実行されます。

CanBuildFromについて

何が起こっているかをよりよく説明するために、マップされるコレクションがMapではなくListである例を示します。後でListに戻ります。今のところ、次の2つの式を検討してください。

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

最初はMapを返し、2番目は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 

AReprがどこから来たかを理解するために、Map自体の定義を考えてみましょう。

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

TraversableLikeMapを拡張するすべての特性に継承されるため、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)) => (Int, Int)型の関数を受け取り、2番目のマップは((Int, String)) => String型の関数を受け取っています。二重括弧を使用して、受け取ったタプルであることを強調しています。これは、先ほど見たAのタイプだからです。

その情報を使用して、他のタイプを考えてみましょう。

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

map (_._2):
B = String

最初のmapが返す型はMap[Int,Int]であり、2番目は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でした。 2番目のケースでは、最初のケースが一致しなかったため、2番目のCanBuildFromを選択しました。

質問に戻る

質問のコード、Listmapの定義(もう一度)を見て、型がどのように推論されるかを見てみましょう。

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]であるため、Aで定義されているタイプReprおよびTraversableLikeは次のとおりです。

A = String
Repr = List[String]

(x => (x.length, x))の型は(String) => (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]]のタイプまたはサブタイプを返す必要があります。

323

ダニエルの答えを基にしたいと思います。それは非常に徹底的でしたが、コメントで述べたように、ブレイクアウトが何をするのかを説明していません。

Re:明示的なビルダーのサポート(2009-10-23)から取ったものです。

コンパイラーに、どの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)
86
Austen Holmes

Daniel Sobralの答えは素晴らしく、 Architecture of Scala Collections (Scalaのプログラミングの第25章)と一緒に読む必要があります。

breakOutと呼ばれる理由を詳しく説明したかっただけです。

なぜbreakOutと呼ばれるのですか?

あるタイプから別のタイプにブレークアウトしたいので

どのタイプからどのタイプに分割しますか?例としてmapSeq関数を見てみましょう:

_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]]_ビルダーファクトリが利用可能ですが、[〜#〜] no [〜#〜]があるためです。 SeqからMapへのビルダーファクトリ)。

コンパイルするためには、何らかの方法でbreakOutのタイプの要件、また、使用するmap関数のマップを生成するビルダーを構築できます。

ダニエルが説明したように、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を単純に開始することに注意してください。

これで問題が解決することを願っています。

7
Dzhu

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]
4
man