Comment appliquer le modèle enrichir-ma-bibliothèque aux collections Scala?

L’un des modèles les plus puissants disponibles dans Scala est le modèle enrich-my-library *, qui utilise les conversions implicites pour append des méthodes aux classes existantes sans nécessiter de résolution de méthode dynamic. Par exemple, si nous souhaitions que toutes les chaînes aient les spaces méthode qui comptent le nombre de caractères d’espace, nous pourrions:

 class SpaceCounter(s: Ssortingng) { def spaces = s.count(_.isWhitespace) } implicit def ssortingng_counts_spaces(s: Ssortingng) = new SpaceCounter(s) scala> "How many spaces do I have?".spaces res1: Int = 5 

Malheureusement, ce modèle rencontre des problèmes lorsqu’il s’agit de collections génériques. Par exemple, un certain nombre de questions ont été posées à propos du regroupement d’éléments séquentiels avec des collections . Il n’y a rien de construit qui fonctionne en une fois, cela semble donc un candidat idéal pour le modèle enrichir-ma-bibliothèque en utilisant une collection générique C et un élément générique de type 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 } } } 

sauf que, bien sûr, ça ne marche pas . La REPL nous dit:

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

Il y a deux problèmes: comment pouvons-nous obtenir un C[C[A]] partir d’une liste C[A] vide (ou à partir d’une couche mince)? Et comment obtenir un C[C[A]] de la same +: ligne same +: au lieu d’une Seq[Seq[A]] ?

* Anciennement appelé pimp-my-library.

La clé pour comprendre ce problème est de réaliser qu’il existe deux manières différentes de créer et d’utiliser des collections dans la bibliothèque de collections. L’un est l’interface des collections publiques avec toutes ses bonnes méthodes. L’autre, qui est largement utilisé dans la création de la bibliothèque de collections, mais qui n’est presque jamais utilisé en dehors de lui, ce sont les générateurs.

Notre problème dans l’enrichissement est exactement le même que celui auquel la bibliothèque de collections doit faire face lorsqu’elle tente de renvoyer des collections du même type. C’est-à-dire que nous voulons construire des collections, mais lorsque nous travaillons de manière générique, nous n’avons aucun moyen de faire référence au “même type que la collection est déjà”. Nous avons donc besoin de constructeurs .

Maintenant, la question est: d’où viennent nos constructeurs? L’endroit évident provient de la collection elle-même. Cela ne fonctionne pas . Nous avons déjà décidé, en adoptant une collection générique, que nous allions oublier le type de la collection. Donc, même si la collection pouvait renvoyer un générateur qui générerait plus de collections du type souhaité, elle ne pourrait pas savoir quel était le type.

Au lieu de cela, nous obtenons nos générateurs à partir des implicites CanBuildFrom qui CanBuildFrom . Celles-ci existent spécifiquement dans le but de faire correspondre les types d’entrée et de sortie et de vous donner un générateur correctement saisi.

Donc, nous avons deux sauts conceptuels à faire:

  1. Nous n’utilisons pas les opérations de collecte standard, nous utilisons des générateurs.
  2. Nous obtenons ces générateurs à partir de CanBuildFrom implicites, pas directement de notre collection.

Regardons un exemple.

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

Supprimons cela. Tout d'abord, pour construire la collection de collections, nous soaps que nous devons construire deux types de collections: C[A] pour chaque groupe et C[C[A]] qui regroupe tous les groupes. Nous avons donc besoin de deux générateurs, un qui prend A s et construit C[A] s, et un qui prend C[A] s et construit C[C[A]] s. En regardant le type signature de CanBuildFrom , on voit

 CanBuildFrom[-From, -Elem, +To] 

ce qui signifie que CanBuildFrom veut connaître le type de collection avec lequel nous commençons - dans notre cas, il s'agit de C[A] , puis les éléments de la collection générée et le type de cette collection. Nous les remplissons donc comme parameters implicites cbfcc et cbfc .

Ayant réalisé cela, c'est la majeure partie du travail. Nous pouvons utiliser nos CanBuildFrom s pour nous donner des constructeurs (tout ce que vous avez à faire est de les appliquer). Et un constructeur peut construire une collection avec += , le convertir à la collection qu'il est censé avoir avec le result et se vider et être prêt à recommencer avec clear . Les générateurs démarrent vides, ce qui résout notre première erreur de compilation, et comme nous utilisons les générateurs au lieu de la récursivité, la seconde erreur disparaît également.

Un dernier petit détail - autre que l'algorithme qui fait réellement le travail - réside dans la conversion implicite. Notez que nous utilisons new GroupingCollection[A,C] pas [A,C[A]] . En effet, la déclaration de classe était pour C avec un paramètre, qu’elle remplit elle-même avec le A transmis. Donc, il suffit de lui remettre le type C , et de le laisser créer C[A] . Des détails mineurs, mais vous obtiendrez des erreurs de compilation si vous essayez une autre manière.

Ici, j'ai rendu la méthode un peu plus générique que la collection "equal elements" - la méthode consiste plutôt à séparer la collection originale lorsque son test des éléments séquentiels échoue.

Voyons notre méthode en action:

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

Ça marche!

Le seul problème est que ces méthodes ne sont généralement pas disponibles pour les tableaux, car cela nécessiterait deux conversions implicites à la suite. Il existe plusieurs façons de contourner ce WrappedArray , y compris l’écriture d’une conversion implicite distincte pour les tableaux, la conversion en WrappedArray , etc.


Edit: Mon approche privilégiée pour traiter les tableaux et les chaînes de caractères est de rendre le code encore plus générique et d'utiliser ensuite les conversions implicites appropriées pour les rendre plus spécifiques de manière à ce que les tableaux fonctionnent également. Dans ce cas particulier:

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

Ici, nous avons ajouté un implicite qui nous donne une Iterable[A] partir de C - pour la plupart des collections, il s'agira simplement de l'identité (par exemple, List[A] est déjà une Iterable[A] ), mais pour les tableaux ce sera un vraie conversion implicite. Et, par conséquent, nous avons abandonné l'exigence que C[A] <: Iterable[A] --nous avons simplement fait l'exigence de <% explicite, donc nous pouvons l'utiliser explicitement à volonté au lieu de remplir le compilateur ça pour nous. De plus, nous avons assoupli la ressortingction selon laquelle notre collection de collections est C[C[A]] --instead, c'est tout D[C] , que nous remplirons plus tard pour être ce que nous voulons. Parce que nous allons le remplir plus tard, nous l'avons placé au niveau de la classe au lieu du niveau de la méthode. Sinon, c'est fondamentalement la même chose.

Maintenant, la question est de savoir comment l'utiliser. Pour les collections régulières, nous pouvons:

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

où maintenant on twig C[A] pour C et C[C[A]] pour D[C] . Notez que nous avons besoin des types génériques explicites lors de l'appel à la new GroupingCollection pour pouvoir garder les types correspondant à quoi. Grâce à l' implicit c2i: C[A] => Iterable[A] , cela gère automatiquement les tableaux.

Mais attendez, et si on veut utiliser des chaînes? Maintenant, nous sums en difficulté, car vous ne pouvez pas avoir une "chaîne de chaînes". C'est là que l’abstraction supplémentaire aide: nous pouvons appeler D quelque chose qui convient pour contenir des chaînes. Choisissons le Vector et procédez comme suit:

 val vector_ssortingng_builder = ( new CanBuildFrom[Ssortingng, Ssortingng, Vector[Ssortingng]] { def apply() = Vector.newBuilder[Ssortingng] def apply(from: Ssortingng) = this.apply() } ) implicit def ssortingngs_have_grouping(s: Ssortingng)( implicit c2i: Ssortingng => Iterable[Char], cbfi: CanBuildFrom[Ssortingng,Char,Ssortingng] ) = { new GroupingCollection[Char,Ssortingng,Vector](s)( c2i, vector_ssortingng_builder, cbfi ) } 

Nous avons besoin d'un nouveau CanBuildFrom pour gérer la construction d'un vecteur de chaînes (mais c'est vraiment facile, car il suffit d'appeler Vector.newBuilder[Ssortingng] ), et ensuite nous devons remplir tous les types pour que GroupingCollection soit tapé judicieusement. Notez que nous avons déjà contourné un [Ssortingng,Char,Ssortingng] CanBuildFrom, afin que les chaînes puissent être créées à partir de collections de caractères.

Essayons:

 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[Ssortingng] = Vector(Hello, , there, !!) 

À partir de cet engagement, il est beaucoup plus facile d’enrichir les collections Scala qu’au moment où Rex a donné son excellente réponse. Pour des cas simples, cela pourrait ressembler à ceci,

 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) 

qui ajoute un “même type de résultat” en respectant le fonctionnement de filterMap pour tous les GenTraversableLike ,

 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: Ssortingng = Hello World scala> s.filterMap(c => if(c >= 'A' && c <= 'Z') Some(c) else None) res2: String = HW 

Et pour l'exemple de la question, la solution ressemble maintenant,

 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) 

Exemple de session 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: Ssortingng = 11223311 scala> s.groupIdentical res2: scala.collection.immutable.IndexedSeq[Ssortingng] = Vector(11, 22, 33, 11) 

Encore une fois, notez que le même principe de type de résultat a été observé exactement de la même manière que si groupIdentical avait été défini directement sur GenTraversableLike .

Au moment de commettre, l’incantation magique est légèrement modifiée par rapport à ce que Miles avait donné de son excellente réponse.

Les œuvres suivantes fonctionnent, mais est-ce canonique? J’espère que l’un des canons le corrigera. (Ou plutôt, des canons, l’un des gros canons.) Si la vue liée est une limite supérieure, vous perdez l’application à Array et Ssortingng. Cela n’a pas d’importance si la limite est GenTraversableLike ou TraversableLike; mais IsTraversableLike vous donne 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) 

Il y a plus d'une façon de peler un chat avec neuf vies. Cette version indique qu'une fois ma source convertie en GenTraversableLike, tant que je peux construire le résultat à partir de GenTraversable, faites-le. Je ne suis pas intéressé par mon ancien 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) 

Cette première tentative comprend une conversion grossière de Repr en 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)