Scala 2.8 breakOut


225

In Scala 2.8 , c'è un oggetto in 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()
 }

Mi è stato detto che questo si traduce in:

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

Che cosa sta succedendo qui? Perché viene breakOutchiamato come argomento per il mio List?


13
Essendo la banale risposta, non è un argomento per List, ma per map.
Daniel C. Sobral,

Risposte:


325

La risposta si trova sulla definizione di map:

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

Si noti che ha due parametri. La prima è la tua funzione e la seconda è implicita. Se non fornisci questo implicito, Scala sceglierà il più specifico disponibile.

Di breakOut

Allora, qual è lo scopo breakOut? Considera l'esempio fornito per la domanda, prendi un elenco di stringhe, trasformi ogni stringa in una tupla (Int, String)e quindi ne produci una Map. Il modo più ovvio per farlo produrrebbe una List[(Int, String)]raccolta intermedia e poi la convertirà.

Dato che maputilizza a Builderper produrre la raccolta risultante, non sarebbe possibile saltare l'intermediario Liste raccogliere i risultati direttamente in a Map? Evidentemente sì, lo è. Per farlo, però, abbiamo bisogno di passare una vera e propria CanBuildFroma map, e questo è esattamente quello che breakOutfa.

Diamo un'occhiata, quindi, alla definizione di 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()
  }

Si noti che breakOutè parametrizzato e che restituisce un'istanza di CanBuildFrom. Come spesso accade, i tipi From, Te Tosono già state dedurre, perché sappiamo che mapsi aspetta CanBuildFrom[List[String], (Int, String), Map[Int, String]]. Perciò:

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

Per concludere esaminiamo l'implicito ricevuto da breakOutsolo. È di tipo CanBuildFrom[Nothing,T,To]. Conosciamo già tutti questi tipi, quindi possiamo determinare che abbiamo bisogno di un tipo implicito CanBuildFrom[Nothing,(Int,String),Map[Int,String]]. Ma esiste una tale definizione?

Diamo un'occhiata alla CanBuildFromdefinizione di:

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

Quindi CanBuildFromè una contro-variante sul suo primo parametro di tipo. Perché Nothingè una classe inferiore (cioè, è una sottoclasse di tutto), ciò significa che qualsiasi classe può essere utilizzata al posto di Nothing.

Poiché esiste un tale costruttore, Scala può usarlo per produrre l'output desiderato.

Informazioni sui costruttori

Molti metodi della libreria delle collezioni di Scala consistono nel prendere la collezione originale, elaborarla in qualche modo (nel caso di map, trasformare ciascun elemento) e archiviare i risultati in una nuova collezione.

Per massimizzare il riutilizzo del codice, questa memorizzazione dei risultati viene eseguita tramite un builder ( scala.collection.mutable.Builder), che sostanzialmente supporta due operazioni: aggiungere elementi e restituire la raccolta risultante. Il tipo di questa raccolta risultante dipenderà dal tipo di builder. Pertanto, un Listbuilder restituirà a List, un Mapbuilder restituirà a Mape così via. L'implementazione del mapmetodo non deve riguardare il tipo di risultato: il costruttore si prende cura di esso.

D'altra parte, ciò significa che mapdeve ricevere questo costruttore in qualche modo. Il problema affrontato durante la progettazione delle collezioni Scala 2.8 era come scegliere il miglior costruttore possibile. Ad esempio, se dovessi scrivere Map('a' -> 1).map(_.swap), mi piacerebbe avere una Map(1 -> 'a')risposta. D'altra parte, a Map('a' -> 1).map(_._1)non può restituire a Map(restituisce a Iterable).

La magia di produrre il meglio possibile Builderdai tipi noti dell'espressione viene eseguita attraverso questo CanBuildFromimplicito.

Di CanBuildFrom

Per spiegare meglio cosa sta succedendo, fornirò un esempio in cui la raccolta da mappare è a Mapanziché a List. Tornerò più Listtardi. Per ora, considera queste due espressioni:

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

Il primo restituisce a Mape il secondo restituisce an Iterable. La magia di restituire una collezione adatta è il lavoro di CanBuildFrom. Consideriamo di mapnuovo la definizione per capirla.

Il metodo mapè ereditato da TraversableLike. È parametrizzato su Be That, e utilizza i parametri di tipo Ae Repr, che parametrizzano la classe. Vediamo insieme entrambe le definizioni:

La classe TraversableLikeè definita come:

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

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

Per capire da dove Ae Reprda dove veniamo, consideriamo la definizione di Mapse stesso:

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

Perché TraversableLikeviene ereditata da tutti i tratti che si estendono Map, Ae Reprpotrebbe essere ereditato da nessuno di loro. L'ultimo ottiene la preferenza, però. Quindi, seguendo la definizione dell'immutabile Mape tutti i tratti che lo collegano TraversableLike, abbiamo:

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

Se passi i parametri di Map[Int, String]tipo lungo tutta la catena, scopriamo che i tipi passati a TraversableLike, e, quindi, utilizzati da map, sono:

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

Tornando all'esempio, la prima mappa riceve una funzione di tipo ((Int, String)) => (Int, Int)e la seconda mappa riceve una funzione di tipo ((Int, String)) => String. Uso la doppia parentesi per sottolineare che si tratta di una tupla ricevuta, visto che è il tipo di Acome abbiamo visto.

Con queste informazioni, consideriamo gli altri tipi.

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

map (_._2):
B = String

Possiamo vedere che il tipo restituito dal primo mapè Map[Int,Int]e il secondo è Iterable[String]. Guardando mapla definizione, è facile vedere che questi sono i valori di That. Ma da dove vengono?

Se guardiamo all'interno degli oggetti compagni delle classi coinvolte, vediamo alcune dichiarazioni implicite che li forniscono. Sull'oggetto Map:

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

E sull'oggetto Iterable, la cui classe è estesa da Map:

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

Queste definizioni forniscono fabbriche per parametri CanBuildFrom.

Scala sceglierà l'implicito più specifico disponibile. Nel primo caso, è stato il primo CanBuildFrom. Nel secondo caso, poiché il primo non corrisponde, ha scelto il secondo CanBuildFrom.

Torna alla domanda

Vediamo il codice per la domanda, la definizione di " Liste map" (di nuovo) per vedere come vengono dedotti i tipi:

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 

Il tipo di List("London", "Paris")è List[String], quindi i tipi Ae Reprdefiniti su TraversableLikesono:

A = String
Repr = List[String]

Il tipo per (x => (x.length, x))è (String) => (Int, String), quindi il tipo di Bè:

B = (Int, String)

L'ultimo tipo sconosciuto, Thatè il tipo del risultato di map, e già lo abbiamo anche:

val map : Map[Int,String] =

Così,

That = Map[Int, String]

Ciò significa che breakOutdeve necessariamente restituire un tipo o sottotipo di CanBuildFrom[List[String], (Int, String), Map[Int, String]].


61
Daniel, riesco a sfogliare i tipi nella tua risposta, ma una volta arrivato alla fine, mi sento come se non avessi acquisito alcuna comprensione di alto livello. Qual è Breakout? Da dove viene il nome "breakOut" (da cosa sto uscendo)? Perché è necessario in questo caso per ottenere una mappa? Esiste sicuramente un modo per rispondere brevemente a queste domande? (anche se il lungo groveling di tipo rimane necessario per cogliere ogni dettaglio)
Seth Tisue

3
@Seth Questa è una preoccupazione valida, ma non sono sicuro di essere all'altezza. L'origine di questo può essere trovata qui: article.gmane.org/gmane.comp.lang.scala.internals/1812/… . Ci penserò, ma, in questo momento, non riesco a pensare a un modo per migliorarlo.
Daniel C. Sobral,

2
C'è un modo per evitare di specificare l'intero tipo di risultato di Map [Int, String] e invece di poter scrivere qualcosa del tipo: 'val map = List ("London", "Paris"). Map (x => (x. lunghezza, x)) (breakOut [... Mappa]) '
IttayD

9
@SethTisue Dalla mia lettura di questa spiegazione, sembra che breakOut sia necessario per "rompere" il requisito che il tuo costruttore deve costruire da un Elenco [String]. Il compilatore desidera un CanBuildFrom [List [String], (Int, String), Map [Int, String]], che non è possibile fornire. La funzione breakOut lo fa bloccando il primo parametro di tipo in CanBuildFrom impostandolo su Nothing. Ora devi solo fornire un CanBuildFrom [Nothing, (Int, String), Map [Int, String]]. Questo è facile perché è fornito dalla classe Map.
Segna il

2
@Mark Quando ho trovato breakOut, il problema che ho visto affrontare era il modo in cui le monadi insistono nel mappare (tramite bind / flatMap) con il proprio tipo. Permette di "spezzare" una catena di mappatura usando una monade in un diverso tipo di monade. Non ho idea di come ci pensasse Adriaan Moors (l'autore)!
Ed Staub,

86

Vorrei basarmi sulla risposta di Daniel. È stato molto approfondito, ma come notato nei commenti, non spiega cosa fa breakout.

Tratto da Re: Supporto per costruttori espliciti (23-10-2009), ecco cosa credo che breakout faccia:

Fornisce al compilatore un suggerimento su quale Builder scegliere implicitamente (essenzialmente consente al compilatore di scegliere quale factory ritiene più adatta alla situazione).

Ad esempio, vedere quanto segue:

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)

Puoi vedere che il tipo di ritorno è scelto implicitamente dal compilatore per abbinare meglio il tipo previsto. A seconda di come si dichiara la variabile di ricezione, si ottengono risultati diversi.

Di seguito sarebbe un modo equivalente per specificare un builder. Nota in questo caso, il compilatore inferirà il tipo previsto in base al tipo del costruttore:

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
Mi chiedo perché si chiama " breakOut"? Sto pensando che qualcosa di simile converto buildADifferentTypeOfCollection(ma più breve) potrebbe essere stato più facile da ricordare.
KajMagnus

8

La risposta di Daniel Sobral è ottima e dovrebbe essere letta insieme alle raccolte Architecture of Scala (capitolo 25 della programmazione in Scala).

Volevo solo approfondire il motivo per cui si chiama breakOut:

Perché si chiama breakOut?

Perché vogliamo uscire da un tipo e in un altro :

Rompere di che tipo in quale tipo? Vediamo la mapfunzione Seqcome esempio:

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

Se volessimo costruire una mappa direttamente dalla mappatura sugli elementi di una sequenza come:

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

Il compilatore si lamenterebbe:

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

Il motivo è che Seq sa solo come costruire un altro Seq (cioè c'è una CanBuildFrom[Seq[_], B, Seq[B]]fabbrica di costruttori impliciti disponibile, ma NON c'è nessuna fabbrica di costruttori da Seq a Mappa).

Per compilare, abbiamo bisogno in qualche modo breakOutdel requisito di tipo ed essere in grado di costruire un builder che produce una mappa per la mapfunzione da utilizzare.

Come ha spiegato Daniel, breakOut ha la seguente firma:

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è una sottoclasse di tutte le classi, quindi è possibile sostituire qualsiasi factory builder al posto di implicit b: CanBuildFrom[Nothing, T, To]. Se abbiamo utilizzato la funzione breakOut per fornire il parametro implicito:

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

Si compilerebbe, perché breakOutè in grado di fornire il tipo richiesto di CanBuildFrom[Seq[(String, Int)], (String, Int), Map[String, Int]], mentre il compilatore è in grado di trovare una factory di tipo implicito di tipo CanBuildFrom[Map[_, _], (A, B), Map[A, B]], al posto di CanBuildFrom[Nothing, T, To], da usare per breakOut per creare il vero builder.

Nota che CanBuildFrom[Map[_, _], (A, B), Map[A, B]]è definito in Mappa e avvia semplicemente un MapBuilderche utilizza una Mappa sottostante.

Mi auguro questo chiarisca tutto.


4

Un semplice esempio per capire cosa breakOutfa:

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]

Grazie per l'esempio! Inoltre val seq:Seq[Int] = set.map(_ % 2).toVectornon ti darà i valori ripetuti come è Setstato conservato per il map.
Matthew Pickering,

@MatthewPickering corretto! set.map(_ % 2)crea un Set(1, 0)primo, che poi viene convertito in a Vector(1, 0).
fdietze,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.