Lire le code
L'examen demande de comprendre un code Scala donné, pas de le réciter. Voici une méthode + 8 codes décortiqués ligne à ligne.
★ Méthode systématique
- Quel type chaque expression produit-elle ? (Int, List[Int], Option[String]…)
- Quel pattern matche dans un
matchou uncase? - Effet de bord ? (println, mutation, Thread.sleep…)
- Ordre d'évaluation : eager (val), lazy (lazy val), by name (=>) ?
- Mutabilité : val vs var, case class (immuable) vs class normale ?
Pour comprendre un code récursif, dérouler 2-3 appels à la main. Pour
map/filter/fold, écrire le résultat intermédiaire de chaque étape.
📖 Les méthodes décortiquées
Pour lire un code, il faut savoir exactement ce que chaque appel produit : sa signature, son type de retour, et son comportement aux bords. Voici les méthodes qui reviennent tout le temps.
① Transformer une collection
map(f : T => U) : C[U]
Applique f à chaque élément, un par un. Conserve la longueur et le type de collection (List→List). Le type des éléments change de T à U.
List(1,2,3).map(_ * 2) // List(2,4,6) — 3 éléments → 3 éléments
List("a","bb").map(_.length) // List(1,2) — List[String] → List[Int]
flatMap(f : T => Iterable[U]) : C[U]
f renvoie une collection par élément, puis flatMap aplatit tout en un seul niveau. ≡ map(f).flatten. Peut changer la longueur.
List(1,2,3).flatMap(x => List(x, -x)) // List(1,-1, 2,-2, 3,-3)
List("hi","yo").flatMap(_.toList) // List(h,i,y,o) — String ≡ Seq[Char]
Piège : si f renvoie une valeur simple et non une collection, c'est map qu'il faut. flatMap + Option sert à filtrer les None : list.flatMap(toInt) ne garde que les conversions réussies.
collect(pf : PartialFunction[T, U]) : C[U]
map + filter combinés : ne garde que les éléments pour lesquels le case matche, et les transforme.
List(1,2,3,4).collect { case x if x%2==0 => x*x } // List(4,16)
② Chercher & tester (renvoient un Boolean / Option)
| méthode | renvoie | ce que ça fait · exemple |
|---|---|---|
exists(p) | Boolean | au moins un élément vérifie p · List(1,2) exists(_>1) → true |
forall(p) | Boolean | tous vérifient p (vrai sur liste vide !) · List(2,4) forall(_%2==0) → true |
count(p) | Int | nombre d'éléments vérifiant p · List(1,2,3) count(_%2==1) → 2 |
find(p) | Option[T] | premier élément vérifiant p, sinon None · List(1,2,3) find(_>1) → Some(2) |
contains(x) | Boolean | l'élément x est présent · List(1,2) contains 2 → true |
headOption | Option[T] | 1ᵉʳ élément ou None si vide (vs head qui throw) |
indexOf(x) | Int | position de x (ou -1) |
Piège : forall sur une collection vide renvoie toujours true (vacuité). exists sur vide → false.
③ Découper & extraire
head (1ᵉʳ), tail (tout sauf le 1ᵉʳ), last (dernier), init (tout sauf le dernier). ⚠ head/tail sur liste vide → exception (utiliser headOption, ou tester isEmpty).
List(5,1,3).head // 5
List(5,1,3).tail // List(1,3)
Nil.head // ⚠ NoSuchElementException
| méthode | ce que ça fait · exemple sur List(1,2,3,4) |
|---|---|
take(n) / drop(n) | n premiers / sans les n premiers · take 2 → List(1,2) · drop 2 → List(3,4) |
takeWhile(p) / dropWhile(p) | tant que p est vrai · takeWhile(_<3) → List(1,2) |
splitAt(n) | = (take n, drop n) en un tuple · splitAt 2 → (List(1,2),List(3,4)) |
partition(p) | = (ceux où p vrai, ceux où p faux) · partition(_%2==0) → (List(2,4),List(1,3)) |
span(p) | = (takeWhile p, dropWhile p) |
distinct | supprime les doublons en gardant l'ordre |
reverse | inverse l'ordre · → List(4,3,2,1) |
Plusieurs renvoient un tuple (splitAt, partition, span) — pense à les déstructurer : val (a, b) = l.partition(_>2).
④ Construire & concaténer une liste
: est toujours du côté de la collection.
x :: liste // "cons" : ajoute x EN TÊTE → nouvelle List (O(1))
1 :: 2 :: Nil // construit List(1,2) (Nil = liste vide)
l1 ::: l2 // concatène deux List
x +: seq // prepend (Seq générique) ; seq :+ x = append
s1 ++ s2 // concatène deux collections quelconques
Pourquoi :: partout en récursif : ajouter en tête est en O(1) sur une List (liste chaînée), alors qu'ajouter en queue (:+) est en O(n). D'où le pattern case h :: t qui décompose tête/reste.
En pattern matching, case h :: t lie h = tête et t = reste ; case Nil = liste vide. C'est l'écriture canonique pour parcourir une liste récursivement.
⑤ Agréger : fold & reduce
foldLeft(z : U)(op : (U, T) => U) : U
Part d'une valeur initiale z (l'accumulateur), et combine avec chaque élément de gauche à droite. L'accumulateur est à gauche dans op(acc, élément).
List(1,2,3).foldLeft(0)(_ + _)
// op(op(op(0,1),2),3) = ((0+1)+2)+3 = 6
List(1,2,3).foldLeft(10)(_ - _)
// ((10-1)-2)-3 = 4 ← l'ordre compte si op non commutative
Déroule toujours étape par étape en notant l'accumulateur :
acc=0 → +1 → acc=1 → +2 → acc=3 → +3 → acc=6
foldRight(z)(op : (T,U)=>U)
Combine de droite à gauche : op(a₁, op(a₂, op(a₃, z))). L'élément est à gauche. Pas tail-récursif (risque de pile sur très grosse liste).
reduce(op)
Comme fold mais sans valeur initiale (part du 1ᵉʳ élément). ⚠ collection vide → exception. Version sûre : reduceOption → Option.
Spécialisés (raccourcis de fold) : sum, product, min, max, maxBy(f) / minBy(f) (max selon une clé), mkString(sep) (concatène en String : List(1,2,3).mkString("-") → "1-2-3"). Bord : collection vide → fold renvoie z ; sum→0 ; mais max sur vide → exception.
⑥ Grouper, trier, zipper
groupBy(f : T => K) : Map[K, C[T]]
Range les éléments dans une Map dont la clé est f(élément). Chaque valeur est la sous-collection des éléments ayant la même clé.
List(1,2,3,4).groupBy(_ % 2)
// Map(1 -> List(1,3), 0 -> List(2,4))
// comptage par groupe : map sur les valeurs
mots.groupBy(identity).map { case (m, l) => m -> l.size }
// ou directement : mots.groupMapReduce(identity)(_ => 1)(_ + _)
| méthode | ce que ça fait · exemple |
|---|---|
sortBy(f) | trie selon une clé · l.sortBy(_.age) · List(3,1,2).sortBy(x=>x) → List(1,2,3) |
sortWith(lt) | trie avec un comparateur · l.sortWith(_ > _) (décroissant) |
zip(autre) | apparie élément par élément · List(1,2) zip List("a","b") → List((1,a),(2,b)) |
zipWithIndex | ajoute l'indice · List("a","b").zipWithIndex → List((a,0),(b,1)) |
unzip | inverse de zip · List((1,'a'),(2,'b')).unzip → (List(1,2),List(a,b)) |
zip s'arrête à la plus courte des deux collections (les éléments en trop sont ignorés).
⑦ Option — une « mini-collection » de 0 ou 1 élément
Une Option[T] se manipule comme une collection : map, flatMap, filter marchent dessus. C'est ce qui permet de chaîner sans if ni try.
| méthode | sur Some(5) | sur None |
|---|---|---|
getOrElse(d) | 5 | d (valeur par défaut) |
map(f) | Some(f(5)) | None (f pas appelée) |
flatMap(f) f:T=>Option | f(5) | None |
filter(p) | Some(5) si p(5), sinon None | None |
isDefined / isEmpty | true / false | false / true |
get | 5 | ⚠ exception |
// chaîner sans exception : tout None court-circuite
val r = users.get("bob") // Option[User]
.map(_.email) // Option[String]
.filter(_.contains("@"))
.getOrElse("inconnu") // String ← sortie sûre
Lecture la plus courante : match { case Some(x) => … ; case None => … }. Le getOrElse est le « point de sortie » qui transforme un Option[T] en T.
⑧ Map & String
m(k) // V — ⚠ throw si absent
m.get(k) // Option[V] — safe
m.getOrElse(k,d)// V ou défaut
m.contains(k) // Boolean
m + (k -> v) // ajout/maj
m - k // retrait
m.keys m.values
s.split(" ") // Array[String]
s.trim // enlève espaces
s.toInt // ⚠ throw si pas un nb
s.length
s.map(_.toUpper)// map sur les Char
l.mkString(",") // liste → String
⑨ Akka — les méthodes des acteurs
dest ! message — tell (« bang »)
Dépose message dans la mailbox de dest et continue immédiatement (asynchrone, fire-and-forget). Ne renvoie rien (Unit). C'est l'envoi par défaut.
| appel | ce que ça fait |
|---|---|
dest ! msg | envoi async sans réponse (tell) |
dest ? msg | envoi qui renvoie un Future[Any] (ask) ; exige un implicit Timeout |
sender() | dans receive : l'ActorRef de l'expéditeur du message courant → permet de répondre |
system.actorOf(Props[A], "n") | crée un acteur de type A, renvoie un ActorRef |
Props(new A(args)) | variante quand le constructeur prend des arguments |
context.actorSelection(path) | retrouve un acteur par chemin (ex "/user/serv") |
def receive : Receive | = PartialFunction[Any, Unit] : pattern matching sur les messages reçus |
Clé de lecture : actorOf renvoie un ActorRef (une référence, jamais l'objet acteur). Un acteur traite ses messages séquentiellement → un var interne est safe. La concurrence est entre acteurs, donc l'ordre des messages de deux expéditeurs est non déterministe.
1. Lambda et map
scala> val nums = List(1, 2, 3, 4)
scala> val result = nums.map(x => x * x).filter(_ > 5)
Que vaut result ?
map(x => x*x) : applique le carré à chaque élément. List(1, 4, 9, 16).Étape 2 —
filter(_ > 5) : garde les éléments > 5. List(9, 16).Résultat :
List[Int] = List(9, 16).Note :
List en entrée → List en sortie (type préservé).
2. Pattern matching sur Option
def safeDiv(a : Int, b : Int) : Option[Int] =
if (b == 0) None
else Some(a / b)
val r = safeDiv(10, 2) match {
case Some(x) if x > 3 => s"grand: $x"
case Some(x) => s"petit: $x"
case None => "division par zero"
}
Que contient r ?
Some(5).Match :
Some(5) → premier case Some(x) if x > 3 match (5 > 3 ✓). x = 5.Résultat :
r = "grand: 5".Note : la séquentialité du match est cruciale — si on avait inversé les deux
case Some, la branche "petit" aurait toujours matché en premier (warning unreachable code).
3. Fold sur liste
val data = List(5, 2, 8, 1, 9, 3)
val sumOfEven = data.foldLeft(0) { (acc, x) =>
if (x % 2 == 0) acc + x else acc
}
val maxVal = data.foldLeft(Int.MinValue)(math.max)
Que valent sumOfEven et maxVal ?
sumOfEven = 2 + 8 = 10.maxVal : on accumule le max.
foldLeft(MinValue)(math.max) applique math.max(acc, x) à chaque pas.→ max(MinValue, 5) = 5 → max(5, 2) = 5 → max(5, 8) = 8 → max(8, 1) = 8 → max(8, 9) = 9 → max(9, 3) = 9.
Donc
maxVal = 9.Note :
foldLeft est équivalent à data.max pour ce cas-là.
4. Récursivité avec accumulateur
def reverse[T](l : List[T]) : List[T] = {
def aux(reste : List[T], acc : List[T]) : List[T] =
reste match {
case Nil => acc
case h :: t => aux(t, h :: acc)
}
aux(l, Nil)
}
reverse(List(1, 2, 3))
Comment ce code inverse-t-il une liste ?
Déroulage de
reverse(List(1, 2, 3)) :aux(List(1,2,3), Nil) · h=1, t=List(2,3)→
aux(List(2,3), List(1)) · h=2, t=List(3)→
aux(List(3), List(2,1)) · h=3, t=Nil→
aux(Nil, List(3,2,1)) · match Nil → renvoie acc.Résultat :
List(3, 2, 1).Note :
h :: acc ajoute h en tête de acc ⇒ inversion naturelle.
5. Case class et pattern matching
sealed trait Shape
case class Circle(r : Double) extends Shape
case class Rectangle(w : Double, h : Double) extends Shape
case object Point extends Shape
def area(s : Shape) : Double = s match {
case Circle(r) => math.Pi * r * r
case Rectangle(w, h) => w * h
case Point => 0.0
}
Pourquoi ce code est-il robuste ?
sealed trait : le compilateur connaît toutes les variantes possibles (Circle, Rectangle, Point).Exhaustivité vérifiée : si on oublie un cas, le compilateur émet un warning.
Pattern matching avec décomposition :
case Circle(r) extrait le rayon directement.Test :
area(Circle(2)) → π × 4 ≈ 12.57area(Rectangle(3, 4)) → 12area(Point) → 0
6. For-comprehension
val users = List("alice", "bob", "charlie")
val domains = List("@a.com", "@b.com")
val emails = for {
u <- users if u.length > 3
d <- domains
} yield u + d
Que contient emails ?
users if u.length > 3 ⇒ "alice" (5), "charlie" (7). "bob" (3) éliminé.Étape 2 : produit cartésien avec
domains :- alice + @a.com
- alice + @b.com
- charlie + @a.com
- charlie + @b.com
List("alice@a.com", "alice@b.com", "charlie@a.com", "charlie@b.com").Équivalent :
users.filter(_.length > 3).flatMap(u => domains.map(d => u + d)).
7. Lazy val et by name
def compute() : Int = { println("calcul") ; 42 }
val a = compute()
lazy val b = compute()
def c = compute()
println("--- avant usage ---")
println(a)
println(b)
println(b)
println(c)
println(c)
Quel est l'ordre d'affichage ?
"calcul" imprimé.lazy val b : non évalué tant que pas utilisé.
def c : ré-évalué à chaque appel.
Sortie :
calcul ← val a
--- avant usage ---
42 ← println(a)
calcul ← b évalué (1er usage)
42 ← println(b)
42 ← println(b), b pas re-évalué
calcul ← c re-évalué
42 ← println(c)
calcul ← c re-évalué
42 ← println(c)
Total : 4 affichages de "calcul" — pas 5.
8. Actor Akka
class Counter extends Actor {
var n : Int = 0
def receive : Receive = {
case "inc" => n += 1
case "get" => sender() ! n
}
}
// Quelque part :
counter ! "inc"
counter ! "inc"
counter ! "inc"
counter ! "get" // pour celui qui demande
Que reçoit l'émetteur de "get" ?
n (mutable) accessible uniquement de l'intérieur — pas de race condition.Traitement séquentiel :
"inc" → n = 1"inc" → n = 2"inc" → n = 3"get" → sender() ! 3 (envoie l'entier 3 à l'expéditeur)Résultat : l'émetteur reçoit un message
3 : Int.Note : l'utilisation de
var n ici est safe parce que l'acteur traite ses messages séquentiellement. Pas besoin de mutex.
★ Récap des patterns à reconnaître
| Pattern | Indice visuel | Comportement |
|---|---|---|
| Map/filter chain | .map(...).filter(...) | Transformation puis filtrage |
| Pattern matching Option | case Some(x) / None | Gestion d'absence sans exception |
| Fold avec accumulateur | foldLeft(z)(op) | Réduction d'une collection en valeur |
| Tail recursion | Helper interne avec accumulateur | Récursion sans pile |
| For-comprehension | for {…} yield | Sucre pour map/flatMap/filter |
| Case class décomposition | case Foo(x, y) | Extraction des champs |
| Sealed trait + ADT | sealed trait + case class/object | Type somme exhaustif |
| Actor message handling | case msg => sender() ! reponse | Communication asynchrone |
Refais les TDs (Scala_01_TD à Scala_04_TD). C'est ce que la prof a recommandé explicitement pour préparer l'exo 2.