Académique Documents
Professionnel Documents
Culture Documents
Cours 7
Évaluation paresseuse
Programmation dirigée par type
1
Évaluation paresseuse
2
Évaluation paresseuse
• Avec paresseux:
val l2 = List(1, 2, 3, 4, 5)
lazy val output2 = l2.map(l=> l*2)
println(output2)
3
Pourquoi une évaluation paresseuse?
4
Pourquoi une évaluation paresseuse?
Avantages
5
Pourquoi une évaluation paresseuse?
Les inconvénients
6
Programmation dirigée par type
7
Programmation dirigée par type
8
Programmation dirigée par type
• Considérez une méthode sort qui prend comme paramètre un
List[Int] et renvoie un autre List[Int] contenant les mêmes
éléments, mais triés. L'implémentation de cette méthode
ressemblerait à ceci:
def sort(xs: List[Int]): List[Int] = {
...
... if (x < y) ...
...
}
• L'implémentation réelle de l'algorithme de tri n'a pas
d'importance dans cet exemple, la partie importante est qu'à un
moment donné, la méthode doit comparer deux éléments x et y
de la liste.
• La signature de la méthode sort, comme indiqué ci-dessus, ne
fonctionne qu'avec des collections de type List[Int]. Est-il
possible de généraliser la méthode pour qu'elle puisse trier 9des
collections de type List[Double] ou List[String]?
Programmation dirigée par type
• Une approche simple consisterait à utiliser un type polymorphe
A pour le type d'éléments:
def sort[A](xs: List[A]): List[A] = ...
• Mais cela ne suffit pas, car l'opération de comparaison <n'est
pas définie pour les types arbitraires A. L'opération sort doit
prendre comme paramètre l'opération de comparaison:
def sort[A](xs: List[A])(lessThan: (A, A) => Boolean)
: List[A] = {
...
... if (lessThan(x, y)) ...
...
}
10
Refactoring
• La fonction de comparaison présentée ci-dessus est un moyen
d'implémenter une relation de classement. En fait, il existe déjà un
type dans la bibliothèque standard qui représente les commandes:
package scala.math
trait Ordering[A] {
def compare(a1: A, a2: A): Int
def lt(a1: A, a2: A): Boolean = compare(a1, a2) <=
0
...
}
• Il a une seule méthode abstraite compare, qui prend deux valeurs et
renvoie un nombre positif si la première valeur est supérieure à la
seconde, un nombre négatif si la première valeur est inférieure à la
seconde ou 0 si les deux valeurs sont égales. Il fournit également des
opérations plus pratiques telles que lt, qui renvoie un booléen
indiquant si la première valeur est inférieure à la deuxième valeur.
11
Refactoring
• Ainsi, au lieu de paramétrer la méthode sort avec la fonction
lessThan, elle peut être refactorisée pour prendre un paramètre de
type Ordering:
def sort[A](xs: List[A])(ord: Ordering[A]): List[A] =
{
...
... if (ord.lt(x, y)) ...
...
}
• Avec ce changement, la méthode de tri peut être appelée comme ceci:
import scala.math.Ordering
sort(xs)(Ordering.Int)
sort(strings)(Ordering.String)
• Cela utilise les valeurs Int et String définies dans l'objet
scala.math.Ordering, qui produisent le bon ordre sur les entiers et les
12
chaînes.
Refactoring
• Notez que les symboles Int et String font référence à des valeurs ici,
pas à des types.
• Dans Scala, il est possible d'utiliser le même symbole pour les types
et les valeurs.
• Selon le contexte, le compilateur déduit si un symbole fait référence à
un type ou à une valeur. Voici comment la valeur Ordering.Int est
définie:
object Ordering {
val Int = new Ordering[Int] {
def compare(x: Int, y: Int) = if (x > y) 1 else if (
x < y) -1 else 0
}
}
13
Refactoring
• Notez que les symboles Int et String font référence à des valeurs ici,
pas à des types.
• Dans Scala, il est possible d'utiliser le même symbole pour les types
et les valeurs.
• Selon le contexte, le compilateur déduit si un symbole fait référence à
un type ou à une valeur. Voici comment la valeur Ordering.Int est
définie:
object Ordering {
val Int = new Ordering[Int] {
def compare(x: Int, y: Int) = if (x > y) 1 else if (
x < y) -1 else 0
}
}
14
Programmation dirigée par type
• La première étape consiste à indiquer que nous voulons que le
compilateur fournisse l'argument ord en le marquant comme
implicite:
def sort[A](xs: List[A])(implicit ord: Ordering[A]):
List[A] = ...
• Ensuite, les appels à sort peuvent omettre le paramètre ord et le
compilateur essaiera de l'inférer pour nous:
sort(xs)
sort(ys)
sort(strings)
• Le compilateur déduit la valeur de l'argument en fonction de son type
attendu.
15
Programmation dirigée par type
• Détaillons les étapes par lesquelles le compilateur passe, afin de
déduire le paramètre implicite. Considérez l'expression suivante:
• sort(xs)
• Puisque xs a le type List[Int], le compilateur corrige le paramètre de
type A de sort à Int:
• sort[Int](xs)
• En conséquence, cela fixe également le type attendu du paramètre ord
à Ordering[Int].
• Le compilateur recherche les définitions candidates qui correspondent
au type attendu Ordering[Int]. Dans notre cas, le seul candidat
correspondant est la définition Ordering.Int. Ainsi, le compilateur
passe la valeur Ordering.Int à la méthode sort:
• sort[Int](xs)(Ordering.Int)
16
Programmation dirigée par type
• Avant d'expliquer comment les valeurs candidates sont définies,
décrivons quelques faits sur les paramètres implicites.
• Une méthode ne peut avoir qu'une seule liste de paramètres implicites
et doit être la dernière liste de paramètres donnée.
• Au site d'appel, les arguments de la clause donnée sont généralement
laissés de côté, bien qu'il soit possible de les passer explicitement:
// Argument déduit par le compilateur
sort(xs)
// Argument explicite
sort(xs)(Ordering.Int.reverse)
17
Argument explicite
• Où le compilateur recherche-t-il les définitions candidates lorsqu'il
tente d'inférer un paramètre implicite de type T?
• Le compilateur recherche les définitions qui:
• avoir le type T,
• sont marqués implicites,
• sont visibles au point de l'appel de fonction, ou sont définis dans
un objet compagnon associé à T.
• S'il existe une seule définition (la plus spécifique), elle sera
considérée comme l'argument réel du paramètre implicite. Sinon, une
erreur est signalée.
18
Définitions implicites
• Une définition implicite est une définition qualifiée avec le mot clé
implicite:
object Ordering {
implicit val Int: Ordering[Int] = ...
}
• Le code ci-dessus définit une valeur implicite de type Ordering[Int],
nommée Int.
• Toute définition val, lazy val, def ou object peut être marquée comme
implicite.
• Enfin, les définitions implicites peuvent prendre des paramètres de
type et des paramètres implicites.
implicit def orderingPair[A, B](implicit
orderingA: Ordering[A],
orderingB: Ordering[B]
): Ordering[(A, B)] = ...
19
Portée de la recherche implicite
• La recherche d'une valeur implicite de type T examine d'abord toutes
les définitions implicites qui sont visibles (héritées, importées ou
définies dans une portée englobante).
• Si le compilateur ne trouve pas d'instance implicite correspondant au
type T interrogé dans la portée lexicale, il continue la recherche dans
les objets compagnons associés à T. Il y a deux concepts à expliquer
ici: les objets compagnons et les types associés à d'autres types.
• Un objet compagnon est un objet qui porte le même nom qu'un type.
Par exemple, l'objet scala.math.Ordering est le compagnon du type
scala.math.Ordering.
• Les types associés à un type T sont:
• si T a des types parents T₁ avec T₂ ... avec Tₙ, l'union des parties
de T₁, ... Tₙ ainsi que T lui-même,
• si T est un type paramétré S [T₁, T₂, ..., Tₙ], l'union des parties de
S et T₁, ..., Tₙ, 20
22
Processus de recherche implicite
• En fait, plusieurs définitions implicites correspondant au même type ne
génèrent pas d’ambiguïté si l’une est plus spécifique que l’autre.
• Une définition a: A est plus spécifique qu'une définition b: B si:
• le type A a plus de pièces «fixes»,
• ou, a est défini dans une classe ou un objet qui est une sous-classe de la
classe définissant b.
• Voyons quelques exemples de priorités au travail.
• Quelle définition implicite correspond au paramètre implicite Int lorsque la
méthode f suivante est appelée?
implicit def universal[A]: A = ???
implicit def int: Int = ???
def f(implicit n: Int) = ()
f
• Dans ce cas, comme universal prend un paramètre de type et que int n'en a
pas, int a plus de parties fixes et est considéré comme plus spécifique que
universal. Ainsi, il n'y a pas d'ambiguïté et le compilateur sélectionne int.23
Processus de recherche implicite
• Quelle définition implicite correspond au paramètre implicite
Int lorsque la méthode f suivante est appelée?
trait A {
implicit val x: Int = 0
}
trait B extends A {
implicit val y: Int = 1
def f(implicit n: Int) = ()
f
}
• Ici, parce que y est défini dans un trait qui étend A (où x est
défini), y est plus spécifique que x. Ainsi, il n'y a pas
d'ambiguïté et le compilateur sélectionne y.
24
Limites de contexte
• Le sucre syntaxique permet l'omission de la liste de paramètres implicites:
def printSorted[A: Ordering](as: List[A]): Unit = {
println(sort(as))
}
• Le paramètre de type A a un contexte lié: Ordering. Cela équivaut à écrire:
def printSorted[A](as: List[A])(implicit ev1: Ordering[A]):
Unit = {
println(sort(as))
}
• Plus généralement, une définition de méthode telle que:
• def f[A: U₁ ... : Uₙ](ps): R = ...
• Est étendu à:
• def f[A](ps)(implicit ev₁: U₁[A], ..., evₙ: Uₙ[A]): R = ..
.
25
Requête implicite
• À tout moment dans un programme, on peut interroger une valeur
implicite d'un type donné en appelant l'opération implicitement:
• scala> implicitly[Ordering[Int]]
• res0: Ordering[Int] = scala.math.Ordering$Int$@72154ac1
• Notez que ce n'est pas implicitement un mot-clé spécial, il est défini
comme une opération de bibliothèque:
• def implicitly[A](implicit value: A): A = value
26
Classes de type
• Nous avons défini un type paramétré Ordering[A], des instances
implicites de ce type pour les types concrets A et des paramètres
implicites de type Ordering[A]:
trait Ordering[A] {
def compare(a1: A, a2: A): Int
}
object Ordering {
implicit val Int: Ordering[Int] =
new Ordering[Int] {
def compare(x: Int, y: Int) = if (x < y) -
1 else if (x > y) 1 else 0
}
implicit val String: Ordering[String] =
new Ordering[String] {
def compare(s: String, t: String) = s.compareTo(t)
}
}
27
Classes de type
• Les classes de types fournissent encore une autre forme de
polymorphisme. La méthode sort peut être appelée avec des listes
contenant des éléments de tout type A pour lesquels il existe une valeur
implicite de type Ordering[A].
28
Extension rétroactive
• Les classes de types nous permettent d'ajouter de nouvelles
fonctionnalités aux types de données sans changer la définition d'origine
de ces types de données. Par exemple, considérons le type rationnel
suivant, modélisant un nombre rationnel:
case class Rational(num: Int, denom: Int)
• On peut ajouter la capacité "à comparer" au type Rational en définissant
une instance implicite de type Ordering[Rational]:
object RationalOrdering {
implicit val orderingRational: Ordering[Rational] =
new Ordering[Rational] {
def compare(q: Rational, r: Rational): Int =
q.num * r.denom - r.num * q.denom
}
}
29
Extension rétroactive - Règles
• Les instances de la classe de type Ordering[A] doivent satisfaire les
propriétés suivantes:
• inverse: le signe du résultat de la comparaison de x et y doit être
l'inverse du signe du résultat de la comparaison de y et x,
• transitive: si une valeur x est inférieure à y et que y est inférieure à z,
alors x doit aussi être inférieur à z,
• cohérent: si deux valeurs x et y sont égales, alors le signe du résultat
de la comparaison de x et z doit être le même que le signe du résultat
de la comparaison de y et z.
30
Classe de type: Anneau (Ring)
• En mathématiques, un anneau est l'une des structures algébriques
fondamentales utilisées en algèbre abstraite. Il consiste en un ensemble
équipé de deux opérations binaires qui généralisent les opérations
arithmétiques d'addition et de multiplication. Grâce à cette généralisation,
les théorèmes de l'arithmétique sont étendus aux objets non numériques
tels que les polynômes, les séries, les matrices et les fonctions.
31
Classe de type: Anneau (Ring)
• (a + b) + c = a + (b + c) + est associatif
• a + b = b + a + est commutatif
• a + 0 = a 0 est l'identité additive
• a + -a = 0 -a est l'inverse additif de a
• (a * b) * c = a * (b * c) * est associatif
• a * 1 = a 1 est l'identité multiplicative
• a * (b + c) = a * b + a * c distributivité gauche
• (b + c) * a = b * a + c * une distributivité droite
32
Classe de type: Anneau (Ring)
• Voici comment nous pouvons définir une classe de type d'anneau
dans Scala:
trait Ring[A] {
def plus(x: A, y: A): A
def mult(x: A, y: A): A
def inverse(x: A): A
def zero: A
def one: A
}
• Voici comment nous définissons une instance de Ring[Int]:
object Ring {
implicit val ringInt: Ring[Int] = new Ring[Int] {
def plus(x: Int, y: Int): Int = x + y
def mult(x: Int, y: Int): Int = x * y
def inverse(x: Int): Int = -x
def zero: Int = 0
def one: Int = 1
33
}
}
Classe de type: Anneau (Ring)
• Voici comment définir une fonction qui vérifie que la loi
d'associativité + est satisfaite par une instance Ring donnée:
def plusAssociativity[A](x: A, y: A, z: A)(implicit r
ing: Ring[A]): Boolean =
ring.plus(ring.plus(x, y), z) == ring.plus(x, ring.
plus(y, z))
• Les classes de types fournissent une forme de polymorphisme: elles
peuvent être utilisées pour implémenter des algorithmes qui peuvent
être appliqués à différents types. Le compilateur sélectionne
l'implémentation de la classe de type pour un type spécifique au
moment de la compilation.
• Une définition de classe de type est un trait qui prend des paramètres
de type et définit les opérations qui s'appliquent à ces types.
Généralement, une définition de classe de type est accompagnée de
lois, vérifiant que les implémentations de leurs opérations sont
correctes. 34
Conclusions
• Il doit y avoir une définition implicite unique (la plus spécifique)
correspondant au type interrogé pour qu'elle soit sélectionnée par le
compilateur.
35
Merci de votre attention!
PF 2021-2022 36