Scala 2.8 breakOut

225

Di Scala 2.8 , ada objek di 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()
 }

Saya telah diberitahu bahwa ini menghasilkan:

> 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)

Apa yang terjadi disini? Mengapa breakOutdipanggil sebagai argumen untuk saya List?

oxbow_lakes
sumber
13
Jawaban yang sepele adalah, itu bukan argumen untuk List, tetapi untuk map.
Daniel C. Sobral

Jawaban:

325

Jawabannya ditemukan pada definisi map:

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

Perhatikan bahwa ia memiliki dua parameter. Yang pertama adalah fungsi Anda dan yang kedua adalah implisit. Jika Anda tidak memberikan yang tersirat itu, Scala akan memilih yang paling spesifik yang tersedia.

Tentang breakOut

Jadi, apa tujuannya breakOut? Pertimbangkan contoh yang diberikan untuk pertanyaan, Anda mengambil daftar string, mengubah setiap string menjadi sebuah tuple (Int, String), dan kemudian menghasilkan yang Mapkeluar. Cara paling jelas untuk melakukan itu akan menghasilkan List[(Int, String)]koleksi perantara , dan kemudian mengubahnya.

Mengingat bahwa mapmenggunakan a Builderuntuk menghasilkan koleksi yang dihasilkan, tidak akan mungkin untuk melewatkan perantara Listdan mengumpulkan hasilnya langsung menjadi Map? Jelas, ya, benar. Namun, untuk melakukannya, kita perlu memberikan hak CanBuildFromkepada map, dan memang itulah yang breakOutdilakukannya.

Mari kita lihat definisi 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()
  }

Perhatikan bahwa breakOutparameternya, dan itu mengembalikan instance dari CanBuildFrom. Seperti yang terjadi, jenisnya From, Tdan Tosudah disimpulkan, karena kita tahu itu mapyang diharapkan CanBuildFrom[List[String], (Int, String), Map[Int, String]]. Karena itu:

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

Untuk menyimpulkan, mari kita periksa implisit yang diterima dengan breakOutsendirinya. Itu adalah tipe CanBuildFrom[Nothing,T,To]. Kita sudah mengetahui semua tipe ini, sehingga kita dapat menentukan bahwa kita membutuhkan tipe implisitCanBuildFrom[Nothing,(Int,String),Map[Int,String]] . Tetapi adakah definisi seperti itu?

Mari kita lihat CanBuildFromdefinisi:

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

Begitu CanBuildFromjuga kontra-varian pada parameter tipe pertama. Karena Nothingkelas bawah (yaitu, itu adalah subkelas dari segalanya), itu berarti setiap kelas dapat digunakan sebagai pengganti Nothing.

Karena pembangun seperti itu ada, Scala dapat menggunakannya untuk menghasilkan output yang diinginkan.

Tentang Pembangun

Banyak metode dari perpustakaan koleksi Scala terdiri dari mengambil koleksi asli, memprosesnya entah bagaimana (dalam kasus map, mentransformasikan setiap elemen), dan menyimpan hasilnya dalam koleksi baru.

Untuk memaksimalkan penggunaan kembali kode, penyimpanan hasil ini dilakukan melalui builder ( scala.collection.mutable.Builder), yang pada dasarnya mendukung dua operasi: menambahkan elemen, dan mengembalikan koleksi yang dihasilkan. Jenis koleksi yang dihasilkan ini akan tergantung pada jenis pembangun. Dengan demikian, Listpembangun akan mengembalikan a List, Mappembangun akan mengembalikan a Map, dan seterusnya. Implementasi mapmetode tidak perlu memusatkan perhatian pada jenis hasilnya: pembangun akan mengurusnya.

Di sisi lain, itu berarti bahwa mapperlu menerima pembangun ini entah bagaimana. Masalah yang dihadapi ketika merancang Koleksi Scala 2.8 adalah bagaimana memilih pembangun terbaik. Sebagai contoh, jika saya menulis Map('a' -> 1).map(_.swap), saya ingin mendapatkan Map(1 -> 'a')kembali. Di sisi lain, a Map('a' -> 1).map(_._1)tidak dapat mengembalikan Map(mengembalikan Iterable).

Keajaiban menghasilkan yang terbaik Builderdari jenis ekspresi yang dikenal dilakukan melalui CanBuildFromimplisit ini .

Tentang CanBuildFrom

Untuk lebih menjelaskan apa yang terjadi, saya akan memberikan contoh di mana koleksi yang dipetakan adalah Mapa List. Saya akan kembali lagi Listnanti. Untuk saat ini, pertimbangkan dua ungkapan ini:

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

Yang pertama mengembalikan Mapdan yang kedua mengembalikan Iterable. Keajaiban mengembalikan koleksi yang pas adalah karya CanBuildFrom. Mari kita pertimbangkan definisi maplagi untuk memahaminya.

Metode mapini diwarisi dari TraversableLike. Ini diparameterisasi pada Bdan That, dan menggunakan tipe parameter Adan Repr, yang parameterkan kelas. Mari kita lihat kedua definisi bersama:

Kelas TraversableLikedidefinisikan sebagai:

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

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

Untuk memahami dari mana Adan Reprberasal, mari pertimbangkan definisi Mapitu sendiri:

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

Karena TraversableLikediwariskan oleh semua sifat yang meluas Map, Adan Reprbisa diwarisi dari mereka. Yang terakhir mendapatkan preferensi. Jadi, mengikuti definisi kekekalan Mapdan semua sifat yang menghubungkannya TraversableLike, kita memiliki:

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

Jika Anda melewatkan parameter tipe dari Map[Int, String]semua jalan ke bawah rantai, kami menemukan bahwa tipe yang diteruskan ke TraversableLike, dan, dengan demikian, digunakan oleh map, adalah:

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

Kembali ke contoh, peta pertama menerima fungsi tipe ((Int, String)) => (Int, Int)dan peta kedua menerima fungsi tipe ((Int, String)) => String. Saya menggunakan tanda kurung ganda untuk menekankan itu adalah tuple yang diterima, seperti itulah jenis yang Akita lihat.

Dengan informasi itu, mari pertimbangkan jenis-jenis lainnya.

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

map (_._2):
B = String

Kita dapat melihat bahwa tipe yang dikembalikan oleh yang pertama mapadalah Map[Int,Int], dan yang kedua adalah Iterable[String]. Melihat mapdefinisi itu, mudah untuk melihat bahwa ini adalah nilai dari That. Tapi dari mana asalnya?

Jika kita melihat ke dalam objek pendamping dari kelas yang terlibat, kita melihat beberapa deklarasi implisit menyediakannya. Pada objek Map:

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

Dan pada objek Iterable, yang kelasnya diperpanjang oleh Map:

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

Definisi-definisi ini menyediakan pabrik untuk diparameterisasi CanBuildFrom.

Scala akan memilih implisit paling spesifik yang tersedia. Dalam kasus pertama, itu adalah yang pertama CanBuildFrom. Dalam kasus kedua, karena yang pertama tidak cocok, ia memilih yang kedua CanBuildFrom.

Kembali ke Pertanyaan

Mari kita lihat kode untuk pertanyaan, List's dan map' s definisi (lagi) untuk melihat bagaimana jenis tersebut disimpulkan:

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 

Jenis List("London", "Paris")is List[String], jadi jenis Adan Reprdidefinisikan TraversableLikeadalah:

A = String
Repr = List[String]

Jenisnya (x => (x.length, x))adalah (String) => (Int, String), jadi jenisnya Badalah:

B = (Int, String)

Jenis yang terakhir tidak diketahui, Thatadalah jenis hasil map, dan kita sudah memiliki itu juga:

val map : Map[Int,String] =

Begitu,

That = Map[Int, String]

Itu berarti breakOutharus, tentu saja, mengembalikan jenis atau subtipe dari CanBuildFrom[List[String], (Int, String), Map[Int, String]].

Daniel C. Sobral
sumber
61
Daniel, aku bisa merendahkan jenis-jenis jawabanmu, tetapi begitu aku sampai di akhir, aku merasa belum mendapatkan pemahaman tingkat tinggi. Apa itu breakOut? Di mana nama "breakOut" berasal (apa yang saya pecah keluar)? Mengapa diperlukan dalam kasus ini untuk mendapatkan Peta? Tentunya ini ada beberapa cara untuk jawaban singkat pertanyaan-pertanyaan ini? (bahkan jika tipe panjang yang merendahkan tetap diperlukan untuk memahami setiap detail)
Seth Tisue
3
@Seth Itu kekhawatiran yang valid, tapi saya tidak yakin saya siap untuk tugas itu. Asal usul ini dapat ditemukan di sini: article.gmane.org/gmane.comp.lang.scala.internals/1812/… . Saya akan memikirkannya, tetapi, saat ini, saya tidak bisa memikirkan banyak cara untuk memperbaikinya.
Daniel C. Sobral
2
Apakah ada cara untuk menghindari menentukan seluruh tipe hasil Peta [Int, String] dan alih-alih bisa menulis sesuatu seperti: 'val map = List ("London", "Paris"). Map (x => (x. panjangnya, x)) (breakOut [... Map]) '
IttayD
9
@SethTisue Dari membaca penjelasan ini, tampaknya breakOut diperlukan untuk "keluar" dari persyaratan yang harus dibuat oleh pembuat Anda dari Daftar [String]. Kompiler menginginkan CanBuildFrom [Daftar [String], (Int, String), Peta [Int, String]], yang tidak dapat Anda berikan. Fungsi breakOut melakukan ini dengan mengalahkan parameter tipe pertama di CanBuildFrom dengan mengaturnya ke Nothing. Sekarang Anda hanya perlu memberikan CanBuildFrom [Nothing, (Int, String), Map [Int, String]]. Ini mudah karena disediakan oleh kelas Peta.
Markus
2
@ Mark Ketika saya menemukan breakOut, masalah yang saya lihat adalah cara Monad bersikeras memetakan (via bind / flatMap) ke tipe mereka sendiri. Hal ini memungkinkan seseorang untuk "keluar" dari rantai pemetaan menggunakan satu monad menjadi tipe monad yang berbeda. Saya tidak tahu apakah itu yang dipikirkan oleh Adriaan Moors (penulis)!
Ed Staub
86

Saya ingin membangun berdasarkan jawaban Daniel. Itu sangat menyeluruh, tetapi seperti disebutkan dalam komentar, itu tidak menjelaskan apa yang dilakukan breakout.

Diambil dari Re: Dukungan untuk Pembangun eksplisit (2009-10-23), berikut adalah apa yang saya yakini tidak terjadi:

Ini memberikan kompiler saran yang Builder untuk memilih secara implisit (pada dasarnya itu memungkinkan kompiler untuk memilih pabrik yang menurutnya paling cocok dengan situasi.)

Misalnya, lihat yang berikut:

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)

Anda dapat melihat tipe pengembalian secara implisit dipilih oleh kompiler untuk paling cocok dengan jenis yang diharapkan. Bergantung pada bagaimana Anda mendeklarasikan variabel penerima, Anda mendapatkan hasil yang berbeda.

Berikut ini akan menjadi cara yang setara untuk menentukan pembangun. Catatan dalam hal ini, kompiler akan menyimpulkan tipe yang diharapkan berdasarkan tipe pembangun:

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)
Austen Holmes
sumber
1
Saya ingin tahu mengapa ini dinamai " breakOut"? Saya berpikir sesuatu seperti convertatau buildADifferentTypeOfCollection(tetapi lebih pendek) mungkin lebih mudah diingat.
KajMagnus
8

Jawaban Daniel Sobral sangat bagus, dan harus dibaca bersama dengan Arsitektur Koleksi Scala (Bab 25 tentang Pemrograman dalam Scala).

Saya hanya ingin menjelaskan mengapa ini disebut breakOut:

Mengapa disebut demikian breakOut?

Karena kami ingin keluar dari satu jenis ke jenis lainnya :

Keluar dari tipe apa ke tipe apa? Mari kita lihat mapfungsi pada Seqsebagai contoh:

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

Jika kami ingin membangun Peta langsung dari pemetaan elemen-elemen urutan seperti:

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

Kompiler akan mengeluh:

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

Alasannya adalah bahwa Seq hanya tahu cara membangun Seq lain (yaitu ada CanBuildFrom[Seq[_], B, Seq[B]]pabrik pembangun implisit yang tersedia, tetapi tidak ada pabrik pembangun dari Seq ke Peta).

Untuk mengkompilasi, kita perlu entah bagaimana breakOutpersyaratan jenis , dan dapat membangun pembangun yang menghasilkan Peta untuk mapfungsi yang akan digunakan.

Seperti yang dijelaskan Daniel, breakOut memiliki tanda tangan berikut:

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()
    }

Nothingadalah subkelas dari semua kelas, sehingga setiap pabrik pembangun dapat diganti di tempat implicit b: CanBuildFrom[Nothing, T, To]. Jika kami menggunakan fungsi breakOut untuk memberikan parameter implisit:

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

Itu akan dikompilasi, karena breakOutmampu memberikan jenis yang diperlukan CanBuildFrom[Seq[(String, Int)], (String, Int), Map[String, Int]], sedangkan kompiler dapat menemukan pabrik pembangun implisit dari jenis CanBuildFrom[Map[_, _], (A, B), Map[A, B]], di tempatCanBuildFrom[Nothing, T, To] , untuk breakOut digunakan untuk membuat pembangun yang sebenarnya.

Catatan yang CanBuildFrom[Map[_, _], (A, B), Map[A, B]]didefinisikan dalam Peta, dan hanya memulai MapBuilderyang menggunakan Peta yang mendasarinya.

Semoga ini jelas.

Dzhu
sumber
4

Contoh sederhana untuk memahami apa yang breakOutdilakukan:

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]
fdietze
sumber
Terima kasih untuk contohnya! Juga val seq:Seq[Int] = set.map(_ % 2).toVectortidak akan memberi Anda nilai yang diulang karena Settelah disimpan untuk map.
Matthew Pickering
@MatthewPickering benar! set.map(_ % 2)menciptakan yang Set(1, 0)pertama, yang kemudian akan dikonversi menjadi Vector(1, 0).
fdietze