Yann Moisan

Scala, Web, Linux…

Tuples

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…