Come applico il pattern arricchisci la mia libreria alle collezioni Scala?


92

Uno dei modelli più potenti disponibili in Scala è il modello arricchire la mia libreria *, che utilizza conversioni implicite per sembrare aggiungere metodi alle classi esistenti senza richiedere la risoluzione del metodo dinamico. Ad esempio, se desiderassimo che tutte le stringhe avessero il metodo spacesche conta quanti caratteri di spazio hanno, potremmo:

class SpaceCounter(s: String) {
  def spaces = s.count(_.isWhitespace)
}
implicit def string_counts_spaces(s: String) = new SpaceCounter(s)

scala> "How many spaces do I have?".spaces
res1: Int = 5

Sfortunatamente, questo modello incontra dei problemi quando si tratta di raccolte generiche. Ad esempio, sono state poste numerose domande sul raggruppamento degli elementi in sequenza con le raccolte . Non c'è nulla di integrato che funzioni in un colpo solo, quindi questo sembra un candidato ideale per il modello di arricchimento della mia libreria utilizzando una raccolta generica Ce un tipo di elemento generico A:

class SequentiallyGroupingCollection[A, C[A] <: Seq[A]](ca: C[A]) {
  def groupIdentical: C[C[A]] = {
    if (ca.isEmpty) C.empty[C[A]]
    else {
      val first = ca.head
      val (same,rest) = ca.span(_ == first)
      same +: (new SequentiallyGroupingCollection(rest)).groupIdentical
    }
  }
}

tranne, ovviamente, non funziona . Il REPL ci dice:

<console>:12: error: not found: value C
               if (ca.isEmpty) C.empty[C[A]]
                               ^
<console>:16: error: type mismatch;
 found   : Seq[Seq[A]]
 required: C[C[A]]
                 same +: (new SequentiallyGroupingCollection(rest)).groupIdentical
                      ^

Ci sono due problemi: come si ottiene un C[C[A]]da una C[A]lista vuota (o dal nulla)? E come otteniamo un C[C[A]]ritorno dalla same +:linea invece di un Seq[Seq[A]]?

* Precedentemente noto come pimp-my-library.


1
Ottima domanda! E, ancora meglio, arriva con una risposta! :-)
Daniel C. Sobral

2
@Daniel - Non ho obiezioni sul fatto che arrivi con due o più risposte!
Rex Kerr

2
Lascia perdere, amico. Lo sto aggiungendo ai segnalibri per cercarlo ogni volta che devo fare qualcosa del genere. :-)
Daniel C. Sobral

Risposte:


74

La chiave per comprendere questo problema è rendersi conto che esistono due modi diversi per creare e lavorare con le raccolte nella libreria delle raccolte. Uno è l'interfaccia delle collezioni pubbliche con tutti i suoi metodi simpatici. L'altro, che è ampiamente utilizzato nella creazione della libreria delle collezioni, ma che non viene quasi mai utilizzato al di fuori di essa, sono i costruttori.

Il nostro problema nell'arricchimento è esattamente lo stesso che la libreria stessa deve affrontare quando si cerca di restituire raccolte dello stesso tipo. Cioè, vogliamo creare raccolte, ma quando si lavora in modo generico, non abbiamo un modo per fare riferimento allo "stesso tipo che la raccolta è già". Quindi abbiamo bisogno di costruttori .

Ora la domanda è: da dove prendiamo i nostri costruttori? Il posto ovvio è dalla collezione stessa. Questo non funziona . Abbiamo già deciso, passando a una raccolta generica, che avremmo dimenticato il tipo di raccolta. Quindi, anche se la raccolta potrebbe restituire un builder che genererebbe più raccolte del tipo desiderato, non saprebbe quale fosse il tipo.

Invece, otteniamo i nostri costruttori da CanBuildFromimpliciti che fluttuano intorno. Questi esistono specificamente allo scopo di abbinare i tipi di input e di output e di fornire un generatore digitato in modo appropriato.

Quindi, abbiamo due salti concettuali da fare:

  1. Non stiamo usando operazioni di raccolta standard, stiamo usando builder.
  2. Otteniamo questi builder da implicit CanBuildFrom, non direttamente dalla nostra raccolta.

Diamo un'occhiata a un esempio.

class GroupingCollection[A, C[A] <: Iterable[A]](ca: C[A]) {
  import collection.generic.CanBuildFrom
  def groupedWhile(p: (A,A) => Boolean)(
    implicit cbfcc: CanBuildFrom[C[A],C[A],C[C[A]]], cbfc: CanBuildFrom[C[A],A,C[A]]
  ): C[C[A]] = {
    val it = ca.iterator
    val cca = cbfcc()
    if (!it.hasNext) cca.result
    else {
      val as = cbfc()
      var olda = it.next
      as += olda
      while (it.hasNext) {
        val a = it.next
        if (p(olda,a)) as += a
        else { cca += as.result; as.clear; as += a }
        olda = a
      }
      cca += as.result
    }
    cca.result
  }
}
implicit def iterable_has_grouping[A, C[A] <: Iterable[A]](ca: C[A]) = {
  new GroupingCollection[A,C](ca)
}

Smontiamo questo. Innanzitutto, per creare la raccolta di raccolte, sappiamo che avremo bisogno di creare due tipi di raccolte: C[A]per ogni gruppo, e C[C[A]]questo riunisce tutti i gruppi insieme. Quindi, abbiamo bisogno di due builder, uno che prende se Acostruisce se C[A]uno che prende se C[A]costruisce C[C[A]]s. Guardando il tipo di firma di CanBuildFrom, vediamo

CanBuildFrom[-From, -Elem, +To]

il che significa che CanBuildFrom vuole conoscere il tipo di raccolta con cui stiamo iniziando - nel nostro caso, è C[A], e quindi gli elementi della raccolta generata e il tipo di quella raccolta. Quindi li inseriamo come parametri impliciti cbfcce cbfc.

Avendo capito questo, è la maggior parte del lavoro. Possiamo usare i nostri CanBuildFromper darci costruttori (tutto ciò che devi fare è applicarli). E un costruttore può creare una raccolta con +=, convertirla nella raccolta con cui dovrebbe essere alla fine result, svuotarsi ed essere pronto per ricominciare clear. I builder iniziano vuoti, il che risolve il nostro primo errore di compilazione, e poiché stiamo usando i builder invece della ricorsione, anche il secondo errore scompare.

Un ultimo piccolo dettaglio - oltre all'algoritmo che effettivamente fa il lavoro - è nella conversione implicita. Nota che usiamo new GroupingCollection[A,C]not [A,C[A]]. Questo perché la dichiarazione della classe era perC con un parametro, che lo riempie da solo con il Apassato. Quindi gli diamo solo il tipo Ce lasciamo che si crei C[A]da esso. Dettagli minori, ma se provi in ​​un altro modo otterrai errori in fase di compilazione.

Qui, ho reso il metodo un po 'più generico rispetto alla raccolta "elementi uguali" - piuttosto, il metodo taglia a parte la raccolta originale ogni volta che il suo test di elementi sequenziali fallisce.

Vediamo il nostro metodo in azione:

scala> List(1,2,2,2,3,4,4,4,5,5,1,1,1,2).groupedWhile(_ == _)
res0: List[List[Int]] = List(List(1), List(2, 2, 2), List(3), List(4, 4, 4), 
                             List(5, 5), List(1, 1, 1), List(2))

scala> Vector(1,2,3,4,1,2,3,1,2,1).groupedWhile(_ < _)
res1: scala.collection.immutable.Vector[scala.collection.immutable.Vector[Int]] =
  Vector(Vector(1, 2, 3, 4), Vector(1, 2, 3), Vector(1, 2), Vector(1))

Funziona!

L'unico problema è che in generale non abbiamo questi metodi disponibili per gli array, poiché ciò richiederebbe due conversioni implicite di seguito. Esistono diversi modi per aggirare questo problema, inclusa la scrittura di una conversione implicita separata per gli array, il casting WrappedArraye così via.


Modifica: il mio approccio preferito per trattare array e stringhe è quello di rendere il codice ancora più generico e quindi utilizzare conversioni implicite appropriate per renderle di nuovo più specifiche in modo tale che anche gli array funzionino. In questo caso particolare:

class GroupingCollection[A, C, D[C]](ca: C)(
  implicit c2i: C => Iterable[A],
           cbf: CanBuildFrom[C,C,D[C]],
           cbfi: CanBuildFrom[C,A,C]
) {
  def groupedWhile(p: (A,A) => Boolean): D[C] = {
    val it = c2i(ca).iterator
    val cca = cbf()
    if (!it.hasNext) cca.result
    else {
      val as = cbfi()
      var olda = it.next
      as += olda
      while (it.hasNext) {
        val a = it.next
        if (p(olda,a)) as += a
        else { cca += as.result; as.clear; as += a }
        olda = a
      }
      cca += as.result
    }
    cca.result
  }
}

Qui abbiamo aggiunto un implicito che ci dà un Iterable[A]da - Cper la maggior parte delle raccolte questa sarà solo l'identità (ad esempio, List[A]è già un Iterable[A]), ma per gli array sarà una vera conversione implicita. E, di conseguenza, abbiamo abbandonato il requisito che C[A] <: Iterable[A]- fondamentalmente abbiamo appena reso <%esplicito il requisito , quindi possiamo usarlo esplicitamente a nostro piacimento invece di farlo compilare dal compilatore per noi. Inoltre, abbiamo allentato la restrizione secondo cui la nostra raccolta di raccolte è C[C[A]], invece, è qualsiasi D[C], che riempiremo in seguito per essere ciò che vogliamo. Poiché lo riempiremo più tardi, lo abbiamo spostato al livello di classe invece che a livello di metodo. Altrimenti, è fondamentalmente lo stesso.

Ora la domanda è come usarlo. Per le collezioni regolari, possiamo:

implicit def collections_have_grouping[A, C[A]](ca: C[A])(
  implicit c2i: C[A] => Iterable[A],
           cbf: CanBuildFrom[C[A],C[A],C[C[A]]],
           cbfi: CanBuildFrom[C[A],A,C[A]]
) = {
  new GroupingCollection[A,C[A],C](ca)(c2i, cbf, cbfi)
}

dove ora ci colleghiamo C[A]per Ce C[C[A]]per D[C]. Nota che abbiamo bisogno dei tipi generici espliciti nella chiamata a in new GroupingCollectionmodo che possa mantenere chiaro quali tipi corrispondono a cosa. Grazie a implicit c2i: C[A] => Iterable[A], questo gestisce automaticamente gli array.

Ma aspetta, e se volessimo usare le stringhe? Adesso siamo nei guai, perché non puoi avere una "stringa di stringhe". È qui che l'astrazione extra aiuta: possiamo chiamare Dqualcosa che è adatto a contenere stringhe. Scegliamo Vectore facciamo quanto segue:

val vector_string_builder = (
  new CanBuildFrom[String, String, Vector[String]] {
    def apply() = Vector.newBuilder[String]
    def apply(from: String) = this.apply()
  }
)

implicit def strings_have_grouping(s: String)(
  implicit c2i: String => Iterable[Char],
           cbfi: CanBuildFrom[String,Char,String]
) = {
  new GroupingCollection[Char,String,Vector](s)(
    c2i, vector_string_builder, cbfi
  )
}

Abbiamo bisogno di un nuovo CanBuildFromper gestire la costruzione di un vettore di stringhe (ma questo è davvero facile, dato che dobbiamo solo chiamare Vector.newBuilder[String]), quindi dobbiamo compilare tutti i tipi in modo che GroupingCollectionsia digitato in modo ragionevole. Nota che abbiamo già fluttuato intorno a un file[String,Char,String] fluttuato CanBuildFrom, quindi le stringhe possono essere create da raccolte di caratteri.

Proviamolo:

scala> List(true,false,true,true,true).groupedWhile(_ == _)
res1: List[List[Boolean]] = List(List(true), List(false), List(true, true, true))

scala> Array(1,2,5,3,5,6,7,4,1).groupedWhile(_ <= _) 
res2: Array[Array[Int]] = Array(Array(1, 2, 5), Array(3, 5, 6, 7), Array(4), Array(1))

scala> "Hello there!!".groupedWhile(_.isLetter == _.isLetter)
res3: Vector[String] = Vector(Hello,  , there, !!)

Puoi usare <% per aggiungere il supporto per gli array.
Anonimo

@ Anonimo - Si sospetterebbe di sì. Ma l'hai provato in questo caso?
Rex Kerr

@Rex: "richiedi due conversioni implicite di seguito" mi ricorda stackoverflow.com/questions/5332801/… Applicabile qui?
Peter Schmitz

@Peter - Molto probabilmente! Tuttavia, tendo a scrivere conversioni implicite esplicite piuttosto che fare affidamento su <% chaining.
Rex Kerr

Sulla base del commento di @Peters ho provato ad aggiungere un'altra conversione implicita per gli array, ma non ci sono riuscito. Non ho davvero capito dove aggiungere i limiti della vista. @ Rex, puoi modificare la tua risposta e mostrare come far funzionare il codice con gli array?
kiritsuku

29

Con questo impegno è molto più facile "arricchire" le collezioni Scala rispetto a quando Rex ha dato la sua eccellente risposta. Per casi semplici potrebbe assomigliare a questo,

import scala.collection.generic.{ CanBuildFrom, FromRepr, HasElem }
import language.implicitConversions

class FilterMapImpl[A, Repr](val r : Repr)(implicit hasElem : HasElem[Repr, A]) {
  def filterMap[B, That](f : A => Option[B])
    (implicit cbf : CanBuildFrom[Repr, B, That]) : That = r.flatMap(f(_).toSeq)
}

implicit def filterMap[Repr : FromRepr](r : Repr) = new FilterMapImpl(r)

che aggiunge uno "stesso tipo di risultato" rispettando l' filterMapoperazione a tutti GenTraversableLikei messaggi,

scala> val l = List(1, 2, 3, 4, 5)
l: List[Int] = List(1, 2, 3, 4, 5)

scala> l.filterMap(i => if(i % 2 == 0) Some(i) else None)
res0: List[Int] = List(2, 4)

scala> val a = Array(1, 2, 3, 4, 5)
a: Array[Int] = Array(1, 2, 3, 4, 5)

scala> a.filterMap(i => if(i % 2 == 0) Some(i) else None)
res1: Array[Int] = Array(2, 4)

scala> val s = "Hello World"
s: String = Hello World

scala> s.filterMap(c => if(c >= 'A' && c <= 'Z') Some(c) else None)
res2: String = HW

E per l'esempio della domanda, la soluzione ora sembra,

class GroupIdenticalImpl[A, Repr : FromRepr](val r: Repr)
  (implicit hasElem : HasElem[Repr, A]) {
  def groupIdentical[That](implicit cbf: CanBuildFrom[Repr,Repr,That]): That = {
    val builder = cbf(r)
    def group(r: Repr) : Unit = {
      val first = r.head
      val (same, rest) = r.span(_ == first)
      builder += same
      if(!rest.isEmpty)
        group(rest)
    }
    if(!r.isEmpty) group(r)
    builder.result
  }
}

implicit def groupIdentical[Repr : FromRepr](r: Repr) = new GroupIdenticalImpl(r)

Esempio di sessione REPL,

scala> val l = List(1, 1, 2, 2, 3, 3, 1, 1)
l: List[Int] = List(1, 1, 2, 2, 3, 3, 1, 1)

scala> l.groupIdentical
res0: List[List[Int]] = List(List(1, 1),List(2, 2),List(3, 3),List(1, 1))

scala> val a = Array(1, 1, 2, 2, 3, 3, 1, 1)
a: Array[Int] = Array(1, 1, 2, 2, 3, 3, 1, 1)

scala> a.groupIdentical
res1: Array[Array[Int]] = Array(Array(1, 1),Array(2, 2),Array(3, 3),Array(1, 1))

scala> val s = "11223311"
s: String = 11223311

scala> s.groupIdentical
res2: scala.collection.immutable.IndexedSeq[String] = Vector(11, 22, 33, 11)

Di nuovo, si noti che lo stesso principio del tipo di risultato è stato osservato esattamente nello stesso modo in cui sarebbe groupIdenticalstato definito direttamente GenTraversableLike.


3
Sìì! Ci sono ancora più pezzi magici per tenere traccia di questo modo, ma si combinano tutti bene! È un sollievo non doversi preoccupare di ogni raccolta non gerarchica.
Rex Kerr

3
Peccato che Iterator sia escluso gratuitamente poiché il mio cambio di una riga è stato rifiutato. "errore: impossibile trovare il valore implicito per il parametro di prova di tipo scala.collection.generic.FromRepr [Iterator [Int]]"
psp

Quale modifica di una riga è stata rifiutata?
Miles Sabin


2
Non lo vedo in master; è evaporato, o è finito in un ramo post-2.10.0, o ...?
Rex Kerr

9

A partire da questo impegno, l'incantesimo magico è leggermente cambiato da quello che era quando Miles ha dato la sua eccellente risposta.

Le opere seguenti, ma sono canoniche? Spero che uno dei canoni lo corregga. (O meglio, cannoni, uno dei grandi cannoni.) Se il limite di visualizzazione è un limite superiore, si perde l'applicazione a Array e String. Non sembra avere importanza se il limite è GenTraversableLike o TraversableLike; ma IsTraversableLike ti dà un GenTraversableLike.

import language.implicitConversions
import scala.collection.{ GenTraversable=>GT, GenTraversableLike=>GTL, TraversableLike=>TL }
import scala.collection.generic.{ CanBuildFrom=>CBF, IsTraversableLike=>ITL }

class GroupIdenticalImpl[A, R <% GTL[_,R]](val r: GTL[A,R]) {
  def groupIdentical[That](implicit cbf: CBF[R, R, That]): That = {
    val builder = cbf(r.repr)
    def group(r: GTL[_,R]) {
      val first = r.head
      val (same, rest) = r.span(_ == first)
      builder += same
      if (!rest.isEmpty) group(rest)
    }
    if (!r.isEmpty) group(r)
    builder.result
  }
}

implicit def groupIdentical[A, R <% GTL[_,R]](r: R)(implicit fr: ITL[R]):
  GroupIdenticalImpl[fr.A, R] =
  new GroupIdenticalImpl(fr conversion r)

C'è più di un modo per scuoiare un gatto con nove vite. Questa versione dice che una volta che la mia sorgente è stata convertita in GenTraversableLike, fintanto che posso creare il risultato da GenTraversable, fallo e basta. Non mi interessa il mio vecchio Repr.

class GroupIdenticalImpl[A, R](val r: GTL[A,R]) {
  def groupIdentical[That](implicit cbf: CBF[GT[A], GT[A], That]): That = {
    val builder = cbf(r.toTraversable)
    def group(r: GT[A]) {
      val first = r.head
      val (same, rest) = r.span(_ == first)
      builder += same
      if (!rest.isEmpty) group(rest)
    }
    if (!r.isEmpty) group(r.toTraversable)
    builder.result
  }
}

implicit def groupIdentical[A, R](r: R)(implicit fr: ITL[R]):
  GroupIdenticalImpl[fr.A, R] =
  new GroupIdenticalImpl(fr conversion r)

Questo primo tentativo include una brutta conversione di Repr in GenTraversableLike.

import language.implicitConversions
import scala.collection.{ GenTraversableLike }
import scala.collection.generic.{ CanBuildFrom, IsTraversableLike }

type GT[A, B] = GenTraversableLike[A, B]
type CBF[A, B, C] = CanBuildFrom[A, B, C]
type ITL[A] = IsTraversableLike[A]

class FilterMapImpl[A, Repr](val r: GenTraversableLike[A, Repr]) { 
  def filterMap[B, That](f: A => Option[B])(implicit cbf : CanBuildFrom[Repr, B, That]): That = 
    r.flatMap(f(_).toSeq)
} 

implicit def filterMap[A, Repr](r: Repr)(implicit fr: ITL[Repr]): FilterMapImpl[fr.A, Repr] = 
  new FilterMapImpl(fr conversion r)

class GroupIdenticalImpl[A, R](val r: GT[A,R])(implicit fr: ITL[R]) { 
  def groupIdentical[That](implicit cbf: CBF[R, R, That]): That = { 
    val builder = cbf(r.repr)
    def group(r0: R) { 
      val r = fr conversion r0
      val first = r.head
      val (same, other) = r.span(_ == first)
      builder += same
      val rest = fr conversion other
      if (!rest.isEmpty) group(rest.repr)
    } 
    if (!r.isEmpty) group(r.repr)
    builder.result
  } 
} 

implicit def groupIdentical[A, R](r: R)(implicit fr: ITL[R]):
  GroupIdenticalImpl[fr.A, R] = 
  new GroupIdenticalImpl(fr conversion r)
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.