Manipuler une collection de Tuples en Scala
La librairie Scala n’offre pas de méthodes utilitaires sur des collections de tuples. Nous allons voir comment implémenter un flatMap sur toutes les valeurs. Nous allons partir de l’implémentation la plus simple qui vient à l’esprit pour l’améliorer progressivement.
def flatMapValues[A, B, C](s:Seq[(A, B)])(f: B => Seq[C]) : Seq[(A, C)] =
s.flatMap { case (a, b) => f(b).map((a, _)) }
Le code client est alors flatMapValues(Seq("a"->1, "b"->2))(Seq.fill(_)("x"))
. Afin d’avoir un
code client fluent, on peut introduire une implicit
class, qui permet de fournir des
méthodes d’extension à un type existant, une séquence de tuples ici.
implicit class TuplesOps[A, B](s: Seq[(A, B)]) {
def flatMapValues[C](f: B => Seq[C]): Seq[(A, C)] =
s.flatMap { case (a, b) => f(b).map((a, _))}
}
Le code client devient Seq("a"->1, "b"->2).flatMapValues(Seq.fill(_)("x"))
. Malheureusement, un
surcoût se produit à l’exécution dû à l’instanciation de TuplesOps
, c’est le wrapping. Or, cela
peut être évité en utilisant une value
class :
implicit class TuplesOps[A, B](val s: Seq[(A, B)]) extends AnyVal {
def flatMapValues[C](f: B => Seq[C]): Seq[(A, C)] =
s.flatMap { case (a, b) => f(b).map((a, _))}
}
Le problème est que ce code n’est pas assez générique. On ne peut pas appeler la méthode avec un
Set
par exemple. Il va donc falloir utiliser une interface commune : Traversable
, qui définit
les fonctions map
, flatMap
.
implicit class TuplesOps[A, B](val s: Traversable[(A, B)]) extends AnyVal {
def flatMapValues[C](f: B => Traversable[C]): Traversable[(A, C)] =
s.flatMap { case (a, b) => f(b).map((a, _))}
}
Le problème est que peu importe le type de la collection, le retour sera toujours un Traversable
,
nous forçant à une transformation explicite. Cependant, nous pouvons encore faire mieux en
reproduisant le principe same-result-type mis en oeuvre dans les collections Scala grâce au trait
TraversableLike.
implicit class TuplesOps[A, B, Repr <: Traversable[(A, B)]](val s: TraversableLike[(A, B), Repr]) extends AnyVal {
def flatMapValues[C, That](f: B => TraversableOnce[C])(implicit bf: CanBuildFrom[Repr, (A, C), That]) =
s.flatMap { case (a, b) => f(b).map((a, _))}
}
L’écriture de code générique, élégant pour l’appelant, et performant requiert quelques efforts en Scala…