Scala 2.8 phá vỡ


225

Trong Scala 2.8 , có một đối tượng trong 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()
 }

Tôi đã nói rằng kết quả này trong:

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

Chuyện gì đang xảy ra ở đây? Tại sao breakOutđược gọi là một đối số với tôi List?


13
Câu trả lời tầm thường là, nó không phải là một đối số List, mà là map.
Daniel C. Sobral

Câu trả lời:


325

Câu trả lời được tìm thấy trên định nghĩa của map:

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

Lưu ý rằng nó có hai tham số. Đầu tiên là chức năng của bạn và thứ hai là ẩn. Nếu bạn không cung cấp ẩn ý đó, Scala sẽ chọn một cái cụ thể nhất có sẵn.

Trong khoảng breakOut

Vậy, mục đích của nó là breakOutgì? Xem xét ví dụ được đưa ra cho câu hỏi, Bạn lấy một danh sách các chuỗi, chuyển đổi từng chuỗi thành một tuple (Int, String)và sau đó tạo ra một Mapchuỗi. Cách rõ ràng nhất để làm điều đó sẽ tạo ra một List[(Int, String)]bộ sưu tập trung gian , và sau đó chuyển đổi nó.

Cho rằng mapsử dụng a Builderđể tạo ra bộ sưu tập kết quả, không thể bỏ qua trung gian Listvà thu thập kết quả trực tiếp vào một Map? Rõ ràng, vâng, nó là. Để làm như vậy, tuy nhiên, chúng ta cần phải vượt qua một thích hợp CanBuildFromđể map, và đó là chính xác những gì breakOutkhông.

Sau đó, hãy nhìn vào định nghĩa của 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()
  }

Lưu ý rằng breakOutđược tham số hóa và nó trả về một thể hiện của CanBuildFrom. Khi nó xảy ra, các loại From, TTođã được suy luận, bởi vì chúng tôi biết rằng đó maplà mong đợi CanBuildFrom[List[String], (Int, String), Map[Int, String]]. Vì thế:

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

Để kết luận chúng ta hãy kiểm tra ngầm nhận được breakOut. Nó là loại CanBuildFrom[Nothing,T,To]. Chúng tôi đã biết tất cả các loại này, vì vậy chúng tôi có thể xác định rằng chúng tôi cần một loại ngầm địnhCanBuildFrom[Nothing,(Int,String),Map[Int,String]] . Nhưng có một định nghĩa như vậy?

Hãy xem CanBuildFromđịnh nghĩa của:

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

Vì vậy, CanBuildFrombiến thể contra trên tham số loại đầu tiên của nó. Bởi vì Nothinglà một lớp dưới cùng (nghĩa là nó là một lớp con của mọi thứ), điều đó có nghĩa là bất kỳ lớp nào cũng có thể được sử dụng thay thế Nothing.

Vì một trình xây dựng như vậy tồn tại, Scala có thể sử dụng nó để tạo ra đầu ra mong muốn.

Giới thiệu về nhà xây dựng

Rất nhiều phương pháp từ thư viện bộ sưu tập của Scala bao gồm lấy bộ sưu tập gốc, xử lý nó bằng cách nào đó (trong trường hợp map, biến đổi từng phần tử) và lưu trữ kết quả trong bộ sưu tập mới.

Để tối đa hóa việc sử dụng lại mã, việc lưu trữ kết quả này được thực hiện thông qua trình xây dựng ( scala.collection.mutable.Builder), về cơ bản hỗ trợ hai thao tác: nối các phần tử và trả về bộ sưu tập kết quả. Loại của bộ sưu tập kết quả này sẽ phụ thuộc vào loại của người xây dựng. Do đó, một Listngười xây dựng sẽ trả lại một List, một Mapngười xây dựng sẽ trả lại một Map, v.v. Việc thực hiện mapphương thức không cần phải quan tâm đến loại kết quả: người xây dựng sẽ chăm sóc nó.

Mặt khác, điều đó có nghĩa là mapcần phải nhận được trình xây dựng này bằng cách nào đó. Vấn đề gặp phải khi thiết kế Bộ sưu tập Scala 2.8 là làm thế nào để chọn người xây dựng tốt nhất có thể. Ví dụ, nếu tôi viết Map('a' -> 1).map(_.swap), tôi muốn lấy Map(1 -> 'a')lại. Mặt khác, Map('a' -> 1).map(_._1)không thể trả về một Map(nó trả về một Iterable).

Phép thuật tạo ra thứ tốt nhất có thể Buildertừ các loại biểu thức đã biết được thực hiện thông qua CanBuildFromẩn này .

Trong khoảng CanBuildFrom

Để giải thích rõ hơn những gì đang diễn ra, tôi sẽ đưa ra một ví dụ trong đó bộ sưu tập được ánh xạ Mapthay vì a List. Tôi sẽ quay lại Listsau. Bây giờ, hãy xem xét hai biểu thức sau:

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

Cái đầu tiên trả về a Mapvà cái thứ hai trả về một Iterable. Sự kỳ diệu của việc trả lại một bộ sưu tập phù hợp là công việc của CanBuildFrom. Hãy xem xét định nghĩa của mapmột lần nữa để hiểu nó.

Phương pháp mapđược kế thừa từ TraversableLike. Nó được tham số hóa trên BThat, và sử dụng các tham số loại ARepr, tham số hóa lớp. Chúng ta hãy xem cả hai định nghĩa cùng nhau:

Lớp TraversableLikeđược định nghĩa là:

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

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

Để hiểu nơi AReprđến từ đâu, hãy xem xét định nghĩa của Mapchính nó:

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

Bởi vì TraversableLikeđược thừa hưởng tất cả những đặc điểm mà mở rộng Map, AReprcó thể được thừa hưởng từ ai trong số họ. Người cuối cùng được ưu tiên, mặc dù. Vì vậy, theo định nghĩa của bất biến Mapvà tất cả các đặc điểm kết nối nó với TraversableLike, chúng ta có:

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

Nếu bạn chuyển các tham số loại của Map[Int, String]tất cả các chuỗi xuống, chúng tôi thấy rằng các loại được truyền cho TraversableLike, và, do đó, được sử dụng bởi map, là:

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

Quay trở lại ví dụ, bản đồ thứ nhất đang nhận chức năng loại ((Int, String)) => (Int, Int)và bản đồ thứ hai đang nhận chức năng loại ((Int, String)) => String. Tôi sử dụng dấu ngoặc đơn để nhấn mạnh rằng đó là một bộ dữ liệu được nhận, vì đó là loại Anhư chúng ta đã thấy.

Với thông tin đó, hãy xem xét các loại khác.

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

map (_._2):
B = String

Chúng ta có thể thấy rằng loại được trả về bởi cái đầu tiên mapMap[Int,Int], và cái thứ hai là Iterable[String]. Nhìn vào mapđịnh nghĩa của nó, dễ dàng nhận thấy đây là những giá trị của That. Nhưng họ đến từ đâu?

Nếu chúng ta nhìn vào bên trong các đối tượng đồng hành của các lớp liên quan, chúng ta sẽ thấy một số khai báo ngầm cung cấp chúng. Về đối tượng Map:

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

Và trên đối tượng Iterable, có lớp được mở rộng bởi Map:

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

Những định nghĩa này cung cấp cho các nhà máy tham số hóa CanBuildFrom.

Scala sẽ chọn ẩn cụ thể nhất có sẵn. Trong trường hợp đầu tiên, đó là lần đầu tiên CanBuildFrom. Trong trường hợp thứ hai, vì lần đầu tiên không khớp, nó đã chọn lần thứ hai CanBuildFrom.

Quay lại câu hỏi

Chúng ta hãy xem các mã cho các câu hỏi, List's và map' s định nghĩa (một lần nữa) để xem cách các loại được suy ra:

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 

Loại List("London", "Paris")List[String], vì vậy các loại AReprđược xác định TraversableLikelà:

A = String
Repr = List[String]

Loại cho (x => (x.length, x))(String) => (Int, String), vì vậy loại Blà:

B = (Int, String)

Loại không xác định cuối cùng, Thatlà loại kết quả mapvà chúng ta cũng đã có loại đó:

val map : Map[Int,String] =

Vì thế,

That = Map[Int, String]

Điều đó có nghĩa là breakOut, nhất thiết phải trả về một loại hoặc kiểu con của CanBuildFrom[List[String], (Int, String), Map[Int, String]].


61
Daniel, tôi có thể mò mẫm các loại trong câu trả lời của bạn, nhưng một khi tôi đi đến cuối cùng, tôi cảm thấy như mình chưa đạt được bất kỳ hiểu biết cấp cao nào. Có gì đột phá? Cái tên "breakOut" đến từ đâu (tôi đang thoát ra khỏi cái gì)? Tại sao cần thiết trong trường hợp này để lấy Bản đồ ra? Chắc chắn là có một số cách để trả lời ngắn gọn những câu hỏi này? (ngay cả khi việc dò tìm kiểu dài vẫn cần thiết để nắm bắt từng chi tiết)
Seth Tisue

3
@ Vì đó là một mối quan tâm hợp lệ, nhưng tôi không chắc là tôi hoàn thành nhiệm vụ. Nguồn gốc của điều này có thể được tìm thấy ở đây: article.gmane.org/gmane.comp.lang.scala.i INTERNals/1812/ . Tôi sẽ nghĩ về nó, nhưng, ngay bây giờ, tôi không thể nghĩ ra nhiều cách để cải thiện nó.
Daniel C. Sobral

2
Có cách nào để tránh chỉ định toàn bộ loại kết quả của Bản đồ [Int, String] và thay vào đó có thể viết một cái gì đó như: 'val map = List ("London", "Paris"). Map (x => (x. chiều dài, x)) (breakOut [... Bản đồ]) '
IttayD

9
@SethTisue Từ việc tôi đọc giải thích này, có vẻ như breakOut là cần thiết để "phá vỡ" yêu cầu mà nhà xây dựng của bạn cần xây dựng từ Danh sách [Chuỗi]. Trình biên dịch muốn có CanBuildFrom [List [String], (Int, String), Map [Int, String]], mà bạn không thể cung cấp. Hàm breakOut thực hiện điều này bằng cách ghi đè tham số loại đầu tiên trong CanBuildFrom bằng cách đặt nó thành Không có gì. Bây giờ bạn chỉ phải cung cấp CanBuildFrom [Không có gì, (Int, String), Map [Int, String]]. Điều này thật dễ dàng vì nó được cung cấp bởi lớp Map.
Đánh dấu

2
@Mark Khi tôi tìm thấy breakOut, vấn đề mà tôi thấy nó giải quyết là cách mà các đơn vị khăng khăng ánh xạ (thông qua bind / FlatMap) theo kiểu riêng của họ. Nó cho phép một người "thoát ra" chuỗi ánh xạ bằng cách sử dụng một đơn nguyên thành một loại đơn nguyên khác. Tôi không biết liệu đó là những gì Adriaan Moors (tác giả) đã nghĩ về nó, mặc dù!
Ed Staub

86

Tôi muốn xây dựng theo câu trả lời của Daniel. Nó rất kỹ lưỡng, nhưng như đã lưu ý trong các bình luận, nó không giải thích được những gì đột phá.

Lấy từ Re: Hỗ trợ cho các Nhà xây dựng rõ ràng (2009-10-23), đây là những gì tôi tin rằng đột phá:

Nó cung cấp cho trình biên dịch một gợi ý về việc Builder sẽ chọn ngầm (về cơ bản nó cho phép trình biên dịch chọn nhà máy nào mà nó cho là phù hợp nhất với tình huống.)

Ví dụ, xem như sau:

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)

Bạn có thể thấy kiểu trả về được trình biên dịch ngầm chọn để phù hợp nhất với loại dự kiến. Tùy thuộc vào cách bạn khai báo biến nhận, bạn nhận được kết quả khác nhau.

Sau đây sẽ là một cách tương đương để chỉ định một người xây dựng. Lưu ý trong trường hợp này, trình biên dịch sẽ suy ra loại dự kiến ​​dựa trên loại của trình tạo:

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)

1
Tôi tự hỏi tại sao nó có tên " breakOut"? Tôi đang nghĩ một cái gì đó như converthoặc buildADifferentTypeOfCollection(nhưng ngắn hơn) có thể dễ nhớ hơn.
KajMagnus

8

Câu trả lời của Daniel Sobral rất hay, và nên được đọc cùng với Architecture of Scala Collection Collection (Chương 25 của Lập trình trong Scala).

Tôi chỉ muốn giải thích lý do tại sao nó được gọi là breakOut:

Tại sao nó được gọi là breakOut?

Bởi vì chúng tôi muốn thoát ra khỏi một loại và thành một loại khác :

Thoát ra khỏi loại nào thành loại nào? Hãy xem xét maphàm trên Seqnhư một ví dụ:

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

Nếu chúng tôi muốn xây dựng Bản đồ trực tiếp từ ánh xạ qua các phần tử của chuỗi, chẳng hạn như:

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

Trình biên dịch sẽ phàn nàn:

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

Lý do là Seq chỉ biết cách xây dựng một Seq khác (tức là có sẵn một CanBuildFrom[Seq[_], B, Seq[B]]nhà máy xây dựng ngầm , nhưng KHÔNG có nhà máy xây dựng nào từ Seq đến Map).

Để biên dịch, chúng ta cần bằng cách nào đó breakOutyêu cầu loại và có thể xây dựng một trình xây dựng tạo Bản đồ cho maphàm để sử dụng.

Như Daniel đã giải thích, breakOut có chữ ký sau:

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

Nothinglà một lớp con của tất cả các lớp, vì vậy bất kỳ nhà máy xây dựng nào cũng có thể được thay thế implicit b: CanBuildFrom[Nothing, T, To]. Nếu chúng ta đã sử dụng hàm breakOut để cung cấp tham số ẩn:

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

Nó sẽ biên dịch, bởi vì breakOutcó thể cung cấp loại yêu cầu CanBuildFrom[Seq[(String, Int)], (String, Int), Map[String, Int]], trong khi trình biên dịch có thể tìm thấy một nhà máy xây dựng ngầm định loại CanBuildFrom[Map[_, _], (A, B), Map[A, B]], thay CanBuildFrom[Nothing, T, To]cho breakOut để sử dụng để tạo trình xây dựng thực tế.

Lưu ý rằng CanBuildFrom[Map[_, _], (A, B), Map[A, B]]được xác định trong Bản đồ và chỉ cần khởi MapBuildertạo bản đồ sử dụng Bản đồ cơ bản.

Hy vọng điều này sẽ làm rõ ràng mọi thứ.


4

Một ví dụ đơn giản để hiểu những gì breakOutkhông:

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]

Cảm ơn ví dụ! Cũng val seq:Seq[Int] = set.map(_ % 2).toVectorsẽ không cung cấp cho bạn các giá trị lặp lại như Setđã được bảo tồn cho map.
Matthew Pickering

@MatthewPickering đúng! set.map(_ % 2)tạo một cái Set(1, 0)đầu tiên, sau đó được chuyển đổi thành a Vector(1, 0).
fdietze
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.