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'execution dû à l'instanciation de TuplesOps
, c'est le wrapping. Or, cela peut être éviter 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…