Scala 2.8 breakOut

225

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

Что здесь происходит? Почему меня breakOutназывают аргументом для моего List?

oxbow_lakes
источник
13
Тривиальный ответ: не аргумент List, а map.
Даниэль С. Собрал,

Ответы:

325

Ответ находится на определение 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используется a 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это нижний класс (т.е. это подкласс всего), это означает, что любой класс может использоваться вместо Nothing.

Поскольку такой конструктор существует, Scala может использовать его для получения желаемого результата.

О строителях

Многие методы из библиотеки коллекций Scala состоят из извлечения исходной коллекции, ее обработки каким-либо образом (в случае mapпреобразования каждого элемента) и сохранения результатов в новой коллекции.

Чтобы максимизировать повторное использование кода, это сохранение результатов осуществляется с помощью builder ( scala.collection.mutable.Builder), который в основном поддерживает две операции: добавление элементов и возврат результирующей коллекции. Тип этой результирующей коллекции будет зависеть от типа компоновщика. Таким образом, Listстроитель вернет a List, Mapстроитель вернет a Mapи так далее. Реализацияmap метода не должна зависеть от типа результата: об этом заботится строитель.

С другой стороны, это означает, что mapэтого строителя нужно как-то получить. Проблема, с которой столкнулись при разработке Scala 2.8 Collections, заключалась в том, как выбрать лучшего строителя из возможных. Например, если бы я написал Map('a' -> 1).map(_.swap), я хотел бы получить Map(1 -> 'a')обратно. С другой стороны, Map('a' -> 1).map(_._1)не может вернуть Map(он возвращает Iterable).

Магия создания наилучшего Builderиз известных типов выражений осуществляется через это CanBuildFromнеявное.

Около CanBuildFrom

Чтобы лучше объяснить, что происходит, я приведу пример, где отображаемая коллекция - это Mapвместо а List. Я вернусь к Listпозже. А пока рассмотрим эти два выражения:

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

Первое возвращает a, 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)) => (Int, Int)а вторая карта получает функцию типа ((Int, String)) => 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.

Вернуться к вопросу

Давайте посмотрим на код вопроса, определения Lists и map(снова), чтобы увидеть, как типы выводятся:

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")is List[String], поэтому типы Aи Reprопределены на TraversableLike:

A = String
Repr = List[String]

Тип для (x => (x.length, x))is (String) => (Int, String), поэтому тип Bis:

B = (Int, String)

Последний неизвестный тип That- это тип результата map, и у нас уже есть это:

val map : Map[Int,String] =

Так,

That = Map[Int, String]

Это означает breakOut, что обязательно должен возвращать тип или подтип CanBuildFrom[List[String], (Int, String), Map[Int, String]].

Даниэль С. Собрал
источник
61
Даниэль, я могу разобраться с типами в твоем ответе, но как только я доберусь до конца, я чувствую, что не получил никакого высокого уровня понимания. Что такое перерыв? Откуда происходит название "breakOut" (из чего я вырываюсь)? Зачем это нужно в этом случае, чтобы вытащить карту? Конечно, есть какой-то способ кратко ответить на эти вопросы? (даже если длительное унижение типов остается необходимым для того, чтобы уловить каждую деталь)
Сет Тисуэ
3
@ Сейт Это серьезная проблема, но я не уверен, что справлюсь с этой задачей. Происхождение этого может быть найдено здесь: article.gmane.org/gmane.comp.lang.scala.internals/1812/… . Я подумаю об этом, но сейчас я не могу придумать, как это улучшить.
Даниэль С. Собрал
2
Есть ли способ избежать указания полного типа результата Map [Int, String] и возможности написать что-то вроде: 'val map = List ("London", "Paris"). Map (x => (x. длина, х)) (breakOut [... Map]) '
IttayD
9
@SethTisue Из моего прочтения этого объяснения кажется, что breakOut необходим для того, чтобы «вырваться» из требования, которое ваш конструктор должен построить из List [String]. Компилятор хочет CanBuildFrom [List [String], (Int, String), Map [Int, String]], который вы не можете предоставить. Функция breakOut делает это путем засорения первого параметра типа в CanBuildFrom, устанавливая для него значение Nothing. Теперь вам нужно только предоставить CanBuildFrom [Nothing, (Int, String), Map [Int, String]]. Это легко, потому что это обеспечивается классом Map.
Марк
2
@Mark Когда я нашел breakOut, проблема, с которой я столкнулся, заключалась в том, что монады настаивают на отображении (через bind / flatMap) их собственного типа. Это позволяет «разрывать» цепочку отображений, используя одну монаду в другой тип монады. Я понятия не имею, думал ли об этом Адриан Мурс (автор), хотя!
Эд Стауб
86

Я хотел бы опираться на ответ Дэниела. Это было очень тщательно, но, как отмечено в комментариях, это не объясняет, что делает прорыв.

Взятый из 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)
Остин Холмс
источник
1
Интересно, почему это называется " breakOut"? Я думаю, что-то вроде convertили buildADifferentTypeOfCollection(но короче), возможно, было легче запомнить.
КаджМагнус
8

Ответ Даниэля Собрала велик, и его следует читать вместе с « Архитектурой коллекций Scala» (глава 25 «Программирование в Scala»).

Я просто хотел уточнить, почему это называется breakOut:

Почему это называется breakOut?

Потому что мы хотим вырваться из одного типа в другой :

Вырваться из какого типа в какой тип? Давайте посмотрим на mapфункцию Seqв качестве примера:

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

Если мы хотим построить карту непосредственно из отображения элементов последовательности, таких как:

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

Для того, чтобы скомпилировать, нам нужно как-то breakOutсоответствовать требованию к типу и быть в состоянии построить конструктор, который создает Map для использования 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 и просто инициирует объект, 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]
fdietze
источник
Спасибо за пример! Также val seq:Seq[Int] = set.map(_ % 2).toVectorне даст вам повторных значений, как Setбыло сохранено для map.
Мэтью Пикеринг
@ MatthewPickering правильно! set.map(_ % 2)создает Set(1, 0)первый, который затем преобразуется в Vector(1, 0).
fdietze