Tutoriel pour apprendre le langage Scala par l'exemple


précédentsommairesuivant

15. Paramètres et conversions implicites

Les paramètres et les conversions implicites sont des outils puissants qui permettent d'adapter les bibliothèques existantes et de créer des abstractions de haut niveau. À titre d'exemple, commençons par la classe abstraite des demi-groupes qui disposent d'un opérateur add :

 
Sélectionnez
1.
2.
3.
abstract class SemiGroup[A] {
    def add(x: A, y: A): A
}

La sous-classe Monoid ajoute un élément neutre ou unité :

 
Sélectionnez
1.
2.
3.
abstract class Monoid[A] extends SemiGroup[A] {
    def unit: A
}

Voici deux implémentations des monoïdes :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
object stringMonoid extends Monoid[String] {
    def add(x: String, y: String): String = x.concat(y)
    def unit: String = ""
}

object intMonoid extends Monoid[Int] {
    def add(x: Int, y: Int): Int = x + y
    def unit: Int = 0
}

Une méthode sum qui fonctionnerait avec des monoïdes quelconques pourrait s'écrire de la façon suivante en Scala :

 
Sélectionnez
1.
2.
3.
def sum[A](xs: List[A])(m: Monoid[A]): A =
    if (xs.isEmpty) m.unit
    else m.add(xs.head, sum(m)(xs.tail)

Cette méthode sum pourra alors être appelée ainsi :

 
Sélectionnez
sum(List("a", "bc", "def"))(stringMonoid)
sum(List(1, 2, 3))(intMonoid)

Tout ceci fonctionne, mais n'est pas très agréable à lire. Le problème est qu'il faut passer les implémentations des monoïdes à tous les codes qui les utilisent. Nous aimerions parfois que le système puisse trouver automatiquement les bons paramètres, un peu comme ce qui se passe lorsque les paramètres de types sont inférés. C'est exactement ce que permettent de faire les paramètres implicites.

15-1. Paramètres implicites : les bases

Le mot-clé implicit a été introduit en Scala 2 et peut apparaître au début d'une liste de paramètres. Sa syntaxe est la suivante :

 
Sélectionnez
ClausesParam ::= {'(' [Param {',' Param}] ')'}
                 ['(' implicit Param {',' Param} ')']

Lorsque ce mot-clé est présent, tous les paramètres de la liste deviennent implicites. La version suivante de sum, par exemple, a un paramètre m implicite :

 
Sélectionnez
1.
2.
3.
def sum[A](xs: List[A])(implicit m: Monoid[A]): A =
    if (xs.isEmpty) m.unit
    else m.add(xs.head, sum(xs.tail))

Comme on peut le constater avec cet exemple, il est possible de combiner des paramètres normaux et implicites. Cependant, il ne peut y avoir qu'une seule liste de paramètres implicites pour une méthode ou un constructeur donné et elle doit être placée à la fin.

implicit peut également servir de modificateur pour les définitions et les déclarations :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
implicit object stringMonoid extends Monoid[String] {
    def add(x: String, y: String): String = x.concat(y)
    def unit: String = ""
}
implicit object intMonoid extends Monoid[Int] {
    def add(x: Int, y: Int): Int = x + y
    def unit: Int = 0
}

L'idée principale derrière les paramètres implicites est que les arguments correspondants peuvent être omis lors de l'appel : ils seront alors inférés par le compilateur Scala.

Les arguments effectifs qui peuvent être passés comme paramètres implicites sont tous les identificateurs X accessibles à l'endroit de l'appel de méthode sans préfixe et qui dénotent une définition ou un paramètre implicite.

Si plusieurs paramètres peuvent correspondre au type du paramètre implicite, le compilateur choisira le plus spécifique en se servant des règles classiques de résolution de la surcharge statique. Supposons par exemple que l'appel

 
Sélectionnez
sum(List(1, 2, 3))

s'effectue dans un contexte où stringMonoid et intMonoid sont tous les deux visibles. Nous savons que le paramètre de type A de sum doit être instancié en Int et la seule valeur possible correspondant au paramètre formel implicite Monoid[Int] est alors intMonoid : c'est donc cet objet qui sera passé comme paramètre implicite.

Cette explication montre également que les types implicites sont inférés après tous les arguments de types.

15-2. Conversions implicites

Supposons que vous ayez une expression E de type T et que vous attendez un type S. T n'est pas conforme à S et n'est pas convertible en S par une conversion prédéfinie. Le compilateur Scala tentera d'appliquer en dernier ressort une conversion implicite I(E)I est un identificateur représentant une définition ou un paramètre implicite accessible sans préfixe à l'endroit de la conversion et pouvant s'appliquer à des paramètres de type T pour produire un résultat conforme au type S.

Les conversions implicites peuvent également s'appliquer lors des sélections de membres. Si, dans une sélection E.x, x n'est pas un membre du type E, le compilateur essaiera d'insérer une conversion implicite I(E).x pour que x soit un membre de I(E).

Voici un exemple de fonction de conversion implicite, qui convertit des entiers en instances de la classe scala.Ordered :

 
Sélectionnez
1.
2.
3.
4.
implicit def int2ordered(x: Int): Ordered[Int] = new Ordered[Int] {
    def compare(y: Int): Int =
        if (x < y) -1 else if (x > y) 1 else 0
}

15-3. Bornes vues

Les bornes vues sont du sucre syntaxique pour représenter les paramètres implicites. Soit la méthode de tri générique suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
def sort[A <% Ordered[A]](xs: List[A]): List[A] =
    if (xs.isEmpty || xs.tail.isEmpty) xs
    else {
        val {ys, zs} = xs.splitAt(xs.length / 2)
        merge(ys, zs)
    }

Le paramètre de type borné par une vue [A <% Ordered[A]] indique que sort est applicable aux listes d'éléments d'un type pour lequel il existe une conversion implicite de A vers Ordered[A]. Cette définition est donc un raccourci pour la signature suivante, qui a un paramètre implicite :

 
Sélectionnez
def sort[A](xs: List[A])(implicit c: A => Ordered[A]): List[A] = ...

(Ici, le nom du paramètre c a été choisi pour ne pas entrer en conflit avec les autres noms du programme).

Pour un exemple plus détaillé, étudiez la méthode merge utilisée par la méthode sort :

 
Sélectionnez
1.
2.
3.
4.
5.
def merge[A <% Ordered[A]](xs: List[A], ys: List[A]): List[A] =
    if (xs.isEmpty) ys
    else if (ys.isEmpty) xs
    else if (xs.head < ys.head) xs.head :: merge(xs.tail, ys)
    else if ys.head :: merge(xs, ys.tail)

Après la traduction de la borne vue et l'insertion de la conversion implicite, cette implémentation devient :

 
Sélectionnez
1.
2.
3.
4.
5.
def merge[A](xs: List[A], ys: List[A]) (implicit c: A => Ordered[A]): List[A] =
    if (xs.isEmpty) ys
    else if (ys.isEmpty) xs
    else if (c(xs.head) < ys.head) xs.head :: merge(xs.tail, ys)
    else if ys.head :: merge(xs, ys.tail)(c)

Les deux dernières lignes de cette définition illustrent deux utilisations différentes du paramètre implicite c. Dans l'avant-dernière ligne, il est appliqué dans une conversion alors que, dans la dernière ligne, il est passé comme paramètre implicite à l'appel récursif.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2017 Martin Odersky. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.