web-dev-qa-db-fra.com

Scala 2.8 breakOut

Dans 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 se traduit par:

> 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 breakOut est-il appelé en tant qu'argument à mon List?

223
oxbow_lakes

La réponse se trouve sur 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 implicite. Si vous ne fournissez pas cette valeur implicite, Scala choisira la plus spécifique disponible.

À propos de breakOut

Alors, quel est le but de breakOut? Prenons l'exemple donné pour la question: Vous prenez une liste de chaînes, transformez chaque chaîne en un Tuple (Int, String), Puis produisez un Map. La méthode la plus évidente consiste à générer une collection intermédiaire List[(Int, String)], puis à 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 de collecter les résultats directement dans un fichier Map? Evidemment, oui. Pour ce faire, cependant, nous devons passer un CanBuildFrom correct à map, et c’est exactement ce que fait breakOut.

Regardons donc la définition de 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é et qu'il renvoie une instance de CanBuildFrom. En l'occurrence, les types From, T et To ont déjà été déduits, car nous savons que map s'attend à ce que 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 qu’il nous faut un implicite de type CanBuildFrom[Nothing,(Int,String),Map[Int,String]]. Mais existe-t-il une telle définition?

Regardons la définition de CanBuildFrom:

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

Donc, CanBuildFrom est une variable contraire à son premier paramètre de type. Puisque Nothing est une classe inférieure (c’est-à-dire qu’elle est une sous-classe de tout), cela signifie n’importe quelle classe peut être utilisée à la place de Nothing.

Puisqu'il existe un tel générateur, 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 à récupérer la collection d'origine, à la traiter (dans le cas de map, transformer chaque élément) et à stocker les résultats dans une nouvelle collection.

Pour optimiser la réutilisation du code, cet enregistrement des résultats est effectué via un générateur (scala.collection.mutable.Builder), Qui prend en charge deux opérations: ajout d'éléments, et renvoyer la collection résultante. Le type de cette collection résultante dépendra du type du générateur. Ainsi, un générateur List renverra un List, un générateur Map renverra un Map, etc. L'implémentation de la méthode map n'a pas besoin de 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 générateur d'une manière ou d'une autre. Le problème rencontré lors de la conception Scala 2.8 Collections était comment choisir le meilleur constructeur possible. Par exemple, si je devais écrire Map('a' -> 1).map(_.swap), je voudrais obtenir un Map(1 -> 'a') back. D'autre part, un Map('a' -> 1).map(_._1) ne peut pas retourner un Map (il retourne un Iterable).

La magie de produire le meilleur possible Builder à partir des types connus de l’expression s’exécute à travers cette implicite CanBuildFrom.

À propos de CanBuildFrom

Pour mieux expliquer ce qui se passe, je vais donner un exemple où la collection en cours de mappage est un Map au lieu d'un List. Je reviens à List plus tard. Pour l'instant, considérons ces deux expressions:

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

Le premier retourne un Map et le second renvoie un Iterable. La magie de rendre une collection de raccord est l'œuvre de CanBuildFrom. Considérons à nouveau la définition de map pour la comprendre.

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

La classe TraversableLike est définie comme:

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 d'où A et Repr, 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é par tous les traits qui s'étendent Map, A et Repr pourraient en être hérités. Le dernier obtient la préférence, cependant. Donc, en suivant la définition de l'immuable Map et de 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 transmettez les paramètres de type de Map[Int, String] Tout au long de la chaîne, nous constatons que les types passés à TraversableLike, et donc utilisés par map, sont les suivants:

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 deuxième carte reçoit une fonction de type ((Int, String)) => String. J'utilise la double parenthèse pour souligner qu'il s'agit d'un tuple en cours de réception, car c'est le type de A tel que nous l'avons vu.

Avec cette information, considérons 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 la définition de 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 impliqué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 étendue par Map:

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

Ces définitions fournissent des fabriques pour CanBuildFrom paramétré.

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

Retour à la question

Voyons le code de la question, les définitions de List et map (à nouveau) 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], Ainsi les types A et Repr définis sur TraversableLike sont:

A = String
Repr = List[String]

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

B = (Int, String)

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

val map : Map[Int,String] =

Alors,

That = Map[Int, String]

Cela signifie que breakOut doit obligatoirement renvoyer un type ou un sous-type de CanBuildFrom[List[String], (Int, String), Map[Int, String]].

323
Daniel C. Sobral

J'aimerais développer la réponse de Daniel. C'était très complet, mais comme indiqué dans les commentaires, cela n'explique pas ce que l'évasion fait.

Tiré de Objet: prise en charge des constructeurs explicites (2009-10-23), voici ce que je crois que la discussion en groupe fait:

Il donne au compilateur une suggestion quant au choix implicite de Builder (essentiellement, il permet au compilateur de choisir la fabrique qu’il pense 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 choisi implicitement par le compilateur pour correspondre au mieux au type attendu. Selon la manière dont vous déclarez la variable de réception, vous obtenez des résultats différents.

Ce qui suit serait un moyen équivalent de spécifier un générateur. Notez que dans ce cas, le compilateur déduira le type attendu en fonction du type du générateur:

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)
86
Austen Holmes

La réponse de Daniel Sobral est excellente et doit être lue conjointement avec Architecture of Scala Collections (Chapitre 25 de la Programmation en Scala).

Je voulais juste expliquer pourquoi il s'appelle breakOut:

Pourquoi s'appelle-t-il breakOut?

Parce que nous voulons passer d'un type à un autre:

Sortir de quel type dans quel type? Regardons la fonction map 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 de la cartographie sur les é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 étant que Seq sait seulement comment construire une autre Seq (c'est-à-dire qu'il existe une fabrique de constructeur implicite CanBuildFrom[Seq[_], B, Seq[B]] Disponible, mais il y a [~ # ~] non [~ # ~ ] constructeur de Seq à Map).

Pour compiler, nous devons en quelque sorte breakOut de l'exigence de type, et pouvoir construire un générateur qui produit une carte pour la fonction map à utiliser.

Comme Daniel l'a expliqué, breakOut porte 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. Toute fabrique de constructeurs 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 serait compilé, car breakOut est capable de fournir le type requis de CanBuildFrom[Seq[(String, Int)], (String, Int), Map[String, Int]], tandis que le compilateur est capable de trouver une fabrique de constructeur implicite de type CanBuildFrom[Map[_, _], (A, B), Map[A, B]], à la place. de CanBuildFrom[Nothing, T, To], à utiliser par breakOut pour créer le constructeur actuel.

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

Espérons que cela arrange les choses.

7
Dzhu

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]
4
man