Scala 2.8 breakOut


En Scala 2.8, hay un objeto en 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()
 }

Se me ha dicho que esto resulta en:

> 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á pasando aquí? ¿Por qué se llama breakOut como argumento a mi List?

Author: DNA, 2009-11-11

4 answers

La respuesta se encuentra en la definición de map:

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

Tenga en cuenta que tiene dos parámetros. La primera es su función y la segunda es una implícita. Si no lo proporciona implícito, Scala elegirá el más específico disponible.

Acerca de breakOut

Entonces, ¿cuál es el propósito de breakOut? Considere el ejemplo dado para la pregunta, Toma una lista de cadenas, transforma cada cadena en una tupla (Int, String), y luego produce a Map fuera de él. La forma más obvia de hacerlo sería producir una colección intermediaria List[(Int, String)], y luego convertirla.

Dado que map utiliza un Builder para producir la colección resultante, ¿no sería posible omitir el intermediario List y recopilar los resultados directamente en un Map? Evidentemente, sí, lo es. Para hacerlo, sin embargo, necesitamos pasar un CanBuildFrom apropiado a map, y eso es exactamente lo que hace breakOut.

Veamos, entonces, la definición 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()
  }

Tenga en cuenta que breakOut está parametrizado, y que devuelve una instancia de CanBuildFrom. Como sucede, los tipos From, T y To ya se han inferido, porque sabemos que map está esperando CanBuildFrom[List[String], (Int, String), Map[Int, String]]. Por lo tanto:

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

Para concluir examinemos lo implícito recibido por breakOut en sí. Es de tipo CanBuildFrom[Nothing,T,To]. Ya conocemos todos estos tipos, por lo que podemos determinar que necesitamos un implícito de tipo CanBuildFrom[Nothing,(Int,String),Map[Int,String]]. Pero hay tal definición?

Veamos la definición de CanBuildFrom:

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

Así que CanBuildFrom es una contra-variante en su primer parámetro de tipo. Debido a que Nothing es una clase inferior (es decir, es una subclase de todo), eso significa cualquier clase puede usarse en lugar de Nothing.

Dado que tal constructor existe, Scala puede usarlo para producir la salida deseada.

Acerca de los constructores

Muchos métodos de la biblioteca de colecciones de Scala consisten en tomar la colección original, procesándola de alguna manera (en el caso de map, transformando cada elemento), y almacenando los resultados en una nueva colección.

Para maximizar la reutilización de código, este almacenamiento de resultados se realiza a través de un constructor (scala.collection.mutable.Builder), que básicamente soporta dos operaciones: anexar elementos y devolver la colección resultante. El tipo de esta colección resultante dependerá del tipo de generador. Por lo tanto, un constructor List devolverá un constructor List, un constructor Map devolverá un Map, y así sucesivamente. La implementación del método map no necesita preocuparse por el tipo del resultado: el constructor se encarga de él.

Por otro lado, eso significa que map necesita recibir este constructor de alguna manera. El problema al diseñar las colecciones de Scala 2.8 fue cómo elegir el mejor constructor posible. Por ejemplo, si tuviera que escribir Map('a' -> 1).map(_.swap), me gustaría obtener un Map(1 -> 'a') de vuelta. Por otro lado, un Map('a' -> 1).map(_._1) no puede devolver un Map (devuelve un Iterable).

La magia de producir lo mejor posible Builder a partir de los tipos conocidos de la expresión se realiza a través de este CanBuildFrom implícito.

Acerca de CanBuildFrom

Para explicar mejor lo que está pasando, voy a dar un ejemplo donde la colección que se mapea es un Map en lugar de un List. Volveré a List más tarde. Por ahora, considere estas dos expresiones:

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

La primera devuelve un Map y la segunda devuelve un Iterable. Magia de devolver una colección adecuada es el trabajo de CanBuildFrom. Consideremos de nuevo la definición de map para entenderla.

El método map se hereda de TraversableLike. Se parametriza en B y That, y hace uso de los parámetros de tipo A y Repr, que parametrizan la clase. Veamos ambas definiciones juntas:

La clase TraversableLike se define como:

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

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

Para entender de dónde vienen A y Repr, consideremos la definición de Map en sí:

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

Porque TraversableLike es heredado por todos los rasgos que se extienden Map, A y Repr podría ser heredado de cualquiera de ellos. El último tiene la preferencia, sin embargo. Entonces, siguiendo la definición de lo inmutable Map y todos los rasgos que lo conectan con TraversableLike, tenemos: {[116]]}

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 pasa los parámetros de tipo de Map[Int, String] hasta el final de la cadena, encontramos que los tipos pasados a TraversableLike, y, por lo tanto, utilizados por map, son:

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

Va volviendo al ejemplo, el primer mapa recibe una función de tipo ((Int, String)) => (Int, Int) y el segundo mapa recibe una función de tipo ((Int, String)) => String. Uso el doble paréntesis para enfatizar que es una tupla que se recibe, ya que ese es el tipo de A como vimos.

Con esa información, consideremos los otros tipos.

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

map (_._2):
B = String

Podemos ver que el tipo devuelto por el primero map es Map[Int,Int], y el segundo es Iterable[String]. Mirando la definición de map, es fácil ver que estos son los valores de That. Pero ¿de dónde vienen?

Si miramos dentro de los objetos compañeros de las clases involucradas, vemos algunas declaraciones implícitas que los proporcionan. Sobre el objeto Map:

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

Y en el objeto Iterable, cuya clase se extiende por Map:

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

Estas definiciones proporcionan fábricas para CanBuildFrom parametrizado.

Scala elegirá el implícito más específico disponible. En el primer caso, fue el primero CanBuildFrom. En el segundo caso, como el primero no coincidió, eligió el segundo CanBuildFrom.

Volver a la pregunta

Veamos el código de la pregunta, List's y map's definición (de nuevo) para ver cómo se deducen los tipos:

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 

El tipo de List("London", "Paris") es List[String], por lo que los tipos A y Repr definidos en TraversableLike son:

A = String
Repr = List[String]

El tipo de (x => (x.length, x)) es (String) => (Int, String), por lo que el tipo de B es:

B = (Int, String)

El último tipo desconocido, That es el tipo de la resultado de map, y ya tenemos eso también:

val map : Map[Int,String] =

Así que,

That = Map[Int, String]

Eso significa que breakOut debe, necesariamente, devolver un tipo o subtipo de CanBuildFrom[List[String], (Int, String), Map[Int, String]].

 317
Author: Daniel C. Sobral,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-27 15:36:25

Me gustaría construir sobre la respuesta de Daniel. Fue muy minucioso, pero como se señaló en los comentarios, no explica lo que hace breakout.

Tomado de Re: Soporte para Constructores explícitos (2009-10-23), esto es lo que creo que breakout hace:

Le da al compilador una sugerencia en cuanto a qué Constructor elegir implícitamente (esencialmente le permite al compilador elegir qué fábrica cree que se ajusta mejor a la situación.)

Por ejemplo, ver el siguiente:

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)

Puede ver que el tipo devuelto es elegido implícitamente por el compilador para que coincida mejor con el tipo esperado. Dependiendo de cómo declare la variable receptora, obtendrá resultados diferentes.

La siguiente sería una forma equivalente de especificar un constructor. Nota en este caso, el compilador inferirá el tipo esperado basado en el tipo del constructor:

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)
 85
Author: Austen Holmes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-05-06 17:34:03

La respuesta de Daniel Sobral es genial, y debe leerse junto con Architecture of Scala Collections (Capítulo 25 de Programming in Scala).

Solo quería explicar por qué se llama breakOut:

¿por Qué se llama breakOut?

Porque queremos salir de un tipo y entrar en otro:

¿Romper de qué tipo en qué tipo? Veamos la función map en Seq como un ejemplo:

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

Si quería construir un Mapa directamente a partir del mapeo sobre los elementos de una secuencia como:

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

El compilador se quejaría:

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

La razón es que Seq solo sabe cómo construir otro Seq (es decir, hay un builder factory implícito CanBuildFrom[Seq[_], B, Seq[B]] disponible, pero NO hay NINGÚN builder factory de Seq al Mapa).

Para compilar, necesitamos de alguna manera breakOut del tipo requisito, y ser capaz de construir un constructor que produce un Asignar la función map a usar.

Como Daniel ha explicado, breakOut tiene la siguiente 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 es una subclase de todas las clases, por lo que cualquier builder factory puede ser sustituido en lugar de implicit b: CanBuildFrom[Nothing, T, To]. Si usamos la función breakOut para proporcionar el parámetro implícito:

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

Compilaría, porque breakOut es capaz de proporcionar el tipo requerido de CanBuildFrom[Seq[(String, Int)], (String, Int), Map[String, Int]], mientras que el compilador es capaz de encontrar una fábrica de constructor implícita de tipo CanBuildFrom[Map[_, _], (A, B), Map[A, B]], en lugar de CanBuildFrom[Nothing, T, To], para breakOut a utilizar para crear el constructor real.

Tenga en cuenta que CanBuildFrom[Map[_, _], (A, B), Map[A, B]] se define en Mapa, y simplemente inicia un MapBuilder que utiliza un Mapa subyacente.

Espero que esto aclare las cosas.

 6
Author: Dzhu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-30 14:26:24

Un ejemplo sencillo para entender lo que hace 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]
 3
Author: man,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-29 22:00:30