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)

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

Ответы

Ответ 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

So CanBuildFrom является противоречивым по своему первому параметру. Поскольку Nothing является нижним классом (т.е. Является подклассом всего), это означает, что вместо Nothing можно использовать любой класс.

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

О конструкторах

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

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

С другой стороны, это означает, что map должен каким-то образом получить этот конструктор. Проблема при проектировании Scala 2.8 Коллекции заключалась в том, как выбрать лучший строитель. Например, если бы я написал 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.

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

Посмотрите код вопроса, List и 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") равен 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]].

Ответ 2

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

Взято из Re: Поддержка явных сборщиков (2009-10-23), вот что я считаю, что прорыв:

Он дает компилятору предложение относительно того, какой Builder выбрать неявно (по сути, он позволяет компилятору выбрать, какой factory он считает лучше всего подходит).

Например, см. следующее:

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)

Ответ 3

Даниэль Собрал ответ велик, и его следует читать вместе с Архитектура 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]] builder factory, но существует конструктор NO factory от Seq to 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 является подклассом всех классов, поэтому любой строитель factory может быть заменен вместо 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]], в то время как компилятор может найти неявный строитель factory типа 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]