224 votes

Scala 2.8 breakOut

En Scala 2.8 il y a un objet dans 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()
 }

On m'a dit que cela entraîne :

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

Qu'est-ce qui se passe ici ? Pourquoi est-ce que breakOut être appelé comme argument à mon List ?

324voto

Daniel C. Sobral Points 159554

La réponse se trouve dans la définition de map :

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

Notez qu'il a deux paramètres. Le premier est votre fonction et le second est un implicite. Si vous ne fournissez pas cet implicite, Scala choisira la fonction la plus appropriée. spécifique un disponible.

À propos de breakOut

Alors, quel est le but de breakOut ? Considérons l'exemple donné pour la question, Vous prenez une liste de chaînes de caractères, transformez chaque chaîne en un tuple (Int, String) et produire ensuite un Map en sortir. La façon la plus évidente de le faire serait de produire un intermédiaire List[(Int, String)] et ensuite la convertir.

Étant donné que map utilise un Builder pour produire la collection résultante, ne serait-il pas possible de sauter l'intermédiaire List et recueillir les résultats directement dans un Map ? A l'évidence, oui, c'est le cas. Pour ce faire, cependant, nous devons passer une bonne CanBuildFrom a map et c'est exactement ce que breakOut fait.

Examinons donc la définition de l'expression suivante 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()
  }

Notez que breakOut est paramétrée, et qu'elle renvoie une instance de CanBuildFrom . Il se trouve que les types From , T y To ont déjà été déduites, parce que nous savons que map attend CanBuildFrom[List[String], (Int, String), Map[Int, String]] . Par conséquent :

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

Pour conclure, examinons l'implicite reçu par breakOut lui-même. Il est de type CanBuildFrom[Nothing,T,To] . Nous connaissons déjà tous ces types, nous pouvons donc déterminer que nous avons besoin d'un implicite de type CanBuildFrom[Nothing,(Int,String),Map[Int,String]] . Mais existe-t-il une telle définition ?

Regardons CanBuildFrom La définition de l'UE :

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

Alors CanBuildFrom est contra-variant sur son premier paramètre de type. Parce que Nothing est une classe inférieure (c'est-à-dire qu'elle est une sous-classe de tout), ce qui signifie que tout peut être utilisée à la place de la classe Nothing .

Puisqu'un tel constructeur existe, Scala peut l'utiliser pour produire le résultat souhaité.

À propos des constructeurs

De nombreuses méthodes de la bibliothèque de collections de Scala consistent à prendre la collection originale, à la traiter d'une manière ou d'une autre (dans le cas de map en transformant chaque élément), et en stockant les résultats dans une nouvelle collection.

Afin de maximiser la réutilisation du code, ce stockage des résultats se fait par le biais d'un fichier de type constructeur ( scala.collection.mutable.Builder ), qui supporte essentiellement deux opérations : l'ajout d'éléments et le retour de la collection résultante. Le type de cette collection résultante dépendra du type du constructeur. Ainsi, un List retournera un List , a Map retournera un Map et ainsi de suite. La mise en œuvre de la map ne doit pas se préoccuper du type du résultat : le constructeur s'en charge.

D'un autre côté, cela signifie que map doit recevoir ce constructeur d'une manière ou d'une autre. Le problème rencontré lors de la conception de Scala 2.8 Collections était de savoir comment choisir le meilleur constructeur possible. Par exemple, si je devais écrire Map('a' -> 1).map(_.swap) J'aimerais avoir un Map(1 -> 'a') dos. D'autre part, un Map('a' -> 1).map(_._1) ne peut pas retourner un Map (il renvoie un Iterable ).

La magie de produire le meilleur possible Builder à partir des types connus de l'expression est effectuée à travers cette CanBuildFrom implicite.

À propos de CanBuildFrom

Pour mieux expliquer ce qui se passe, je vais donner un exemple où la collection mise en correspondance est une Map au lieu d'un List . Je vais retourner à List plus tard. Pour l'instant, considérez ces deux expressions :

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

La première renvoie un Map et la seconde renvoie un Iterable . La magie du retour d'une collection appropriée est l'œuvre de CanBuildFrom . Considérons la définition de map encore une fois pour le comprendre.

La méthode map est hérité de TraversableLike . Elle est paramétrée sur B y That et utilise les paramètres de type A y Repr qui paramètrent la classe. Voyons les deux définitions ensemble :

La classe TraversableLike est défini comme suit :

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

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

Pour comprendre où A y Repr proviennent, considérons la définition de Map lui-même :

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

Parce que TraversableLike est héritée par tous les traits qui prolongent Map , A y Repr pourrait être hérité de n'importe lequel d'entre eux. Mais c'est le dernier qui a la préférence. Ainsi, en suivant la définition de l'immuable Map et tous les traits qui le relient à TraversableLike nous avons :

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

Si vous passez les paramètres de type de Map[Int, String] tout au long de la chaîne, nous constatons que les types transmis à TraversableLike et, par conséquent, utilisé par map sont :

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

Pour revenir à l'exemple, la première carte reçoit une fonction de type ((Int, String)) => (Int, Int) et la seconde carte reçoit une fonction de type ((Int, String)) => String . J'utilise la double parenthèse pour insister sur le fait qu'il s'agit d'un tuple reçu, car c'est le type d'information que l'on reçoit. A comme nous l'avons vu.

Avec cette information, examinons les autres types.

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

map (_._2):
B = String

Nous pouvons voir que le type retourné par le premier map est Map[Int,Int] et le second est Iterable[String] . En regardant map il est facile de voir que ce sont les valeurs de That . Mais d'où viennent-ils ?

Si nous regardons à l'intérieur des objets compagnons des classes concernées, nous voyons des déclarations implicites les fournissant. Sur l'objet Map :

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

Et sur l'objet Iterable dont la classe est prolongée par Map :

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

Ces définitions fournissent des usines pour paramétrer CanBuildFrom .

Scala choisira l'implicite le plus spécifique disponible. Dans le premier cas, c'est le premier CanBuildFrom . Dans le second cas, comme le premier ne correspondait pas, il a choisi le second CanBuildFrom .

Retour à la question

Voyons le code de la question, List et map (encore) pour voir comment les types sont déduits :

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 

Le type de List("London", "Paris") est List[String] donc les types A y Repr défini sur TraversableLike sont :

A = String
Repr = List[String]

Le type de (x => (x.length, x)) est (String) => (Int, String) donc le type de B est :

B = (Int, String)

Le dernier type inconnu, That est le type du résultat de map et nous l'avons déjà :

val map : Map[Int,String] =

Donc,

That = Map[Int, String]

Cela signifie que breakOut doit, nécessairement, retourner un type ou un sous-type de CanBuildFrom[List[String], (Int, String), Map[Int, String]] .

86voto

Austen Holmes Points 561

J'aimerais m'appuyer sur la réponse de Daniel. Elle était très complète, mais comme indiqué dans les commentaires, elle n'explique pas ce que fait Breakout.

Tiré de Re : Support pour les constructeurs explicites (2009-10-23), voici ce que je pense que Breakout fait :

Il donne implicitement au compilateur une suggestion quant au choix du constructeur (il permet essentiellement au compilateur de choisir la fabrique qui lui semble la mieux adaptée à la situation).

Par exemple, voir ce qui suit :

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)

Vous pouvez voir que le type de retour est implicitement choisi par le compilateur pour correspondre au mieux au type attendu. Selon la façon dont vous déclarez la variable réceptrice, vous obtenez des résultats différents.

Ce qui suit serait une manière équivalente de spécifier un constructeur. Notez que dans ce cas, le compilateur déduira le type attendu sur la base du type du constructeur :

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)

10voto

Dzhu Points 1563

La réponse de Daniel Sobral est excellente, et doit être lue conjointement avec Architecture des collections Scala (chapitre 25 de Programming in Scala).

Je voulais juste développer la raison pour laquelle il est appelé breakOut :

Pourquoi s'appelle-t-il breakOut ?

Parce que nous voulons sortir d'un type et entrer dans un autre :

Sortir de quel type pour entrer dans quel type ? Regardons les map fonction sur Seq à titre d'exemple :

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

Si nous voulions construire une carte directement à partir du mappage des éléments d'une séquence telle que :

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

Le compilateur se plaindrait :

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

La raison en est que Seq sait seulement comment construire un autre Seq (c'est-à-dire qu'il existe un lien implicite entre Seq et l'utilisateur). CanBuildFrom[Seq[_], B, Seq[B]] usine de construction disponible, mais il y a NON constructeur de Seq à Map).

Afin de compiler, nous devons en quelque sorte breakOut de l'exigence de type et être capable de construire un constructeur qui produise une carte pour l'ensemble de l'échantillon. map à utiliser.

Comme Daniel l'a expliqué, breakOut a la signature suivante :

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 est une sous-classe de toutes les classes, donc n'importe quelle fabrique de constructeur peut être substituée à la place de implicit b: CanBuildFrom[Nothing, T, To] . Si nous avons utilisé la fonction breakOut pour fournir le paramètre implicite :

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

Il compilerait, car breakOut est en mesure de fournir le type requis de CanBuildFrom[Seq[(String, Int)], (String, Int), Map[String, Int]] alors que le compilateur est capable de trouver une fabrique implicite de constructeurs de type CanBuildFrom[Map[_, _], (A, B), Map[A, B]] au lieu de CanBuildFrom[Nothing, T, To] pour que breakOut l'utilise pour créer le constructeur réel.

Notez que CanBuildFrom[Map[_, _], (A, B), Map[A, B]] est défini dans Map, et initie simplement une MapBuilder qui utilise une carte sous-jacente.

J'espère que ça va éclaircir les choses.

6voto

man Points 156

Un exemple simple pour comprendre ce que breakOut fait :

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]

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X