Перерва Scala 2.8


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 ?


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, полягала в тому, як вибрати найкращого будівельника. Наприклад, якби писати 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а другий повертає an 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]].


61
Даніеле, я можу роздивитися типи у вашій відповіді, але як тільки я закінчу, я відчуваю, що не здобув розуміння на високому рівні. Що таке breakOut? Звідки походить назва "breakOut" (що я вириваю)? Навіщо це потрібно в цьому випадку для того, щоб витягнути карту? Звичайно, є якийсь спосіб коротко відповісти на ці питання? (навіть якщо тривалий шпунтування залишається необхідним для того, щоб зрозуміти кожну деталь)
Seth Tisue

3
@Seth Це обгрунтована проблема, але я не впевнений, що я до цього завдання. Походження цього можна дізнатися тут: article.gmane.org/gmane.comp.lang.scala.internals/1812/… . Я подумаю над цим, але зараз я не можу придумати багато способів його вдосконалити.
Даніель К. Собрал

2
Чи є спосіб уникнути вказівки всього типу результату Map [Int, String] і натомість мати можливість написати щось на зразок: 'val map = List ("Лондон", "Париж"). Map (x => (x. довжина, х)) (breakOut [... Карта]) '
IttayD

9
@SethTisue З мого читання цього пояснення, здається, що breakOut необхідно "вирвати" з вимоги, яку повинен створити ваш будівельник зі списку [String]. Компілятор хоче CanBuildFrom [Список [String], (Int, String), Map [Int, String]], який ви не можете надати. Функція breakOut робить це, клобуючи параметр першого типу в CanBuildFrom, встановивши його на Ніщо. Тепер вам потрібно лише надати 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(але коротше), можливо, було б простіше запам'ятати.
KajMagnus

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]]конструктор завод доступна, але є NO будівельник завод з посліду на карту).

Для компіляції нам потрібно якось 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що використовує базову Map.

Сподіваюсь, це все прояснить.


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]

Дякую за приклад! Також val seq:Seq[Int] = set.map(_ % 2).toVectorне дасть вам повторних значень, оскільки Setзбережено для map.
Меттью Пікерінг

@MatthewPickering правильний! set.map(_ % 2)створює Set(1, 0)перше, яке потім перетворюється на a Vector(1, 0).
fdietze
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.