Scala 2.8 breakOut

225

No Scala 2.8 , há um objeto em 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()
 }

Foi-me dito que isso resulta em:

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

O que está acontecendo aqui? Por que está breakOutsendo chamado como argumento para o meu List?

oxbow_lakes
fonte
13
A resposta trivial é que não é um argumento para List, mas para map.
2213 Daniel C. Sobral

Respostas:

325

A resposta é encontrada na definição de map:

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

Observe que ele possui dois parâmetros. A primeira é a sua função e a segunda é implícita. Se você não fornecer esse implícito, o Scala escolherá o mais específico disponível.

Sobre breakOut

Então, qual é o propósito breakOut? Considere o exemplo dado para a pergunta: você pega uma lista de cadeias, transforma cada cadeia em uma tupla (Int, String)e, em seguida, produz um Mapresultado. A maneira mais óbvia de fazer isso produziria uma List[(Int, String)]coleção intermediária e depois a converteria.

Dado que mapusa a Builderpara produzir a coleção resultante, não seria possível pular o intermediário Liste coletar os resultados diretamente em uma Map? Evidentemente, é sim. Para isso, no entanto, é preciso passar por um adequado CanBuildFrompara map, e que é exatamente o que breakOutfaz.

Vejamos, então, a definição 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()
  }

Observe que breakOutestá parametrizado e retorna uma instância de CanBuildFrom. Por acaso, os tipos From, Te Tojá foram inferidos, porque sabemos que isso mapé esperado CanBuildFrom[List[String], (Int, String), Map[Int, String]]. Portanto:

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

Para concluir, vamos examinar o implícito recebido por breakOutsi só. É do tipo CanBuildFrom[Nothing,T,To]. Já conhecemos todos esses tipos, para podermos determinar que precisamos de um tipo implícito CanBuildFrom[Nothing,(Int,String),Map[Int,String]]. Mas existe essa definição?

Vamos dar uma olhada na CanBuildFromdefinição de:

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

Então, CanBuildFromé contra-variante em seu primeiro parâmetro de tipo. Como Nothingé uma classe inferior (ou seja, é uma subclasse de tudo), isso significa que qualquer classe pode ser usada no lugar de Nothing.

Como esse construtor existe, o Scala pode usá-lo para produzir a saída desejada.

Sobre os construtores

Muitos métodos da biblioteca de coleções do Scala consistem em pegar a coleção original, processá-la de alguma forma (no caso de maptransformar cada elemento) e armazenar os resultados em uma nova coleção.

Para maximizar a reutilização de código, esse armazenamento de resultados é feito por meio de um construtor ( scala.collection.mutable.Builder), que basicamente suporta duas operações: anexar elementos e retornar a coleção resultante. O tipo dessa coleção resultante dependerá do tipo do construtor. Assim, um Listconstrutor retornará a List, um Mapconstrutor retornará a Mape assim por diante. A implementação do mapmétodo não precisa se preocupar com o tipo de resultado: o construtor cuida dele.

Por outro lado, isso significa que mapprecisa receber esse construtor de alguma forma. O problema enfrentado ao projetar o Scala 2.8 Collections foi como escolher o melhor construtor possível. Por exemplo, se eu escrevesse Map('a' -> 1).map(_.swap), gostaria de me Map(1 -> 'a')vingar. Por outro lado, a Map('a' -> 1).map(_._1)não pode retornar a Map(retorna uma Iterable).

A mágica de produzir o melhor possível a Builderpartir dos tipos conhecidos da expressão é realizada por esse CanBuildFromimplícito.

Sobre CanBuildFrom

Para explicar melhor o que está acontecendo, darei um exemplo em que a coleção que está sendo mapeada é um em Mapvez de a List. Eu voltarei Listmais tarde. Por enquanto, considere estas duas expressões:

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

O primeiro retorna ae Mapo segundo retorna a Iterable. A mágica de devolver uma coleção apropriada é o trabalho de CanBuildFrom. Vamos considerar a definição de mapnovamente para entendê-la.

O método mapé herdado de TraversableLike. É parametrizado em Be That, e utiliza os parâmetros de tipo Ae Repr, que parametrizam a classe. Vamos ver as duas definições juntas:

A classe TraversableLikeé definida 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 onde Ae de onde Reprvem, vamos considerar a definição em Mapsi:

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

Porque TraversableLikeé herdada por todas as características que se estendem Map, Ae Reprpode ser herdada de qualquer um deles. O último recebe a preferência, no entanto. Então, seguindo a definição do imutável Mape de todas as características que o conectam TraversableLike, temos:

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 você passar os parâmetros de tipo de Map[Int, String]toda a cadeia, descobrimos que os tipos passados ​​para TraversableLikee, portanto, usados ​​por mapsão:

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

Voltando ao exemplo, o primeiro mapa está recebendo uma função do tipo ((Int, String)) => (Int, Int)e o segundo mapa está recebendo uma função do tipo ((Int, String)) => String. Eu uso o parêntese duplo para enfatizar que é uma tupla sendo recebida, pois esse é o tipo Aque vimos.

Com essa informação, vamos considerar os outros tipos.

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

map (_._2):
B = String

Podemos ver que o tipo retornado pelo primeiro mapé Map[Int,Int]e o segundo é Iterable[String]. Olhando para mapa definição de, é fácil ver que esses são os valores de That. Mas de onde eles vêm?

Se olharmos dentro dos objetos complementares das classes envolvidas, vemos algumas declarações implícitas fornecendo-os. No objeto Map:

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

E no objeto Iterable, cuja classe é estendida por Map:

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

Essas definições fornecem fábricas para parametrizados CanBuildFrom.

Scala escolherá o implícito mais específico disponível. No primeiro caso, foi o primeiro CanBuildFrom. No segundo caso, como o primeiro não correspondeu, ele escolheu o segundo CanBuildFrom.

Voltar à pergunta

Vamos ver o código da definição de pergunta Liste de map(novamente) para ver como os tipos são inferidos:

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 

O tipo de List("London", "Paris")é List[String], portanto, os tipos Ae os Reprdefinidos TraversableLikesão:

A = String
Repr = List[String]

O tipo de (x => (x.length, x))é (String) => (Int, String), portanto, o tipo de Bé:

B = (Int, String)

O último tipo desconhecido, Thaté o tipo do resultado de map, e já o temos também:

val map : Map[Int,String] =

Assim,

That = Map[Int, String]

Isso significa que breakOutdeve, necessariamente, retornar um tipo ou subtipo de CanBuildFrom[List[String], (Int, String), Map[Int, String]].

Daniel C. Sobral
fonte
61
Daniel, posso analisar os tipos de respostas, mas quando chego ao fim, sinto que não obtive nenhum entendimento de alto nível. O que é breakOut? De onde vem o nome "breakOut" (do que estou saindo)? Por que é necessário neste caso para obter um mapa? Certamente há alguma maneira de responder brevemente a essas perguntas? (mesmo se os-rastejante tipo longas continua a ser necessário, a fim de captar todos os detalhes)
Seth tisue
3
@ Set Isso é uma preocupação válida, mas não tenho certeza se estou pronto para a tarefa. A origem disso pode ser encontrada aqui: article.gmane.org/gmane.comp.lang.scala.internals/1812/… . Vou pensar nisso, mas, no momento, não consigo pensar em uma maneira de melhorá-lo.
Daniel C. Sobral
2
Existe uma maneira de evitar a especificação de todo o tipo de resultado Map [Int, String] e, em vez disso, poder escrever algo como: 'val map = List ("London", "Paris"). Map (x => (x. length, x)) (breakOut [... Map]) '
IttayD
9
@SethTisue Da minha leitura desta explicação, parece que é necessário "quebrar" o requisito que seu construtor precisa construir a partir de uma List [String]. O compilador deseja um CanBuildFrom [List [String], (Int, String), Map [Int, String]], que você não pode fornecer. A função breakOut faz isso usando o primeiro parâmetro de tipo no CanBuildFrom, definindo-o como Nothing. Agora você só precisa fornecer um CanBuildFrom [Nothing, (Int, String), Map [Int, String]]. Isso é fácil porque é fornecido pela classe Map.
Mark
2
@ Mark Quando eu encontrei breakOut, o problema que eu vi abordar foi a maneira que as mônadas insistem em mapear (via bind / flatMap) para seu próprio tipo. Permite "romper" uma cadeia de mapeamento usando uma mônada em um tipo diferente de mônada. Eu não tenho idéia se é assim que Adriaan Moors (o autor) estava pensando sobre isso!
precisa
86

Eu gostaria de aproveitar a resposta de Daniel. Foi muito completo, mas, como observado nos comentários, não explica o que a fuga faz.

Retirado de Re: Support for Builders explícito (23-10-2009), eis o que eu acredito que a fuga faz:

Ele fornece ao compilador uma sugestão sobre qual Construtor escolher implicitamente (essencialmente, permite ao compilador escolher qual fábrica ele se encaixa melhor na situação).

Por exemplo, veja o seguinte:

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)

Você pode ver que o tipo de retorno é escolhido implicitamente pelo compilador para corresponder melhor ao tipo esperado. Dependendo de como você declara a variável de recebimento, você obtém resultados diferentes.

A seguir, seria uma maneira equivalente de especificar um construtor. Observe que, neste caso, o compilador inferirá o tipo esperado com base no tipo do construtor:

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)
Austen Holmes
fonte
1
Eu me pergunto por que é chamado " breakOut"? Estou pensando que algo como convertou buildADifferentTypeOfCollection(mas mais curto) pode ter sido mais fácil de lembrar.
KajMagnus 01/03
8

A resposta de Daniel Sobral é ótima e deve ser lida em conjunto com a Architecture of Scala Collections (capítulo 25 de Programação em Scala).

Eu só queria explicar por que é chamado breakOut:

Por que é chamado breakOut?

Porque queremos sair de um tipo para outro :

Quebrar de que tipo em que tipo? Vamos ver a mapfunção Seqcomo um exemplo:

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

Se quisermos criar um mapa diretamente do mapeamento sobre os elementos de uma sequência, como:

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

O compilador reclamaria:

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

O motivo é que o Seq sabe apenas como construir outro Seq (ou seja, há uma CanBuildFrom[Seq[_], B, Seq[B]]fábrica implícita de construtores disponível, mas NÃO existe uma fábrica de construtores da Seq para o Mapa).

Para compilar, precisamos de alguma forma breakOutdo requisito de tipo e poder construir um construtor que produz um Mapa para a mapfunção usar.

Como Daniel explicou, breakOut tem a seguinte assinatura:

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é uma subclasse de todas as classes, portanto, qualquer fábrica de construtores pode ser substituída no lugar de implicit b: CanBuildFrom[Nothing, T, To]. Se usamos a função breakOut para fornecer o parâmetro implícito:

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

Ele seria compilado, porque breakOuté capaz de fornecer o tipo necessário de CanBuildFrom[Seq[(String, Int)], (String, Int), Map[String, Int]], enquanto o compilador é capaz de encontrar uma fábrica implícita de construtores do tipo CanBuildFrom[Map[_, _], (A, B), Map[A, B]], no lugar de CanBuildFrom[Nothing, T, To], para breakOut usar para criar o construtor real.

Observe que CanBuildFrom[Map[_, _], (A, B), Map[A, B]]é definido no mapa e simplesmente inicia um MapBuilderque usa um mapa subjacente.

Espero que isso esclareça as coisas.

Dzhu
fonte
4

Um exemplo simples para entender o que breakOutfaz:

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]
fdietze
fonte
Obrigado pelo exemplo! Também val seq:Seq[Int] = set.map(_ % 2).toVectornão fornecerá os valores repetidos, pois o Setfoi preservado para o map.
Matthew Pickering
@MatthewPickering correct! set.map(_ % 2)cria um Set(1, 0)primeiro, que é convertido em a Vector(1, 0).
Fdietze