Le carnet On est
Compagnon · entraînement Exo 2

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.

~ 35 min Format type Exo 2 du DS Refais les exos des TDs aussi !

Méthode systématique

5 questions à se poser sur tout code Scala
  1. Quel type chaque expression produit-elle ? (Int, List[Int], Option[String]…)
  2. Quel pattern matche dans un match ou un case ?
  3. Effet de bord ? (println, mutation, Thread.sleep…)
  4. Ordre d'évaluation : eager (val), lazy (lazy val), by name (=>) ?
  5. Mutabilité : val vs var, case class (immuable) vs class normale ?
📌 Astuce déterminante
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 (ListList). 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éthoderenvoiece que ça fait · exemple
exists(p)Booleanau moins un élément vérifie p · List(1,2) exists(_>1)true
forall(p)Booleantous vérifient p (vrai sur liste vide !) · List(2,4) forall(_%2==0)true
count(p)Intnombre 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)Booleanl'élément x est présent · List(1,2) contains 2true
headOptionOption[T]1ᵉʳ élément ou None si vide (vs head qui throw)
indexOf(x)Intposition de x (ou -1)

Piège : forall sur une collection vide renvoie toujours true (vacuité). exists sur vide → false.

③ Découper & extraire

Accès aux extrémités : 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éthodece que ça fait · exemple sur List(1,2,3,4)
take(n) / drop(n)n premiers / sans les n premiers · take 2List(1,2) · drop 2List(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)
distinctsupprime les doublons en gardant l'ordre
reverseinverse 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

Le : 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 : reduceOptionOption.

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éthodece 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))
zipWithIndexajoute l'indice · List("a","b").zipWithIndexList((a,0),(b,1))
unzipinverse 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éthodesur Some(5)sur None
getOrElse(d)5d (valeur par défaut)
map(f)Some(f(5))None (f pas appelée)
flatMap(f) f:T=>Optionf(5)None
filter(p)Some(5) si p(5), sinon NoneNone
isDefined / isEmptytrue / falsefalse / true
get5exception
// 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

Map[K,V]
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
String ≡ Seq[Char]
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 ! messagetell (« 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.
appelce que ça fait
dest ! msgenvoi async sans réponse (tell)
dest ? msgenvoi 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 ?
Étape 1 — 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 ?
safeDiv(10, 2) = 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 : on accumule la somme des nombres pairs. Pairs dans la liste : 2, 8. Donc 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 ?
Pattern : tail-recursion avec accumulateur.

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 ?
Type somme via 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.57
area(Rectangle(3, 4))12
area(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 ?
Étape 1 : 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
Résultat : 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 ?
val a : évalué immédiatement à la déclaration ⇒ "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" ?
Acteur Counter : tient un état 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

PatternIndice visuelComportement
Map/filter chain.map(...).filter(...)Transformation puis filtrage
Pattern matching Optioncase Some(x) / NoneGestion d'absence sans exception
Fold avec accumulateurfoldLeft(z)(op)Réduction d'une collection en valeur
Tail recursionHelper interne avec accumulateurRécursion sans pile
For-comprehensionfor {…} yieldSucre pour map/flatMap/filter
Case class décompositioncase Foo(x, y)Extraction des champs
Sealed trait + ADTsealed trait + case class/objectType somme exhaustif
Actor message handlingcase msg => sender() ! reponseCommunication asynchrone
🎯 Conseil final
Refais les TDs (Scala_01_TD à Scala_04_TD). C'est ce que la prof a recommandé explicitement pour préparer l'exo 2.