Il est possible de déclarer des val/var contenant des tuples
val foo: (String, Int, Char) = ("Wow", 1, 'z') var zo: (Char, (Int, Double)) = ('a', (42, 3.2))
Accéder aux composantes
On utilise la syntaxe ._i
val foo: (String, Int, Int) = ("Wow", 1, 2) println(foo._1) // affiche "Wow" en console val sum: Int = foo._2 + foo._3 // sum vaut 1 + 2 = 3 val zo: (Char, (Int, Double)) = ('a', (42, 3.2)) println(zo._2._1) // affiche 42 en console
Notation des indices
Les indices démarrent à 1 et non 0. Donc le premier élément est à l’indice 1.
Pattern-matching
Syntaxe :
expr match { case (... , ... , ...) => ... case (... , ... , ...) => ... ... case (... , ... , ...) => ... }
Motif valide de tuple
La bonne taille : autant de composantes que le tuple expr sinon, erreur de compilation!
motifs des composantes : doivent représenter des valeurs du bon type
Immutabilité des tuples
On ne peut pas modifier les composantes d’un tuple
val foo : (String, Int, Char) = ("Wow", 1, 'z') // foo._1 = "Hey" <-- interdit, ne compile pas (immutabilité)
On peut insérer un tuple dans une var (mais pas dans une val car immutable)
var blob: (String, Int) = ("Yo", 5) blob = ("Hey", 3) // autorisé: blob est var, et types compatibles
Même dans une var, un tuple reste immutable
var blob: (String, Int) = ("Yo", 5) blob = ("Hey", 3) // blob._1 = "Hey" <-- ne compile toujours pas ! (immutabilité)
Nouveaux motifs pour le pattern-matching
Le pattern-matching inspecte la valeur d’une expression mais surtout sa structure, sa “forme”.
De nouveaux motifs peuvent être utilisés pour le patter-matching :
tuples
imbriqués
variables
pour les types algébriques
Motif variable
Motif universel ⇒ il correspond à n’importe quoi du bon type, comme _, mais il permet de nommer ce n’importe quoi (→ plus joli que ._1 par exemple)
Syntaxe : identifiant quelconque, commençant par une minuscule
val truc : (Int, String) = ... truc match { case (_, nom) => println(nom) //présence d'un motif universel `_`et d'un motif variable `nom` }
L’identifiant choisi (nom dans l’exemple) devient une val :
qui n’est utilisable que dans la branche du motif
et dont la valeur n’est déterminée qu’à l’exécution.
Le motif variable peut masquer une val déclarée plus haut
val ok : String = "OK"val x: (Int, String) = (3, "Blah")x match { case(_,ok) => println(ok) //Affiche "Blah" et non "OK", ce n'est pas la même variable}
La portée de la variable ok dans le match est locale à la branche, et est donc prioritaire
Motifs imbriqués
⇒ Permet de gagner en concision, un seul motif exprime alors plusieurs critères en même temps
/** * UserInfo : un triplet (3-tuple): * identifiant, mot de passe, et droits admin */type UserInfo = (String, String, Boolean) val user1: UserInfo = ("georges", "9999", false) val user2: UserInfo = ("nlambert", "aeNg9aip", true) /** * @param user un utilisateur * @return le login de user s'il est admin, sinon "Not an admin". */def unameOfAdmin(user: UserInfo): String = { user match { case (uname, _, true) => uname case _ => "Not an admin" }}
Cela permet d’analyser la structure d’une valeur de façon “profonde” en imbriquant les motifs.
Types algébriques
Les tuples :
utiles pour regrouper plusieurs données en une seule
typealias pour les tuples : améliore la lisibilité
type Data = (Int,Int,Int) //jour, mois, année
Alias de type
Un alias est juste un autre nom, pas un nouveau type
Limitations des tuples
Accès aux composantes ._i : sujet à erreur (par exemple date en FR et ENG) ⇒ solution (partielle) → le pattern-matching et les motifs variables
Type sans alternatives : taille fixe ⇒ un n-tuple aura toujoursn composantes
Les types algébriques permettent de
définir un nouveau type de données, composite
nommer les composantes du type
autoriser les alternatives
case class
Par exemple pour modéliser une date par un jour, un mois et une année
Déclaration de type :
case class Date(jour: Int, mois: Int, an: Int)
Création de valeurs :
Date(25,12,2018)
Notes sur case clas
Pas de mot-clef new, la valeur est uniquement définie par le nom du constructeur et la valeur des composantes
Date est le nom du type et du constructeur
Pour accéder aux composantes, on utilise leur nom (notation pointée) :
val d: Date = Date(10, 11, 2012) println(d.jour + "-" + d.mois + "-" + d.an) // 10-11-2012
Les case class sont immutables, on doit donc renvoyer de nouvelles valeurs
/** * @param d une date * @return le début de mois pour la date d */def debutMois(d: Date): Date = { Date(1, d.mois, d.an)}
Pour le pattern-matching, Date peut aussi être un motif :
/** * @param d une date * @return une chaîne décrivant la date */def description(d: Date): String = { d match { case Date(25, 12, _) => "Noël" case Date(31, 10, _) => "Halloween" case Date(14, 2, _) => "St Valentin" case _ => "" } }
sealed trait et extends
Une image peut être :
soit un rectangle : défini par sa longueur et sa largeur
soit un cercle : défini par son rayon
soit plus compliquée : celle contenue dans un fichier
⇒ Création de type qui réunissent des types créés avec case class
Nommage du nouveau type par sealed trait Type
sealed trait Imagecase class Rectangle(w: Float, h: Float) extends Imagecase class Circle(r: Float) extends Imagecase class FromFile(filename: String) extends Image
On autorise des alternatives par extends ⇒ ici 3 variantes pour le type Image
Le mot-clé sealed : ces variantes (les case class de ce fichier) sont les seules possibles.
==On peut donc créer des types de tailles diverses et hétérogènes dans leurs types==
On a le choix pour le type déclaré ⇒ cela détermine la validité des accès aux composantes.
val r1: Rectangle = Rectangle(1, 3) // r1 de type Rectangle println("Aire de r1 = " + (r1.h * r1.w)) // accès composantes OK val r2: Image = Rectangle(2, 4) // r2 de type Image println("Aire de r2 = " + ???) // r2.h et r2.w n'existent pas
Le type de la variante, ici Rectangle, est plus précis. Souvent, en programmation, il vaut mieux, quand c’est possible, réfléchir de manière générale, sur le type algébrique global, ici Image.
Les types algébriques simples sont immutables ⇒ composés de case class
Pattern-matching :
motifs des constructeurs: traiter tous les cas possibles
motifs des composantes: accéder à leur contenu
/** * @param i une image * @return i est un cercle de manière certaine * */def estUnCercle(i: Image): Boolean = { i match { case Circle(_) => true case Rectangle(_, _) => false case FromFile(_) => false } }
Le type Scala Option[T]
On souhaite calculer l’aire d’une image, on peut pour un cercle, pour un rectangle, mais pas pour un fichier (d’après les case class définis précédemment)
On peut définir un nouveau type algébrique représentant qu’un résultat n’existe pas toujours :
sealed trait Resultcase class Ok(r: Double) extends Resultcase object Undef extends Result
Remarque
Une case class sans composante se note case object
Puis on peut utiliser ce type pour définir notre fonction :
def aire(i: Image): Result = { i match { case Circle(r) => Ok(math.Pi * r * r) case Rectangle(h, w) => Ok(h * w) case FromFile(_) => Undef } }
Un type prédéfini joue le rôle du type Result : le type Option[T] où T est un type quelconque (Double dans l’exemple) avec 2 alternatives :
Some : 1 composante de type T
None : pas de composante.
/** * @param i une image * @return l'aire de l'image i, si on peut la calculer sans effet * */def aire(i: Image) : Option[Double] = { i match { case Circle(r) => Some(math.Pi * r * r) case Rectangle(h, w) => Some(h * w) case FromFile(_) => None } }
Bilan
Types de données composites :
tuples et types algébriques
modéliser des données composites, agrégeant plusieurs informations
Pattern-matching :
motif tuples, variables, imbriqués, et constructeur
motifs: contraintes sur les valeurs possibles
moyen d’accéder au contenu effectif des composantes de la valeur composite