Skip to content

Scala Aide-mémoire

Langage fonctionnel/OO hybride sur la JVM.

01

Bases

Variables et types

Préférez val (immuable) à var (mutable) pour un code plus sûr et plus prévisible. Scala infère les types, mais les annotations explicites aident la lisibilité pour les APIs publiques. Tout est un objet—pas de primitives (Int, Boolean sont des classes).

scala
val name = "Alice"   // immutable (preferred)
var age = 30          // mutable
val pi: Double = 3.14159
val isDev: Boolean = true
val nums: List[Int] = List(1, 2, 3)
println(name.getClass)  // class java.lang.String

Interpolation de chaînes

s"..." active l'interpolation ${expr}. f"..." ajoute le formatage style printf (%s, %.2f). raw"..." désactive les séquences d'échappement. Ces préfixes rendent la construction de chaînes type-safe et lisible.

scala
val name = "Alice"
val age = 30
println(s"Name: ${name}, Age: ${age}")
println(s"Next year: ${age + 1}")
println(s"Upper: ${name.toUpperCase}")
println(f"${name}%s weighs ${65.5}%.1f kg")  // formatted
println(raw"No \n escape")  // raw string

Tuples

Les tuples groupent 2-22 valeurs hétérogènes. Accès via _1, _2 (indexé à partir de 1). Déstructurez avec val (a, b) = tuple. Pour plus de 2 éléments, préférez les case classes pour les champs nommés et une meilleure lisibilité.

scala
val pair = ("Alice", 30)
println(pair._1)  // Alice
println(pair._2)  // 30
val (name, age) = pair  // destructure
println(s"${name}: ${age}")
// Scala 3: val p = ("a", 1, 2.0)
val triple = ("a", 1, 2.0)
println(triple._3)  // 2.0

Inférence de type et ascription

Scala infère les types pour les variables locales et les types de retour. Utilisez des types explicites pour les APIs publiques, les fonctions récursives et les cas ambigus. L'ascription de type (expr: Type) force un type—utile pour l'upcasting ou la levée d'ambiguïté.

scala
val x = 42           // Int inferred
val y: Long = 42      // explicit Long
val z = 42: Long      // type ascription
val list = List(1, 2, 3)  // List[Int]
val mixed: List[Any] = List(1, "a", true)
def double(x: Int) = x * 2  // return type inferred

Unit et Nothing

Unit est comme void (une valeur : ()). Nothing est le type bas sans instances—utilisé pour les fonctions qui ne retournent jamais (throw, boucle infinie). Nothing est un sous-type de tous les types, permettant une inférence de type flexible.

scala
def printIt(x: Int): Unit = println(x)  // like void
val u: Unit = ()    // Unit has one value: ()
// Nothing is the bottom type—no instances
def error(msg: String): Nothing =
  throw new RuntimeException(msg)
// Nothing is a subtype of everything
val n: Nothing = error("boom")
02

Chaînes de caractères

Méthodes courantes de chaînes

Les chaînes Scala sont des chaînes Java avec des méthodes supplémentaires via des conversions implicites (StringOps). La plupart des méthodes retournent de nouvelles chaînes (immuables). Utilisez-les au lieu de boucles manuelles pour la clarté et l'exactitude.

scala
val s = "Hello, World"
println(s.length)        // 12
println(s.toUpperCase)   // HELLO, WORLD
println(s.toLowerCase)   // hello, world
println(s.split(", "))   // Array(Hello, World)
println(s.replace("o", "0"))  // Hell0, W0rld
println(s.reverse)       // dlroW ,olleH
println(s.contains("World"))  // true

Chaînes multilignes (triple guillemets)

Les chaînes à triple guillemets préservent tous les espaces et nouvelles lignes. Utilisez stripMargin avec | pour aligner le code proprement—seul le texte après | est conservé. Idéal pour SQL, JSON ou les templates intégrés au code.

scala
val sql = """
  SELECT * FROM users
  WHERE age > 18
  ORDER BY name
"""
println(sql.trim)
// StripMargin for clean indentation
val text = """|Hello
              |World""".stripMargin
println(text)  // Hello
World

Construction de chaînes

mkString joint les collections avec préfixe/suffixe optionnel—idiomatique et efficace. Utilisez StringBuilder pour construire de grandes chaînes dans des boucles. Évitez la concaténation + répétée dans les boucles (crée de nombreux objets intermédiaires).

scala
val parts = List("apple", "banana", "cherry")
println(parts.mkString(", "))     // apple, banana, cherry
println(parts.mkString("[", ", ", "]"))  // [apple, banana, cherry]
val sb = new StringBuilder
for (p <- parts) sb.append(p).append(" ")
println(sb.toString.trim)

Chaîne vers nombre

toInt/toDouble lèvent une exception sur entrée invalide. Utilisez toIntOption (Scala 2.13+) pour une analyse sûre retournant Option. Pour l'analyse en masse, utilisez Try ou Either pour gérer les erreurs fonctionnellement sans exceptions.

scala
val n = "42".toInt        // 42
val d = "3.14".toDouble    // 3.14
val b = "true".toBoolean   // true
val safe = "abc".toIntOption  // Some(42) or None
// Handling errors
val result = try "x".toInt catch { case _ => 0 }
println(result)  // 0

Regex

.r convertit une chaîne en Regex. findFirstIn retourne Option, findAllIn retourne un itérateur. Utilisez dans le pattern matching avec case email(e) => pour l'extraction. Regex est le Pattern de Java en dessous.

scala
import scala.util.matching.Regex
val email: Regex = "[\w.]+@[\w]+\.[a-z]+".r
val text = "Contact: [email protected]"
email.findFirstIn(text) match {
  case Some(e) => println(s"Found: ${e}")
  case None => println("No email")
}
val replaced = "[0-9]+".r.replaceAllIn("a1b2c3", "#")
println(replaced)  // a#b#c#
03

Structures de données

List et Seq

List est une liste chaînée immuable—O(1) head/prepend, O(n) accès aléatoire. Utilisez Vector pour l'accès aléatoire (O(1) effectif). +: prepend, :+ append. Préférez les collections immuables pour la thread safety.

scala
val nums = List(1, 2, 3, 4, 5)
println(nums.head)      // 1
println(nums.tail)      // List(2,3,4,5)
println(nums.reverse)   // List(5,4,3,2,1)
println(nums.take(2))   // List(1, 2)
println(nums.drop(2))   // List(3, 4, 5)
println(nums.mkString)  // 12345
val combined = 0 +: nums :+ 6  // List(0,1,2,3,4,5,6)

Map

Map est immuable—les opérations retournent de nouveaux Maps. Utilisez get(key) pour un accès Option, getOrElse pour les valeurs par défaut. + ajoute/met à jour, - supprime. Pour les maps mutables, utilisez scala.collection.mutable.Map. Les clés doivent être Hashable.

scala
val ages = Map("Alice" -> 30, "Bob" -> 25)
println(ages("Alice"))            // 30 (throws if missing)
println(ages.getOrElse("Eve", 0)) // 0 (safe)
val updated = ages + ("Eve" -> 28)  // new Map
val removed = ages - "Bob"
ages.foreach { case (k, v) => println(s"${k}: ${v}") }
println(ages.keys)  // Set(Alice, Bob)

Set

Set est immuable avec O(1) contains. union (|), intersect (&), diff (~) pour les opérations ensemblistes. + ajoute, - supprime. Utilisez pour la déduplication et le test d'appartenance. Variante mutable : scala.collection.mutable.Set.

scala
val a = Set(1, 2, 3)
val b = Set(3, 4, 5)
println(a union b)        // Set(1,2,3,4,5)
println(a intersect b)    // Set(3)
println(a diff b)         // Set(1,2)
println(a subsetOf(Set(1,2,3,4)))  // true
val added = a + 6  // Set(1,2,3,6)

Option (sécurité null)

Option remplace null—Some(value) ou None. Utilisez map/filter/flatMap pour les transformations, getOrElse pour les valeurs par défaut. Les for-comprehensions fonctionnent sur Option. Cela élimine NullPointerException en rendant l'absence explicite dans le type.

scala
def findUser(id: Int): Option[String] =
  if (id == 1) Some("Alice") else None
val name = findUser(1)
println(name.getOrElse("Unknown"))  // Alice
println(name.map(_.toUpperCase))    // Some(ALICE)
println(name.filter(_.startsWith("A")))  // Some(Alice)
val result = for {
  n <- findUser(1)
  if n.startsWith("A")
} yield n.toUpperCase  // Some(ALICE)

Array et Vector

Array est un tableau Java mutable (le plus rapide, mais pas de mises à jour fonctionnelles). Vector est immuable avec accès aléatoire et mises à jour effectivement en O(1)—préféré pour les collections immuables à accès aléatoire. Utilisez List pour le séquentiel, Vector pour l'indexé.

scala
val arr = Array(1, 2, 3, 4)  // mutable, Java array
arr(0) = 10
println(arr(0))  // 10
val vec = Vector(1, 2, 3, 4)  // immutable, fast random access
println(vec(2))  // 3
val updated = vec.updated(0, 10)  // Vector(10,2,3,4)
// Vector: O(1) random access + immutable
// Array: mutable, Java interop, fastest
04

Flux de contrôle

If / Else (expression)

Dans Scala, if/else est une expression qui retourne une valeur. Cela élimine le besoin d'un opérateur ternaire. Les deux branches doivent avoir des types compatibles. Utilisez cela pour une affectation conditionnelle concise.

scala
val score = 85
val grade =
  if (score >= 90) "A"
  else if (score >= 80) "B"
  else if (score >= 70) "C"
  else "F"
println(grade)  // B
// if returns a value—no ternary operator needed

For comprehension

Les for comprehensions itèrent et peuvent filtrer (if guards) et transformer (yield). Sans yield, c'est une boucle ; avec yield, ça construit une collection. to inclut la fin, until l'exclut. Équivalent aux chaînes flatMap/map/filter.

scala
for (i <- 1 to 5) println(i)  // 1 2 3 4 5
for (i <- 1 until 5) print(i)  // 1 2 3 4
// With yield (generates collection)
val doubled = for (n <- List(1,2,3)) yield n * 2
println(doubled)  // List(2, 4, 6)
// With filter (guard)
val evens = for (n <- 1 to 10 if n % 2 == 0) yield n
println(evens.toList)  // List(2,4,6,8,10)

Match (pattern matching)

match est le pattern matching puissant de Scala—comme un switch sous stéroïdes. Supporte les littéraux, OR (|), les gardes (if), la correspondance de type et la déstructuration. Doit être exhaustif (le compilateur avertit sur les cas manquants). C'est une expression retournant une valeur.

scala
val n = 2
val label = n match {
  case 0 => "zero"
  case 1 | 2 | 3 => "small"
  case x if x < 10 => "medium"
  case _ => "large"
}
println(label)  // small
// Match on types
def describe(x: Any): String = x match {
  case i: Int => s"Int: ${i}"
  case s: String => s"String: ${s}"
  case _ => "unknown"
}

While et do-while

while/do-while sont des boucles impératives qui retournent Unit. Elles nécessitent un état mutable (var). Préférez les for-comprehensions ou la récursion pour du Scala fonctionnel idiomatique. Utilisez while uniquement lorsque la performance l'exige ou pour les effets de bord.

scala
var i = 0
while (i < 3) {
  println(i)
  i += 1
}
var j = 0
do {
  println(j)
  j += 1
} while (j < 3)
// Prefer recursion or for-comprehensions for immutability

Try / Catch / Finally

try/catch/finally est comme Java mais utilise le pattern matching dans catch. Préférez Try pour la gestion fonctionnelle des erreurs—il enveloppe les exceptions comme des valeurs Success/Failure, permettant les chaînes map/flatMap sans boilerplate try/catch.

scala
import scala.util.{Try, Success, Failure}
val result = try {
  "abc".toInt
} catch {
  case e: NumberFormatException => 0
} finally {
  println("cleanup")
}
println(result)  // 0
// Functional alternative
val r2 = Try("abc".toInt).getOrElse(0)
println(r2)  // 0
05

Fonctions

Définition de méthode

Les méthodes utilisent def name(params): ReturnType = body. Paramètres par défaut et arguments nommés supportés. Retour Unit = effet de bord uniquement. Les corps à expression unique omettent les accolades. = est requis (sans lui, c'est une procédure retournant Unit).

scala
def add(a: Int, b: Int): Int = a + b
def greet(name: String, greeting: String = "Hello"): String =
  s"${greeting}, ${name}!"
def log(msg: String): Unit = println(msg)
println(add(3, 4))           // 7
println(greet("Alice"))      // Hello, Alice!
println(greet("Bob", greeting = "Hi"))  // named arg

Lambda (fonction anonyme)

Lambdas : (params) => body. Utilisez _ comme raccourci pour les paramètres uniques (x => x * 2 devient _ * 2). Plusieurs _ font référence à des paramètres différents (_ + _). Les lambdas sont first-class—passez-les à map, filter, reduce, etc.

scala
val square = (x: Int) => x * x
println(square(5))  // 25
val nums = List(1, 2, 3)
println(nums.map(_ * 2))      // List(2, 4, 6)
println(nums.filter(_ > 1))   // List(2, 3)
println(nums.reduce(_ + _))   // 6
// _ is shorthand for the parameter

Fonctions d'ordre supérieur

Les fonctions d'ordre supérieur prennent ou retournent des fonctions. Cela permet des abstractions puissantes : map/filter/reduce, composition, application partielle. makeAdder retourne une closure capturant n. C'est le cœur de la programmation fonctionnelle.

scala
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
println(applyTwice(_ + 3, 5))  // 11
def makeAdder(n: Int): Int => Int = _ + n
val add5 = makeAdder(5)
println(add5(10))  // 15
// Functions returning functions = currying-like

Currying et application partielle

Le currying sépare les paramètres en plusieurs listes : def f(a)(b). Application partielle avec _ pour créer des fonctions spécialisées. Les listes de paramètres multiples améliorent l'inférence de type (le compilateur peut inférer les types de f depuis la liste). Courant dans les APIs de collection.

scala
def add(a: Int)(b: Int): Int = a + b  // curried
val add5 = add(5)_  // partially applied
println(add5(3))  // 8
// Multiple parameter lists
def foldLeft[A, B](list: List[A])(z: B)(f: (B, A) => B): B = ???
// Helps type inference
val sum = List(1,2,3).foldLeft(0)(_ + _)

By-name et lazy

Les paramètres by-name (=> T) sont évalués paresseusement à chaque utilisation—utiles pour le logging (sauter le msg coûteux si désactivé) et les structures de contrôle personnalisées. lazy val diffère l'initialisation jusqu'au premier accès—utilisez pour les valeurs coûteuses ou optionnelles.

scala
// By-name parameter: evaluated on each use
def debug(msg: => String): Unit =
  if (debugEnabled) println(msg)
// Lazy evaluation
lazy val expensive = computeHeavy()
println(expensive)  // computed now
def computeHeavy(): Int = { println("computing"); 42 }
06

Classes et POO

Classe et constructeur

Les paramètres du constructeur primaire sont dans la signature de la classe. Les paramètres val deviennent des champs immuables (getter public), var mutables. Le corps de la classe EST le constructeur. Les constructeurs auxiliaires utilisent def this(...) et doivent appeler un autre constructeur.

scala
class Person(val name: String, val age: Int) {
  // Constructor params with val/var become fields
  def greet: String = s"Hi, I'm ${name}"
  def isAdult: Boolean = age >= 18
}
val p = new Person("Alice", 30)
println(p.greet)   // Hi, I'm Alice
println(p.name)    // Alice (val field)
println(p.isAdult) // true

Case class

Les case classes sont des classes de données immuables avec equals, hashCode, toString, copy et objet compagnon avec apply/unapply auto-générés. Utilisez pour la modélisation de données et le pattern matching. Pas besoin de 'new'. Elles sont la fondation des ADT dans Scala.

scala
case class Point(x: Int, y: Int)
val p1 = Point(3, 4)  // no 'new' needed
val p2 = Point(3, 4)
println(p1 == p2)  // true (value equality)
val moved = p1.copy(x = 5)  // Point(5, 4)
println(p1.x, p1.y)  // 3 4 (fields auto-visible)
// Auto: equals, hashCode, toString, copy, companion

Traits (interfaces avec implémentation)

Les traits sont comme les interfaces Java mais peuvent avoir une implémentation. Une classe peut mixer plusieurs traits (avec extends/with). Les traits permettent l'héritage multiple de comportement. Utilisez pour les interfaces partagées, les mixins et les modifications empilables via linéarisation.

scala
trait Greetable {
  def name: String  // abstract
  def greet: String = s"Hello, ${name}"  // concrete
}
trait Named {
  val name: String
}
class User(val name: String) extends Greetable
val u = new User("Alice")
println(u.greet)  // Hello, Alice
// Stackable traits via linearization

Object (singleton)

object déclare un singleton (une instance). Les objets compagnons (même nom qu'une classe) contiennent des méthodes de type statique, des factories (apply) et des extracteurs (unapply). Utilisez apply pour les méthodes factory afin que les appelants omettent 'new'. C'est du Scala idiomatique.

scala
object Config {
  val version = "1.0"
  def load(): Map[String, String] = Map("key" -> "value")
}
println(Config.version)  // 1.0
// Companion object (same name as class)
class Person(val name: String)
object Person {
  def apply(name: String): Person = new Person(name)
}
val p = Person("Alice")  // uses apply, no 'new'

Héritage et classe abstraite

abstract class peut avoir des membres non implémentés. Utilisez extends pour hériter, override pour redéfinir. Préférez les traits pour les mixins (héritage multiple). Utilisez abstract class lorsque vous avez besoin de paramètres de constructeur ou d'un type de base. Héritage simple des classes.

scala
abstract class Shape {
  def area: Double  // abstract
  def describe: String = s"Area: ${area}"
}
class Circle(r: Double) extends Shape {
  def area: Double = math.Pi * r * r
}
val c = new Circle(5)
println(c.describe)  // Area: 78.53...
// override required for concrete members
// abstract class vs trait: use abstract for base, trait for mixins
07

Collections et fonctionnel

Map / Filter / Fold

map transforme, filter sélectionne, reduce/foldLeft agrège. Ce sont le cœur de la transformation fonctionnelle des données. foldLeft prend une graine et est associatif ; reduce nécessite non-vide. Utilisez-les au lieu des boucles pour la clarté et l'immutabilité.

scala
val nums = List(1, 2, 3, 4, 5)
println(nums.map(_ * 2))           // List(2,4,6,8,10)
println(nums.filter(_ % 2 == 0))   // List(2,4)
println(nums.reduce(_ + _))        // 15
println(nums.foldLeft(0)(_ + _))   // 15
println(nums.sum)                  // 15
println(nums.mkString(", "))       // 1, 2, 3, 4, 5

FlatMap et for-comprehension

flatMap map et aplatit en une étape—essentiel pour les collections imbriquées et les opérations monadiques. Les for-comprehensions sont du sucre syntaxique pour les chaînes flatMap/map/filter. Utilisez pour les transformations imbriquées complexes—c'est plus lisible.

scala
val nested = List(List(1, 2), List(3, 4))
println(nested.flatten)        // List(1,2,3,4)
println(nested.flatMap(_.map(_ * 2)))  // List(2,4,6,8)
// Equivalent for-comprehension:
val result = for {
  inner <- nested
  n <- inner
} yield n * 2
println(result)  // List(2,4,6,8)

Regroupement et tri

groupBy partitionne par clé dans un Map. sorted trie naturellement, sortBy par fonction clé, sortWith avec un comparateur. Ils retournent de nouvelles collections (immuables). Utilisez pour l'analyse de données, la catégorisation et l'ordonnancement.

scala
val words = List("apple", "bat", "cat", "ant")
val byFirst = words.groupBy(_.head)
// Map(a -> List(apple, ant), b -> List(bat), c -> List(cat))
println(byFirst)
val sorted = words.sorted  // List(ant, apple, bat, cat)
val byLen = words.sortBy(_.length)  // List(ant, bat, cat, apple)
val desc = words.sortWith(_ > _)  // descending
println(sorted, byLen)

Either et Try

Try enveloppe les exceptions comme des valeurs Success/Failure. Either représente Left (erreur) ou Right (succès)—utilisez pour les erreurs de domaine où vous voulez distinguer les types d'erreur. Les deux supportent map/flatMap pour la propagation fonctionnelle des erreurs.

scala
import scala.util.{Try, Success, Failure}
def parse(s: String): Try[Int] = Try(s.toInt)
parse("42") match {
  case Success(n) => println(s"OK: ${n}")
  case Failure(e) => println(s"Err: ${e.getMessage}")
}
// Either for domain errors
def divide(a: Int, b: Int): Either[String, Int] =
  if (b == 0) Left("div by zero") else Right(a / b)
println(divide(10, 2))  // Right(5)
println(divide(10, 0))  // Left(div by zero)

Collections paresseuses (View et Lazy)

.view rend les collections paresseuses—les opérations sont différées jusqu'à être forcées (toList, sum, etc.). Évite les collections intermédiaires pour une meilleure performance sur les grandes données. LazyList (anciennement Stream) permet les séquences infinies—éléments calculés à la demande.

scala
val nums = (1 to 1000000).view
val result = nums
  .filter(_ % 2 == 0)
  .map(_ * 2)
  .take(5)
  .toList  // forces evaluation
println(result)  // List(4, 8, 12, 16, 20)
// view = lazy, no intermediate collections
// LazyList (Stream) for infinite sequences
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _)
08

Pattern matching

Correspondance de case classes

Sealed traits + case classes forment des Algebraic Data Types (ADT). Le compilateur vérifie l'exhaustivité—ajoutez un cas et il avertit où les correspondances doivent être mises à jour. Le pattern matching déstructure les case classes directement. C'est la modélisation Scala idiomatique.

scala
sealed trait Shape
case class Circle(r: Double) extends Shape
case class Square(s: Double) extends Shape
case class Rect(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
  case Circle(r) => math.Pi * r * r
  case Square(s) => s * s
  case Rect(w, h) => w * h
}
println(area(Circle(5)))  // 78.53...

Gardes et conditions

Les gardes (if condition) ajoutent des vérifications à l'exécution aux patterns. Elles rendent la correspondance plus expressive. L'ordre compte—première correspondance gagne. Utilisez les gardes pour les intervalles, conditions ou logique complexe que les patterns simples ne peuvent exprimer.

scala
val n = 15
val desc = n match {
  case x if x < 0 => "negative"
  case 0 => "zero"
  case x if x % 2 == 0 => "even"
  case _ => "odd"
}
println(desc)  // odd
// Guards add boolean conditions to patterns

Correspondance de collections

:: le pattern cons déstructure les listes en head et tail. _* correspond à zéro ou plusieurs éléments dans les tableaux/listes. Ces patterns permettent le traitement récursif des listes et la décomposition structurelle. Puissant pour l'analyse et le parcours d'arbre.

scala
val list = List(1, 2, 3, 4)
list match {
  case Nil => println("empty")
  case head :: Nil => println(s"one: ${head}")
  case head :: tail => println(s"head=${head}, rest=${tail}")
  case _ => println("other")
}
// head :: tail destructures a list
val arr = Array(1, 2, 3)
arr match {
  case Array(1, _*) => println("starts with 1")
  case _ => println("other")
}

Correspondance d'Option et Either

Le pattern matching sur Option/Either est idiomatique—Some(x)/None, Right(x)/Left(e). Les for-comprehensions se désucrent en flatMap avec match. Cela fait couler naturellement la gestion des erreurs sans vérifications if-else explicites.

scala
def find(id: Int): Option[String] =
  if (id == 1) Some("Alice") else None
find(1) match {
  case Some(name) => println(s"Found: ${name}")
  case None => println("Not found")
}
// In for-comprehensions
val result = for {
  name <- find(1)
  upper = name.toUpperCase
} yield upper
println(result)  // Some(ALICE)

Extracteurs (unapply)

Les extracteurs personnalisés via unapply permettent le pattern matching sur n'importe quel type. La méthode unapply retourne Option des valeurs extraites. Cela vous permet de définir vos propres patterns—puissant pour les DSL et l'analyse. Les case classes auto-génèrent unapply.

scala
object Email {
  def unapply(s: String): Option[(String, String)] = {
    val parts = s.split("@")
    if (parts.length == 2) Some((parts(0), parts(1))) else None
  }
}
"[email protected]" match {
  case Email(user, domain) =>
    println(s"User: ${user}, Domain: ${domain}")
  case _ => println("Not an email")
}
09

Génériques et implicites

Classes et méthodes génériques

Les génériques (paramètres de type [A]) écrivent du code type-safe et réutilisable. Utilisez [A] pour un seul type, [A, B] pour deux. L'inférence de type trouve généralement le type. Les génériques sont effacés à l'exécution (limitation JVM) mais vérifiés à la compilation.

scala
class Stack[A] {
  private var items: List[A] = Nil
  def push(x: A): Unit = { items = x :: items }
  def pop: Option[A] = items.headOption
}
val s = new Stack[Int]
s.push(1); s.push(2)
println(s.pop)  // Some(2)
def first[A](list: List[A]): Option[A] = list.headOption
println(first(List("a", "b")))  // Some(a)

Bornes de type

<: borne supérieure (A est un sous-type), >: borne inférieure (A est un super-type). Context bounds (A: Ordering) nécessitent une valeur implicite de ce type. View bounds (A <% B) sont dépréciés—utilisez les context bounds à la place. Ils contraignent les paramètres de type.

scala
// Upper bound: A must be Animal or subclass
class Box[A <: Animal](val content: A)
// Lower bound: A must be Dog or superclass
class Kennel[A >: Dog](val occupant: A)
// Context bound: A must have an Ordering
def max[A: Ordering](a: A, b: A): A =
  if (implicitly[Ordering[A]].gt(a, b)) a else b
abstract class Animal { def name: String }
class Dog extends Animal { def name = "Rex" }

Paramètres implicites

Les paramètres implicites sont injectés par le compilateur depuis la portée. Utilisez pour la configuration, les type classes ou les dépendances que vous ne voulez pas passer partout. Déclarez avec implicit val/def. Le compilateur recherche la portée englobante et les objets compagnons.

scala
def greet(name: String)(implicit greeting: String): String =
  s"${greeting}, ${name}!"
implicit val defaultGreeting: String = "Hello"
println(greet("Alice"))  // Hello, Alice! (implicit injected)
println(greet("Bob")("Hi"))  // Hi, Bob! (explicit override)
// Compiler finds implicit in scope

Type classes (conversions implicites)

Les type classes (via implicites) ajoutent du comportement aux types sans les modifier—polymorphisme ad hoc. Définissez un trait, fournissez des instances implicites et utilisez des params implicites. C'est ainsi que fonctionnent Ordering, Numeric et Show. Plus flexible que l'héritage.

scala
trait Show[A] { def show(a: A): String }
object Show {
  implicit val intShow: Show[Int] = (a: Int) => a.toString
  implicit val strShow: Show[String] = identity
}
def printIt[A](a: A)(implicit s: Show[A]): Unit =
  println(s.show(a))
printIt(42)       // 42
printIt("hello")  // hello
// Type class: ad-hoc polymorphism

Méthodes d'extension (Scala 2)

Les implicit classes ajoutent des méthodes d'extension aux types existants. Définissez une implicit class avec un seul paramètre, et ses méthodes deviennent disponibles sur ce type. Utilisez pour ajouter des méthodes utilitaires à Int, String, etc. Scala 3 utilise la syntaxe plus propre 'extension'.

scala
implicit class IntOps(val n: Int) extends AnyVal {
  def times(f: => Unit): Unit = (1 to n).foreach(_ => f)
  def squared: Int = n * n
}
5.times { print("hi") }  // hihihihihi
println(5.squared)  // 25
// Scala 3: extension (n: Int) def squared = n * n
10

Concurrence et Future

Future et async

Future représente un calcul asynchrone. onComplete enregistre un callback. Ne bloquez jamais (Await.result) dans les serveurs web de production—cela immobilise les threads. Utilisez les for-comprehensions pour chaîner les futures fonctionnellement. Nécessite un ExecutionContext.

scala
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f: Future[Int] = Future {
  Thread.sleep(1000)
  42
}
f.onComplete {
  case scala.util.Success(v) => println(s"Got ${v}")
  case scala.util.Failure(e) => println(s"Err: ${e}")
}
// Don't block in production—use callbacks or for-comprehensions

Composition de Futures

Les for-comprehensions sur Futures les exécutent séquentiellement (chacun attend le précédent). Pour l'exécution parallèle, démarrez tous les Futures d'abord, puis utilisez Future.sequence pour combiner. Future.traverse map + sequence en une étape. C'est la manière idiomatique de composer du travail async.

scala
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f1 = Future { 10 }
val f2 = Future { 20 }
val sum = for {
  a <- f1
  b <- f2
} yield a + b  // Future(30)
// Parallel execution
val results = Future.sequence(List(
  Future { 1 }, Future { 2 }, Future { 3 }
))
results.map(_.sum)  // Future(6)

Collections parallèles

.par convertit une collection en version parallèle—les opérations utilisent plusieurs threads automatiquement. Bon pour le travail CPU-bound sur de grandes collections. Attention : les opérations non associatives (comme la soustraction) peuvent donner des résultats différents. Pas pour le travail I/O-bound.

scala
val nums = (1 to 1000000).toList
val sum = nums.par.sum  // parallel sum
println(sum)
val doubled = nums.par.map(_ * 2).toList
// .par converts to ParCollection
// Operations run on multiple threads
// Use for CPU-bound work on large collections

Promise (Future manuelle)

Promise est le côté inscriptible d'un Future—vous le complétez manuellement avec success/failure. Utilisez lors du pont entre des APIs à callback et les Futures, ou lorsque vous devez compléter un Future depuis plusieurs endroits. Future est en lecture seule ; Promise est write-once.

scala
import scala.concurrent.{Promise, Future, ExecutionContext}
import ExecutionContext.Implicits.global
val p = Promise[Int]()
val f = p.future
// Complete the promise from another thread
Future { Thread.sleep(100); p.success(42) }
f.foreach(println)  // 42 (when complete)
// p.failure(new Exception) for errors
// Promise = write side, Future = read side

Sync vs async (Await)

Await.result bloque le thread courant jusqu'à ce que le Future se termine (avec timeout). Utilisez uniquement dans les tests ou les méthodes main—bloquer dans du code async va à l'encontre du but. En production, utilisez les callbacks (onComplete, map) ou les for-comprehensions pour rester non-bloquant.

scala
import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
import ExecutionContext.Implicits.global
val f = Future { Thread.sleep(500); 42 }
// Block and wait (use sparingly—mainly in tests)
val result = Await.result(f, 1.second)
println(result)  // 42
// Await.ready returns Try, Await.result returns value
// Avoid in production servers—use callbacks instead
11

Implicites en profondeur

Paramètres implicites

Les paramètres implicites sont passés automatiquement par le compilateur lorsqu'une valeur implicite du type correspondant est dans la portée. Cela réduit le boilerplate pour les paramètres de 'contexte' (ExecutionContext, logging, configuration). Le compilateur recherche : portée locale, objets compagnons, portée implicite. Vous pouvez toujours passer explicitement pour surcharger. L'abus rend le code difficile à tracer—utilisez pour les dépendances réellement contextuelles.

scala
import scala.concurrent.ExecutionContext

// Method with implicit parameter
def process[A](data: List[A])
    (implicit ec: ExecutionContext): Future[Unit] = {
  Future { data.foreach(println) }
}

// The compiler finds an implicit ExecutionContext in scope
implicit val ec: ExecutionContext = ExecutionContext.global
process(List(1, 2, 3))  // ec passed automatically

// Explicitly providing (overrides implicit)
process(List(1, 2, 3))(myCustomEC)

// Multiple implicit parameters
def log(msg: String)(implicit
    level: Level, logger: Logger): Unit = {
  logger.log(level, msg)
}

Conversions implicites

Les conversions implicites convertissent automatiquement entre les types lorsque nécessaire. implicit class (étendant AnyVal pour zéro overhead) ajoute des méthodes d'extension aux types existants—c'est ainsi que Scala ajoute des méthodes à Int, String, etc. Attention : les conversions implicites peuvent rendre le code confus (qu'est-ce qui est converti ?). Préférez les implicit classes pour les extensions aux implicit defs bruts. Activez avec import scala.language.implicitConversions.

scala
import scala.language.implicitConversions

// Implicit conversion: one type to another
implicit def intToString(n: Int): String = n.toString
val s: String = 42  // intToString(42) called implicitly

// Extension via implicit class (Scala 2.10+)
implicit class RichInt(val self: Int) extends AnyVal {
  def times(f: => Unit): Unit = (1 to self).foreach(_ => f)
  def squared: Int = self * self
}

5.times { println("hi") }  // prints hi 5 times
3.squared  // 9

// Implicit conversion for type compatibility
implicit def javaToScalaList(jl: java.util.List[Int]): List[Int] =
  import scala.jdk.CollectionConverters._
  jl.asScala.toList

val javaList: java.util.List[Int] = ???
val scalaList: List[Int] = javaList  // converted

Priorité de résolution implicite

Le compilateur résout les implicites par priorité : portée locale > objets compagnons > importés > hérités. Si deux implicites du même type sont également dans la portée, vous obtenez une erreur 'ambiguous implicit'. Le pattern de trait LowPriorityImplicits fournit des valeurs par défaut qui peuvent être surchargées par des implicites plus spécifiques. Comprendre l'ordre de résolution est crucial pour la conception de bibliothèques—placez les valeurs par défaut dans des traits de basse priorité pour que les utilisateurs puissent surcharger.

scala
// Priority of implicit resolution (highest to lowest):
// 1. Local implicit (defined in current scope)
implicit val ec1: ExecutionContext = ec1

// 2. Implicit in companion object
object MyService {
  implicit val ec2: ExecutionContext = ec2  // lower priority
}

// 3. Implicit scope (imported)
import somePackage.Implicits._

// 4. Implicit parameter default (inherited trait)
trait DefaultEc {
  implicit val ec: ExecutionContext = ExecutionContext.global
}

// More specific type wins
implicit def ord1: Ordering[Int] = ???
implicit def ord2: Ordering[Int] = ???  // ambiguous error!

// LowPriorityImplicits trait pattern
object MyLib {
  implicit val high: Ordering[Int] = ???
}
object MyLib extends LowPriorityImplicits
trait LowPriorityImplicits {
  implicit val low: Ordering[Int] = ???  // fallback
}

Context bounds et evidence

Les context bounds [A: TypeClass] sont du sucre syntaxique pour les paramètres implicites—ils assertent qu'un TypeClass[A] implicite existe. Utilisez implicitly[TypeClass[A]] (Scala 2) ou summon[TypeClass[A]] (Scala 3) pour le récupérer. Les context bounds rendent les contraintes de type class lisibles : def sort[A: Ordering]. Plusieurs bounds s'empilent : [A: Ordering: Numeric]. C'est la manière idiomatique d'exprimer les exigences de type class.

scala
// Context bound: [A: Ordering] means there's an implicit Ordering[A]
def max[A: Ordering](a: A, b: A): A = {
  val ord = implicitly[Ordering[A]]  // retrieve the implicit
  if (ord.gt(a, b)) a else b
}

// Equivalent to:
def max2[A](a: A, b: A)(implicit ord: Ordering[A]): A =
  if (ord.gt(a, b)) a else b

// summon (Scala 3) instead of implicitly
def max3[A: Ordering](a: A, b: A): A = {
  val ord = summon[Ordering[A]]
  if (ord.gt(a, b)) a else b
}

// Type class evidence
def sort[A: Ordering](list: List[A]): List[A] =
  list.sorted  // uses the implicit Ordering

// Multiple context bounds
def process[A: Ordering: Numeric](x: A, y: A): A = ???

Portée implicite et objets compagnons

La portée implicite est plus large que la portée courante—elle inclut les objets compagnons des types impliqués. C'est pourquoi vous n'avez pas besoin d'importer Ordering[Int] : il vit dans le compagnon de Int. Ce mécanisme rend les type classes ergonomiques : définissez l'instance dans le compagnon du type, et elle est automatiquement disponible. Les objets package contiennent les implicites partagés pour un package. Cette conception permet l'usage de type class 'sans import'.

scala
// Implicit in companion object is found automatically
case class UserId(value: Long)
object UserId {
  implicit val ordering: Ordering[UserId] =
    Ordering.by(_.value)
}

// No import needed—companion object implicits are in scope
List(UserId(3), UserId(1), UserId(2)).sorted
// Works because Ordering[UserId] is in UserId's companion

// Implicit scope includes:
// 1. Companion object of the type (UserId)
// 2. Companion object of the type class (Ordering)
// 3. Companion objects of type parameters

// This is why Int has an Ordering:
// object Int { implicit val ord: Ordering[Int] = ... }

// Package object for shared implicits
package object myapp {
  implicit val ec: ExecutionContext = ExecutionContext.global
  type Id = Long
}
12

Type classes

Définir une type class

Une type class est un trait paramétré par type, avec des instances fournissant du comportement pour des types spécifiques. Contrairement à l'héritage, vous pouvez ajouter des instances de type class rétroactivement (pour des types que vous ne possédez pas). Show[A] définit comment afficher A. Les instances vivent dans l'objet compagnon (portée implicite automatique). C'est du polymorphisme ad hoc—comportement différent par type sans modifier les types. Les type classes sont l'abstraction la plus puissante de Scala.

scala
// Type class: a trait parameterized by type
trait Show[A] {
  def show(a: A): String
}

// Instances for specific types
object Show {
  // Instance for Int
  implicit val showInt: Show[Int] = (a: Int) => a.toString

  // Instance for String
  implicit val showString: Show[String] = (s: String) => s""$s""

  // Instance for List (recursive)
  implicit def showList[A](implicit s: Show[A]): Show[List[A]] =
    (list: List[A]) => list.map(s.show).mkString("[", ", ", "]")
}

// Usage with implicit parameter
def print[A](a: A)(implicit s: Show[A]): Unit =
  println(s.show(a))

print(42)           // 42
print("hello")      // "hello"
print(List(1, 2, 3))  // [1, 2, 3]

Utiliser les type classes (sucre syntaxique)

Les context bounds [A: Show] + summon récupèrent les instances de type class. Les méthodes d'extension (implicit class) ajoutent des méthodes comme .show qui utilisent la type class. Cette combinaison donne une API propre : 42.show fonctionne si Show[Int] existe. La bibliothèque standard fournit de nombreuses type classes : Numeric, Ordering, Eq, Monoid (Cats). Importer la syntaxe (Numeric.Implicits._) ajoute des opérateurs comme + et sum qui utilisent la type class.

scala
// Context bound syntax
def printAll[A: Show](items: List[A]): Unit =
  items.foreach(a => println(summon[Show[A]].show(a)))

// Extension methods via implicit class
implicit class ShowOps[A](val a: A) extends AnyVal {
  def show(implicit s: Show[A]): String = s.show(a)
}

42.show           // "42"
"hi".show         // ""hi""
List(1,2).show    // "[1, 2]"

// Combining: type class + extension methods
def format[A: Show](a: A): String = a.show

// Standard library type classes
def sum[A: Numeric](xs: List[A]): A =
  summon[Numeric[A]].plus(xs.head, xs.tail.foldLeft(
    summon[Numeric[A]].zero)(summon[Numeric[A]].plus))

// Or with syntax:
import Numeric.Implicits._
def sum2[A: Numeric](xs: List[A]): A = xs.sum

Type classes courantes (Cats/Scalaz)

Cats et Scalaz fournissent des type classes standard. Monoid (empty + combine) permet l'agrégation générique. Functor (map) et Monad (pure + flatMap) abstraient les conteneurs (List, Option, Future, IO). Eq fournit une égalité type-safe (pas de comparaisons inter-types accidentelles). Ils composent : un Monad est un Functor, un Monoid est un Semigroup. Les type classes permettent d'écrire du code générique réutilisable qui fonctionne sur de nombreux types.

scala
// Monoid: combine values with empty
trait Monoid[A] {
  def empty: A
  def combine(a: A, b: A): A
}
object Monoid {
  implicit val intAdd: Monoid[Int] = new Monoid[Int] {
    def empty = 0
    def combine(a: Int, b: Int) = a + b
  }
  implicit def listMonoid[A]: Monoid[List[A]] = new Monoid[List[A]] {
    def empty = Nil
    def combine(a: List[A], b: List[A]) = a ++ b
  }
}

// Functor: map over structure
trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

// Monad: chain operations
trait Monad[F[_]] {
  def pure[A](a: A): F[A]
  def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
}

// Eq: type-safe equality
trait Eq[A] {
  def eqv(a: A, b: A): Boolean
}

// Semigroup: combine (no empty)
trait Semigroup[A] {
  def combine(a: A, b: A): A
}

Lois et test des type classes

Les lois de type class sont des propriétés mathématiques que les instances doivent satisfaire. Monoid requiert l'associativité et l'identité. Functor requiert la préservation de l'identité et de la composition. Des bibliothèques comme Cats fournissent des définitions de lois ; discipline + ScalaCheck les testent automatiquement. Les lois expliquent pourquoi les type classes sont puissantes : le code générique (comme foldMap) fonctionne correctement pour toute instance respectant les lois. Vérifiez toujours que vos instances satisfont les lois—les bugs dans les instances cassent tout le code générique les utilisant.

scala
// Type class laws: properties that must hold
// Monoid laws:
// 1. Left identity:  combine(empty, a) == a
// 2. Right identity: combine(a, empty) == a
// 3. Associativity:  combine(a, combine(b, c)) == combine(combine(a, b), c)

// Functor laws:
// 1. Identity:    map(fa)(identity) == fa
// 2. Composition: map(fa)(f andThen g) == map(map(fa)(f))(g)

// Testing laws with ScalaCheck (discipline)
import org.scalacheck.Prop.forAll
import cats.kernel.laws.MonoidLaws

class MonoidSpec extends munit.FunSuite with Discipline {
  checkAll("Int Monoid", MonoidLaws[Int].monoid)
}

// Custom law test
def monoidLeftIdentity[A](implicit m: Monoid[A], arb: Arbitrary[A]) =
  forAll { (a: A) =>
    m.combine(m.empty, a) == a
  }

// Laws make type classes trustworthy:
// if an instance satisfies laws, generic code works correctly

Dérivation de type class (Scala 3)

Scala 3 simplifie la dérivation de type class avec le mot-clé 'derives' et Mirror. Le compilateur peut auto-générer des instances pour les case classes et enums en composant les instances d'éléments. Cela élimine le boilerplate d'écriture d'instances pour chaque case class (courant en Scala 2 avec shapeless). Des bibliothèques comme Cats et Circe supportent la dérivation Scala 3. Le type Mirror donne un accès à la compilation à la structure d'un type (types de champs, labels) pour la programmation générique.

scala
// Scala 3: derive type class instances automatically
import scala.deriving.Mirror

trait Show[A] {
  def show(a: A): String
}

object Show {
  // Inline given for derivation
  given showInt: Show[Int] with
    def show(a: Int) = a.toString

  given showString: Show[String] with
    def show(s: String) = s""$s""

  // Derive for products (case classes)
  given showProduct[A](using m: Mirror.ProductOf[A])
      (using ev: Show[m.MirroredElemTypes]): Show[A] with
    def show(a: A): String = ???

  // Or use Scala 3's derivation
  inline given derive[A](using m: Mirror.Of[A]): Show[A] = ???
}

// Auto-derive for case classes
case class Person(name: String, age: Int) derives Show
// Show[Person] is generated automatically
13

For comprehensions en profondeur

For comprehensions de base

Les for comprehensions sont du sucre syntaxique pour flatMap/map/withFilter. Chaque <- est flatMap (sauf le dernier, qui est map). Les if guards deviennent withFilter. yield fait retourner une collection ; omettre yield le rend impératif (foreach). Cela fonctionne sur tout type avec flatMap/map (Monad) : List, Option, Future, Try, IO. Maîtriser les for-comprehensions est clé pour du Scala idiomatique—ils remplacent les maps/flatMaps imbriqués par une syntaxe séquentielle lisible.

scala
// For comprehension: syntactic sugar for flatMap/map
val result = for {
  x <- List(1, 2, 3)
  y <- List(10, 20)
} yield x + y
// List(11, 21, 12, 22, 13, 23)

// Desugared:
List(1, 2, 3).flatMap { x =>
  List(10, 20).map { y => x + y }
}

// With filters (if guards)
val evens = for {
  x <- 1 to 10
  if x % 2 == 0
} yield x
// Vector(2, 4, 6, 8, 10)

// Desugared:
(1 to 10).withFilter(_ % 2 == 0).map(identity)

// Without yield: imperative (foreach)
for (x <- 1 to 3) println(x)  // 1, 2, 3

For avec Option et Future

Les for-comprehensions fonctionnent sur n'importe quel Monad. Avec Option, ils court-circuitent sur None (retournent None). Avec Future, ils court-circuitent sur échec. Cela fait que le code async/peut-échouer séquentiel se lit comme du code impératif en ligne tout en restant fonctionnel. Chaque ligne <- peut dépendre des liaisons précédentes. C'est bien plus propre que les appels flatMap imbriqués. La même syntaxe fonctionne pour Try, Either, IO et les monades personnalisées.

scala
// Option: chain operations that might return None
def getUser(id: Int): Option[User] = ???
def getEmail(user: User): Option[String] = ???

val email: Option[String] = for {
  user <- getUser(42)
  email <- getEmail(user)
} yield email

// Desugared:
getUser(42).flatMap(user => getEmail(user).map(email => email))

// Future: chain async operations
val result: Future[Int] = for {
  user <- fetchUser(1)      // Future[User]
  posts <- fetchPosts(user) // Future[List[Post]]
} yield posts.size

// If any returns None/failed Future, the whole chain short-circuits
// This is the power of monadic composition

For avec Either et gestion d'erreurs

Either est la gestion d'erreurs typée de Scala. Les for-comprehensions chaînent les Eithers, court-circuitant sur Left (erreur). C'est la gestion fonctionnelle des erreurs—pas d'exceptions, les erreurs sont des valeurs. Le type Left est l'erreur (généralement String ou un sealed trait). Either est right-biased dans Scala 2.12+ (map/flatMap opèrent sur Right). Ce pattern remplace try/catch par une propagation d'erreurs composable et type-safe. Validated de Cats est une alternative pour accumuler les erreurs.

scala
// Either for error handling
def parseAge(s: String): Either[String, Int] =
  s.toIntOption.toRight(s"not a number: $s")

def validateAge(age: Int): Either[String, Int] =
  if (age >= 0) Right(age) else Left(s"negative: $age")

val result: Either[String, Int] = for {
  age <- parseAge("30")     // Right(30)
  valid <- validateAge(age) // Right(30)
} yield valid

// With error in chain
val error: Either[String, Int] = for {
  age <- parseAge("abc")    // Left("not a number: abc")
  valid <- validateAge(age) // skipped!
} yield valid
// error == Left("not a number: abc")

// Scala 3: for-comprehensions work with Either directly
// (Scala 2 needed either.map(_.right) or withFilter)

Désucrage et monades personnalisées

Tout type avec flatMap et map supporte les for-comprehensions—c'est le pattern Monad. Définissez ces méthodes sur votre type pour activer la syntaxe for. Le = (pas <-) crée une liaison locale (pas désucré en flatMap). Comprendre le désucrage aide à déboguer les comprehensions complexes et implémenter des monades personnalisées. Le compilateur traduit les for-comprehensions en chaînes flatMap/map/withFilter. C'est pourquoi for fonctionne uniformément sur List, Option, Future, IO, etc.

scala
// For-comprehensions require flatMap, map, withFilter
// Define your own monad to use for-comprehensions

case class Box[A](value: A) {
  def map[B](f: A => B): Box[B] = Box(f(value))
  def flatMap[B](f: A => Box[B]): Box[B] = f(value)
  def withFilter(p: A => Boolean): Box[A] =
    if (p(value)) this else throw new NoSuchElementException
}

val result = for {
  x <- Box(10)
  y <- Box(20)
  if x < y
} yield x + y
// Box(30)

// Desugaring steps:
// 1. Box(10).flatMap { x =>
// 2.   Box(20).withFilter(_ > x... wait, order matters
// Actual:
// Box(10).flatMap(x =>
//   Box(20).withFilter(y => x < y).map(y => x + y))

// Assignment within for (= instead of <-)
for {
  x <- List(1, 2, 3)
  doubled = x * 2  // local val
} yield doubled

For vs map/flatMap (quand utiliser)

Utilisez les for-comprehensions pour 2+ opérations dépendantes—ils sont plus lisibles que les flatMaps imbriqués. Pour une transformation unique, map est plus clair. Pour aplatir, flatMap directement. Pour les effets de bord (pas de résultat), utilisez for sans yield. Les for-comprehensions brillent lorsque chaque étape dépend de la précédente (chaînage monadique). Ils font que le code async/gestion-d'erreurs se lit séquentiellement. Évitez les fors profondément imbriqués (>5 niveaux)—extrayez des helpers pour la lisibilité.

scala
// For: sequential, dependent operations
val result = for {
  user <- fetchUser(id)
  profile <- fetchProfile(user.id)
  avatar <- fetchAvatar(profile.avatarId)
} yield avatar

// Equivalent with flatMap (harder to read):
fetchUser(id).flatMap(user =>
  fetchProfile(user.id).flatMap(profile =>
    fetchAvatar(profile.avatarId)))

// Map: single transformation (no chaining)
users.map(_.name)  // simple, use map

// FlatMap: when you need to chain but for is overkill
users.flatMap(_.posts)  // List[Post]

// For without yield: side effects
for (user <- users) {
  saveToDb(user)
  sendEmail(user)
}

// Guidelines:
// - 1 operation: use map/flatMap directly
// - 2+ dependent operations: use for-comprehension
// - Side effects: use for without yield
14

Scala 3 : Given et Using

Instances given (remplaçant implicit val)

Scala 3 remplace implicit val/def par 'given'. Les givens sont plus clairs et plus explicites. Les givens anonymes (given Type = ...) ont des noms générés par le compilateur. 'given T with' définit des instances avec plusieurs méthodes. Les givens conditionnels (given [A: Ordering]: Ordering[List[A]]) remplacent les implicit defs. Les givens dans les objets compagnons sont dans la portée implicite automatiquement, comme en Scala 2. Le changement de mot-clé réduit la surcharge d''implicit' (qui signifiait 4 choses en Scala 2).

scala
// Scala 2: implicit val
// implicit val ec: ExecutionContext = ExecutionContext.global

// Scala 3: given
given ec: ExecutionContext = ExecutionContext.global

// Anonymous given (inferred name)
given ExecutionContext = ExecutionContext.global

// Given with 'with' for complex types
given Show[Int] with
  def show(a: Int): String = a.toString

// Given in companion object (automatic scope)
object MyType:
  given Ordering[MyType] = Ordering.by(_.id)

// Conditional given (like implicit def)
given [A: Ordering]: Ordering[List[A]] with
  def compare(a: List[A], b: List[A]): Int =
    a.zip(b).find((x, y) => x != y) match
      case Some((x, y)) => summon[Ordering[A]].compare(x, y)
      case None => a.length - b.length

Clauses using (remplaçant les params implicites)

Scala 3 remplace les paramètres implicites par 'using'. Cela sépare les deux significations d''implicit' (paramètres vs conversions). Les clauses using sont passées explicitement avec le mot-clé 'using'. Les context bounds [A: T] restent les mêmes. summon[T] remplace implicitly[T] (nom plus clair). Le mot-clé using rend évident aux sites d'appel quand vous fournissez un contexte. C'est un changement purement syntaxique—la sémantique est la même que les paramètres implicites.

scala
// Scala 2: implicit parameter
// def process[A](data: List[A])(implicit ec: ExecutionContext): Unit

// Scala 3: using
def process[A](data: List[A])(using ec: ExecutionContext): Unit =
  data.foreach(println)

// Context bound (unchanged)
def sort[A: Ordering](list: List[A]): List[A] = list.sorted

// Multiple using clauses
def log(msg: String)(using level: Level, logger: Logger): Unit =
  logger.log(level, msg)

// summon replaces implicitly
def max[A: Ordering](a: A, b: A): A =
  val ord = summon[Ordering[A]]
  if ord.gt(a, b) then a else b

// Provide explicitly with 'using'
process(data)(using myEC)
log("hello")(using Level.INFO, myLogger)

Méthodes d'extension (Scala 3)

Scala 3 remplace les implicit classes par 'extension'—plus clair et plus ciblé. extension (self: T) définit des méthodes sur T. Les extensions peuvent être génériques (extension [A]). Combinées avec using, elles fournissent la syntaxe de type class (42.show). Les extensions sont juste des ajouts de méthodes—elles ne créent pas d'objets wrapper (zéro overhead avec AnyVal en Scala 2, natif en Scala 3). C'est la manière idiomatique d'ajouter des méthodes aux types existants en Scala 3.

scala
// Scala 2: implicit class
// implicit class RichInt(val self: Int) extends AnyVal {
//   def squared: Int = self * self
// }

// Scala 3: extension
extension (self: Int)
  def squared: Int = self * self
  def times(f: => Unit): Unit = (1 to self).foreach(_ => f)

5.squared   // 25
3.times { println("hi") }

// Extension on generic type
extension [A](self: List[A])
  def takeWhileInclusive(p: A => Boolean): List[A] = ???

// Extension with using (type class syntax)
extension [A](self: A) def show(using s: Show[A]): String = s.show(self)

42.show  // uses given Show[Int]

// Multiple extensions in one block
extension (s: String)
  def isBlank: Boolean = s.trim.isEmpty
  def words: List[String] = s.split(" ").toList

Enums et ADT (Scala 3)

Les enums de Scala 3 remplacent sealed trait + case objects pour les ADT. Ils sont plus concis et supportent les paramètres, champs et méthodes. Les cas d'enum peuvent avoir des paramètres (comme les case classes). Le pattern matching est vérifié pour l'exhaustivité. Les enums peuvent être génériques (Option[A]). Cela unifie les enums et ADT en un seul constructe. Pour les hiérarchies ouvertes (extensibles), utilisez toujours sealed trait + case classes. Pour les énumérations/ADT fermés, enum est plus propre.

scala
// Scala 3 enum (replaces sealed trait + case objects)
enum Color:
  case Red, Green, Blue

enum HttpStatus(val code: Int):
  case Ok extends HttpStatus(200)
  case NotFound extends HttpStatus(404)
  case Error extends HttpStatus(500)

// Pattern matching (exhaustive)
def describe(c: Color): String = c match
  case Color.Red => "red"
  case Color.Green => "green"
  case Color.Blue => "blue"

// Access fields
HttpStatus.Ok.code  // 200

// Parameterized enum cases
enum Option[+A]:
  case Some(value: A)
  case None

// ADT with methods
enum Tree[+A]:
  case Leaf(value: A)
  case Node(left: Tree[A], right: Tree[A])

  def size: Int = this match
    case Leaf(_) => 1
    case Node(l, r) => l.size + r.size + 1

Définitions top-level et indentation

Scala 3 permet les définitions top-level—pas besoin d'envelopper tout dans un object. Cela simplifie la structure de fichier (comme Python/Go). La nouvelle syntaxe supporte les accolades et l'indentation significative (optionnel). if/then remplace if/else avec parenthèses. match peut être utilisé comme expression sans accolades. Ces changements rendent Scala 3 plus abordable tout en gardant la rétrocompatibilité. Vous pouvez mixer les styles—utilisez les accolades où elles ajoutent de la clarté, l'indentation où elle réduit le bruit.

scala
// Scala 3: top-level definitions (no class wrapper needed)
// File: MyMath.scala
def add(a: Int, b: Int): Int = a + b  // top-level function

val Pi: Double = 3.14159  // top-level val

type Id = Long  // top-level type alias

given Show[Int] = (a: Int) => a.toString  // top-level given

extension (i: Int) def squared: Int = i * i  // top-level extension

// Indentation-based syntax (optional, braces still work)
def factorial(n: Int): Int =
  if n <= 1 then 1
  else n * factorial(n - 1)

// Or with braces:
def factorial2(n: Int): Int = {
  if (n <= 1) 1
  else n * factorial2(n - 1)
}

// if/then, match/case without braces
val sign = if x > 0 then 1 else -1
val desc = x match
  case 0 => "zero"
  case _ => "nonzero"
15

Méthodes d'extension et syntaxe

Implicit classes (Scala 2)

implicit class (Scala 2.10+) ajoute des méthodes d'extension aux types existants. Étendre AnyVal le rend sans allocation (le compilateur efface le wrapper). La classe doit prendre un seul paramètre de constructeur (le type étendu). Les méthodes sur la implicit class deviennent disponibles sur le type étendu. C'est ainsi que Scala enrichit Int, String, etc. Placez les implicit classes dans un objet package ou un objet utilitaire pour le partage. Dans Scala 3, utilisez 'extension' à la place.

scala
// Scala 2: implicit class for extension methods
import scala.language.implicitConversions

implicit class RichString(val s: String) extends AnyVal {
  def wordCount: Int = s.split("\\s+").length
  def slug: String = s.toLowerCase.replaceAll("[^a-z0-9]+", "-")
  def encrypt(key: Int): String = s.map(c => (c + key).toChar)
}

"Hello World".wordCount  // 2
"My Blog Post!".slug     // "my-blog-post-"
"abc".encrypt(1)         // "bcd"

// AnyVal avoids allocation (zero overhead)
// implicit class must be in a trait, class, or object

// For generic extensions
implicit class RichList[A](val list: List[A]) extends AnyVal {
  def middle: Option[A] = list.lift(list.length / 2)
}

List(1, 2, 3, 4, 5).middle  // Some(3)

Méthodes d'extension en pratique

Les méthodes d'extension sont la manière standard d'ajouter des utilitaires aux collections et autres types. distinctBy, chunked, tap sont des ajouts courants. Les opérateurs === et =!= (via la type class Eq) fournissent une égalité type-safe (contrairement à == qui autorise inter-types). Groupez les extensions dans un objet et importez où nécessaire. Cela garde les types centraux propres tout en permettant des méthodes spécifiques au domaine. La bibliothèque de collections de Scala utilise elle-même ce pattern massivement.

scala
// Common pattern: add utility methods to collections
object CollectionExtensions {
  implicit class RichSeq[A](val seq: Seq[A]) extends AnyVal {
    def chunked(size: Int): Seq[Seq[A]] =
      seq.grouped(size).toSeq

    def distinctBy[B](f: A => B): Seq[A] =
      seq.groupBy(f).values.map(_.head).toSeq

    def tap(f: A => Unit): Seq[A] = {
      seq.foreach(f)
      seq
    }
  }
}

import CollectionExtensions._

List(1, 2, 3, 4, 5).chunked(2)  // List(List(1,2), List(3,4), List(5))
List("aa", "bb", "ab").distinctBy(_.head)  // List("aa", "ab")
List(1, 2, 3).tap(println)  // prints 1,2,3, returns List(1,2,3)

// Type class syntax via extension
implicit class EqOps[A](val a: A) extends AnyVal {
  def ===(b: A)(implicit eq: Eq[A]): Boolean = eq.eqv(a, b)
  def =!=(b: A)(implicit eq: Eq[A]): Boolean = !eq.eqv(a, b)
}

Conversions implicites (à utiliser avec précaution)

Les conversions implicites convertissent automatiquement les types, mais elles sont dangereuses—le comportement du code devient non évident. Scala 2.10+ nécessite une opt-in explicite (scala.language.implicitConversions). Préférez les méthodes d'extension (qui ajoutent des méthodes sans changer les types) aux conversions (qui changent les types). Usages légitimes : interop Java (conversion entre types de collection), construction de DSL. Le compilateur avertit des conversions implicites—traitez ces avertissements sérieusement. Dans Scala 3, given Conversion[T, U] est le mécanisme explicite.

scala
import scala.language.implicitConversions

// Implicit conversion between types
implicit def stringToInt(s: String): Int = s.toInt
val n: Int = "42"  // stringToInt("42")

// Dangerous: can cause surprising behavior
implicit def intToBoolean(n: Int): Boolean = n != 0
if (1) println("yes")  // works! (intToBoolean(1))

// Safer: use extension methods instead
extension (n: Int) def toBool: Boolean = n != 0
if (1.toBool) println("yes")

// When implicit conversions are appropriate:
// 1. Java interop (java.util.List <-> scala.List)
implicit def javaListToScala[A](jl: java.util.List[A]): List[A] =
  import scala.jdk.CollectionConverters._
  jl.asScala.toList

// 2. Backward compatibility layers
// 3. DSL construction (use sparingly)

// Enable per-file: import scala.language.implicitConversions

Programmation au niveau des types

La programmation au niveau des types encode des informations dans les types que le compilateur vérifie. Les types fantômes (paramètres de type inutilisés) suivent l'état (Open/Closed) empêchant l'usage impropre (ne peut pas lire un fichier fermé). Les naturels de Peano représentent les nombres comme des types. Cela permet des garanties de correction à la compilation—les bugs deviennent des erreurs de compilation. Utilisé dans les machines à états, unités de mesure, vecteurs dimensionnés. Puissant mais complexe—utilisez-le lorsque la sécurité vaut la complexité des types. Des bibliothèques comme shapeless (Scala 2) permettent la programmation avancée au niveau des types.

scala
// Phantom types: track state at type level
sealed trait State
trait Open extends State
trait Closed extends State

class File[S <: State] private (val path: String)

object File {
  def open(path: String): File[Open] = new File[Open](path)
}

def close[S <: Open](f: File[S]): File[Closed] =
  new File[Closed](f.path)  // unsafe cast internally

def read[S <: Open](f: File[S]): String = "data"
// def read[S <: Closed](f: File[S]): String  // compile error

val f = File.open("test.txt")
read(f)  // OK: f is File[Open]
val closed = close(f)
// read(closed)  // COMPILE ERROR: closed is File[Closed]

// Type-level natural numbers (Peano)
sealed trait Nat
trait Zero extends Nat
trait Succ[N <: Nat] extends Nat

// Compile-time list length check
type _0 = Zero
type _1 = Succ[_0]
type _2 = Succ[_1]

Types opaques (Scala 3)

Les types opaques (Scala 3) créent des newtypes sans overhead—contrairement aux value classes (AnyVal), ils ne boxent jamais et sont véritablement juste le type sous-jacent à l'exécution. La distinction de type n'existe qu'à la compilation, empêchant les confusions (Celsius vs Fahrenheit, UserId vs Long). À l'intérieur de l'objet définissant, le type et son type sous-jacent sont interchangeables ; à l'extérieur, ils sont distincts. C'est le pattern 'newtype' de Haskell—des types pilotés par le domaine sans coût à l'exécution. Utilisez pour les IDs, unités et primitives de domaine.

scala
// Scala 2: value classes for zero-overhead wrappers
// case class UserId(value: Long) extends AnyVal

// Scala 3: opaque types (true zero overhead, no boxing)
object Types:
  type UserId = Long
  object UserId:
    def apply(value: Long): UserId = value

  type Email = String
  object Email:
    def apply(s: String): Email =
      require(s.contains("@"), "invalid email")
      s

import Types.*
val id: UserId = UserId(42)  // just a Long at runtime
val email: Email = Email("[email protected]")

// UserId and Long are NOT interchangeable outside the object
// def wrong(x: Long): UserId = x  // ERROR
// But inside the object, they're the same

// Newtype pattern: domain types without overhead
type Celsius = Double
type Fahrenheit = Double
// Prevents mixing up Celsius and Fahrenheit
def toF(c: Celsius): Fahrenheit = c * 9 / 5 + 32
16

Tests (ScalaTest et ScalaCheck)

Styles ScalaTest

ScalaTest offre plusieurs styles. FunSuite est le plus simple (test("name") { ... }). FlatSpec est style BDD ("A Stack" should "..."). Matchers fournit des assertions lisibles (shouldBe, should contain, should throw). Choisissez un style par projet pour la cohérence. FunSuite est populaire pour les tests unitaires ; FlatSpec pour les tests axés comportement. Tous les styles supportent les mêmes matchers et hooks de cycle de vie. Le style affecte la syntaxe, pas les capacités.

scala
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

// FunSuite: simple test functions
class MyTest extends AnyFunSuite with Matchers {
  test("addition works") {
    1 + 1 should be (2)
    1 + 1 shouldBe 2
    List(1, 2, 3) should contain (2)
  }

  test("string operations") {
    "hello".length shouldBe 5
    "hello" should startWith ("he")
  }
}

// FlatSpec: BDD-style
import org.scalatest.flatspec.AnyFlatSpec
class StackSpec extends AnyFlatSpec with Matchers {
  "A Stack" should "pop values in LIFO order" in {
    val stack = Stack(1, 2, 3)
    stack.pop() shouldBe 3
  }

  it should "throw on empty pop" in {
    val stack = Stack()
    a [NoSuchElementException] shouldBe thrownBy(stack.pop())
  }
}

// WordSpec, FreeSpec, PropSpec also available

Assertions et Matchers

Les Matchers fournissent des assertions expressives. shouldBe/should be pour l'égalité. contain, have size, have key pour les collections. startWith/endWith/include pour les chaînes. thrownBy pour les exceptions. Matchers personnalisés (be >, be <=) pour les comparaisons. Le DSL 'should' se lit comme l'anglais, rendant les tests auto-documentés. Pour les assertions complexes, utilisez des matchers personnalisés ou un simple assert(condition). Évitez de trop chaîner les matchers—lisibilité plutôt que clarté.

scala
import org.scalatest.matchers.should.Matchers._

// Equality
result shouldBe 42
result should be (42)
result should equal (42)

// Collections
list should contain (5)
list should not contain (0)
list shouldBe empty
list should have size 5
list should contain inOrder (1, 2, 3)
map should contain key ("name")
map should contain value ("Alice")

// Strings
s should startWith ("Hello")
s should endWith ("world")
s should include ("lo")
s should fullyMatch regex "H.*d".r

// Exceptions
a [IOException] should be thrownBy riskyOp()
the [IOException] thrownBy riskyOp() should have message "fail"

// Custom matchers
result should be > 0
result should be <= 100

// Type checks
result shouldBe a [List[_]]
result should be an [IllegalArgumentException]

BeforeAndAfter et Fixtures

BeforeAndAfterEach exécute setup/teardown autour de chaque test. Surchargez beforeEach/afterEach. Pour la gestion des ressources, le pattern loan (withDb { conn => ... }) est plus propre—il garantit le nettoyage via try/finally et rend la ressource explicite. ScalaTest supporte aussi les contextes de fixture (FixtureContext) et les fixtures partagés via traits. Préférez le pattern loan ou les méthodes de fixture à l'état mutable beforeEach—c'est plus fonctionnel et évite les bugs d'état partagé entre les tests.

scala
import org.scalatest.BeforeAndAfterEach
import org.scalatest.funsuite.AnyFunSuite

class DbTest extends AnyFunSuite with BeforeAndAfterEach {
  var conn: Connection = _

  override def beforeEach(): Unit = {
    conn = DriverManager.getConnection("jdbc:h2:mem:test")
    conn.execute("CREATE TABLE users (id INT, name VARCHAR)")
  }

  override def afterEach(): Unit = {
    conn.close()
  }

  test("insert works") {
    conn.execute("INSERT INTO users VALUES (1, 'Alice')")
    val count = conn.query("SELECT COUNT(*) FROM users")
    count shouldBe 1
  }
}

// Fixture via loan pattern
class FixtureTest extends AnyFunSuite {
  def withDb(test: Connection => Unit): Unit = {
    val conn = DriverManager.getConnection("jdbc:h2:mem:test")
    try test(conn) finally conn.close()
  }

  test("query works") {
    withDb { conn =>
      conn.execute("INSERT ...")
      // assertions
    }
  }
}

Tests basés sur propriétés (ScalaCheck)

ScalaCheck génère des entrées de test aléatoires, testant des propriétés sur de nombreux cas. forAll exécute la propriété avec 100 entrées aléatoires par défaut. whenever filtre les entrées. Les types Gen personnalisés contraignent les données générées (Gen.choose, Gen.nonEmptyListOf). Cela détecte les cas limites que vous manqueriez avec des tests basés sur exemples (listes vides, nombres négatifs, grandes valeurs). Les tests table-driven (Table) sont pour des cas spécifiques. Les tests basés sur propriétés sont puissants pour les fonctions pures et transformations de données—définissez ce qui devrait toujours être vrai.

scala
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import org.scalacheck.Prop.forAll

class ListSpec extends AnyFunSuite with ScalaCheckPropertyChecks {
  // Property: reversing twice = identity
  test("reverse twice is identity") {
    forAll { (xs: List[Int]) =>
      xs.reverse.reverse shouldBe xs
    }
  }

  // Property with conditions
  test("head of sorted list is min") {
    forAll { (xs: List[Int]) =>
      whenever(xs.nonEmpty) {
        xs.sorted.head shouldBe xs.min
      }
    }
  }

  // Custom generators
  import org.scalacheck.Gen
  val smallInt = Gen.choose(1, 100)
  val nonEmptyList = Gen.nonEmptyListOf(smallInt)

  test("custom generator") {
    forAll(nonEmptyList) { (xs: List[Int]) =>
      xs should not be empty
      xs.forall(_ >= 1) shouldBe true
    }
  }

  // Table-driven tests
  test("addition table") {
    val cases = Table(
      ("a", "b", "sum"),
      (1, 2, 3),
      (10, 20, 30),
      (-1, 1, 0)
    )
    forAll(cases) { (a, b, sum) =>
      a + b shouldBe sum
    }
  }
}

Mocking et doubles de test

Mockito (via ScalaTestPlus) crée des doubles de test. mock[T] crée un mock ; when(...).thenReturn(...) stubbe les méthodes ; verify vérifie les interactions. Utilisez les mocks pour isoler l'unité testée des dépendances (bases de données, APIs). Les argument matchers (argThat) vérifient des arguments spécifiques. Ne mockez pas trop—si vous mockez tout, testez plutôt la vraie intégration. Préférez les fakes (implémentations en mémoire) aux mocks pour les dépendances complexes—ils sont plus robustes et lisibles.

scala
import org.scalatest.funsuite.AnyFunSuite
import org.scalatestplus.mockito.MockitoSugar
import org.mockito.Mockito._

class UserServiceSpec extends AnyFunSuite with MockitoSugar {
  test("getUser returns user from repo") {
    // Create mock
    val repo = mock[UserRepository]
    val user = User(1, "Alice")

    // Stub: when X then Y
    when(repo.findById(1)).thenReturn(Some(user))

    val service = new UserService(repo)
    val result = service.getUser(1)

    result shouldBe Some(user)

    // Verify: was X called?
    verify(repo).findById(1)
    verify(repo, never()).findById(2)
  }

  test("createUser saves to repo") {
    val repo = mock[UserRepository]
    val service = new UserService(repo)

    service.createUser("Bob")

    // Verify with argument matcher
    verify(repo).save(argThat((u: User) => u.name == "Bob"))
  }
}

// Stubbing exceptions
when(repo.findById(99)).thenThrow(new RuntimeException("not found"))
17

Fonctions d'ordre supérieur et FP

Fonctions comme valeurs

Les fonctions sont des valeurs dans Scala—vous pouvez les stocker, passer et retourner. (A, B) => C est le type fonction. L'eta-expansion (multiply _) convertit les méthodes en valeurs de fonction. Les fonctions d'ordre supérieur (prenant/retournant des fonctions) permettent des abstractions puissantes : map, filter, reduce sont des HOFs. La composition de fonctions (andThen, compose) construit des pipelines. Le currying (adder(5) retourne une fonction) application partielle. C'est la fondation de la programmation fonctionnelle dans Scala.

scala
// Functions are first-class values
val add: (Int, Int) => Int = (a, b) => a + b
val square: Int => Int = x => x * x

// Apply
add(2, 3)  // 5
square(4)  // 16

// Method to function (eta-expansion)
def multiply(a: Int, b: Int): Int = a * b
val mul = multiply _  // or just 'multiply' in Scala 3
mul(3, 4)  // 12

// Higher-order: function taking function
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(square, 2)  // 16

// Higher-order: function returning function
def adder(n: Int): Int => Int = _ + n
val add5 = adder(5)
add5(10)  // 15

// Function composition
val f: Int => Int = _ + 1
val g: Int => Int = _ * 2
val h = f andThen g  // f then g: (x+1)*2
val h2 = f compose g  // g then f: (x*2)+1
h(3)  // 8
h2(3)  // 7

Currying et application partielle

Le currying sépare une fonction en plusieurs listes de paramètres, permettant l'application partielle (fixer certains args, obtenir une fonction pour le reste). Cela aide l'inférence de type (les params antérieurs contraignent les ultérieurs) et crée des fonctions configurables (withDb(config)). Le placeholder _ applique partiellement : sum(1, _) crée une fonction. Les listes de paramètres multiples sont le mécanisme de currying de Scala. Utilisez le currying lorsque certains args sont de la 'configuration' et d'autres des 'données'.

scala
// Curried function: multiple parameter lists
def add(a: Int)(b: Int): Int = a + b
val add5: Int => Int = add(5)  // partial application
add5(10)  // 15

// Multiple parameter lists for type inference
def map[A, B](list: List[A])(f: A => B): List[B] =
  list.map(f)
map(List(1, 2, 3))(x => x * 2)  // A, B inferred from first list

// Curried form (Function types)
val curriedAdd: Int => Int => Int = a => b => a + b
curriedAdd(5)(10)  // 15

// Uncurrying
def uncurriedAdd(a: Int, b: Int): Int = a + b

// Practical: configuration via currying
def withDb(config: DbConfig)(f: Connection => Result): Result = ???
val withMyDb = withDb(myConfig) _
withMyDb { conn => /* ... */ }

// Partial application with placeholder
val sum: (Int, Int) => Int = _ + _
val addOne: Int => Int = sum(1, _)

Fonctions pures et transparence référentielle

Les fonctions pures retournent toujours la même sortie pour la même entrée et n'ont pas d'effets de bord. Elles sont faciles à tester, raisonner, paralléliser et composer. La transparence référentielle signifie que vous pouvez remplacer un appel de fonction par son résultat sans changer le comportement. Scala n'impose pas la pureté, mais des bibliothèques comme Cats Effect (monade IO) vous permettent d'isoler les effets de bord. Cœur pur + IO aux bords est une architecture FP courante : la logique métier est pure, les I/O sont enveloppés dans IO.

scala
// Pure function: same input → same output, no side effects
def pureAdd(a: Int, b: Int): Int = a + b  // pure

// Impure: depends on external state
var counter = 0
def impureAdd(a: Int): Int = { counter += 1; a + counter }  // impure

// Impure: side effect
def impurePrint(a: Int): Int = { println(a); a }  // side effect

// Referential transparency: can replace call with result
val x = pureAdd(2, 3)
val y = x + x  // same as pureAdd(2, 3) + pureAdd(2, 3)

// Benefits of purity:
// 1. Easy to test (no setup/teardown)
// 2. Easy to reason about (no hidden state)
// 3. Parallelizable (no shared state)
// 4. Memoizable (cache results)
// 5. Composable (predictable)

// IO monad for side effects (Cats Effect)
import cats.effect.IO
val program: IO[Unit] = IO.println("hello")
val mapped: IO[String] = IO.pure("world").map(_.toUpperCase)
// Side effects captured in IO, run at edge of program

Données immuables et collections persistantes

Les structures de données immuables retournent de nouvelles copies lors de modification, partageant la structure interne pour l'efficacité (structures de données persistantes). Les collections de Scala sont immuables par défaut. case class copy crée des copies modifiées. Pour les mises à jour profondes, les bibliothèques Lens (Monocle) fournissent des accesseurs composables. L'immutabilité élimine des classes entières de bugs (race conditions, mutations inattendues) et rend le code plus facile à raisonner. Le coût de performance est souvent acceptable grâce au partage structurel (O(log n) pas O(n)).

scala
// Immutable: operations return new collections
val list1 = List(1, 2, 3)
val list2 = list1 :+ 4  // List(1, 2, 3, 4)
// list1 is unchanged: List(1, 2, 3)

// Persistent data structures: share structure (efficient)
val map1 = Map("a" -> 1, "b" -> 2)
val map2 = map1 + ("c" -> 3)  // shares structure with map1
// O(log n) due to structural sharing, not O(n) copy

// Case classes: copy for modification
case class User(name: String, age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31)  // new instance, alice unchanged

// Lens (Monocle library) for nested updates
import monocle.macros.GenLens
val ageLens = GenLens[User](_.age)
val updated = ageLens.modify(_ + 1)(alice)  // User("Alice", 31)

// Benefits:
// - No bugs from shared mutation
// - Easy to reason about (values don't change)
// - Free concurrency (no locks needed)
// - Undo/redo trivial (keep old versions)

Récursion et appels terminaux

La récursion terminale (où l'appel récursif est la dernière opération) est optimisée par le compilateur Scala en une boucle—pas de croissance de pile. @tailrec vérifie cela à la compilation. Le pattern accumulateur (passage du résultat accumulé) convertit la récursion non-terminale en récursion terminale. foldLeft/foldRight encapsulent les patterns de récursion courants. Pour la récursion non-terminale profonde, utilisez les trampolines (Cats) ou réécrivez avec des folds. Préférez les folds à la récursion explicite pour la clarté—ils sont tail-recursive et idiomatiques.

scala
// Regular recursion (can stack overflow)
def factorial(n: Int): Int =
  if (n <= 1) 1 else n * factorial(n - 1)
factorial(10000)  // StackOverflowError!

// Tail recursion: compiler optimizes to loop
import scala.annotation.tailrec
@tailrec
def factorialTail(n: Int, acc: Int = 1): Int =
  if (n <= 1) acc else factorialTail(n - 1, n * acc)
factorialTail(10000)  // works (no stack overflow)

// @tailrec annotation: compiler verifies it's tail-recursive
// Error if not actually tail-recursive

// Trampoline for non-tail recursion
// (Cats' Trampoline or Free monads)
// Converts stack recursion to heap

// Fold as alternative to recursion
def sum(list: List[Int]): Int = list.foldLeft(0)(_ + _)
// foldLeft is tail-recursive internally

// Pattern: accumulator pattern
@tailrec
def reverse[A](list: List[A], acc: List[A] = Nil): List[A] =
  list match
    case Nil => acc
    case head :: tail => reverse(tail, head :: acc)
18

Case classes et ADT

Bases des case classes

Les case classes sont l'outil principal de modélisation de données de Scala. Le compilateur génère equals, hashCode, toString, copy, apply, unapply (pour le pattern matching) et les méthodes accesseurs. Elles sont immuables par défaut (champs val). Utilisez les case classes pour les DTOs, value objects, messages et entités de domaine (lorsqu'immuables). L'apply de l'objet compagnon permet de construire sans 'new'. copy permet les mises à jour non-destructives. Les case classes sont la fondation des ADT (Algebraic Data Types).

scala
// Case class: immutable data holder with boilerplate generated
case class Person(name: String, age: Int)

val alice = Person("Alice", 30)
val bob = Person("Bob", 25)

// Auto-generated methods:
alice.toString  // "Person(Alice,30)"
alice == Person("Alice", 30)  // true (structural equality)
alice.hashCode  // based on fields

// Pattern matching
alice match
  case Person(name, age) => s"$name is $age"

// Copy with modifications
val older = alice.copy(age = 31)  // Person("Alice", 31)

// Companion object with apply (no 'new' needed)
val p = Person("Charlie", 40)  // Person.apply called

// Fields accessed directly
alice.name  // "Alice"
alice.age   // 30

// Case class with default values
case class Point(x: Double = 0, y: Double = 0)
Point()  // Point(0.0, 0.0)
Point(y = 5)  // Point(0.0, 5.0)

Sealed traits et ADT

Les ADT (Algebraic Data Types) modélisent les données comme un ensemble fermé de cas (sealed trait + case classes/objects). 'sealed' signifie que tous les sous-types sont dans le même fichier, permettant le pattern matching exhaustif—le compilateur avertit si vous manquez un cas. Ajouter un nouveau cas montre tous les endroits nécessitant des mises à jour (refactoring sûr). Les case objects sont des singletons (pas de params). Option, List, Either sont tous des ADT. C'est la manière fonctionnelle de modéliser les domaines—chaque variante est un cas, le comportement est dans le pattern matching.

scala
// ADT: sealed trait + case classes/objects
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(a: Double, b: Double, c: Double) extends Shape

// Exhaustive pattern matching (compiler checks all cases)
def area(shape: Shape): Double = shape match
  case Circle(r) => math.Pi * r * r
  case Rectangle(w, h) => w * h
  case Triangle(a, b, c) =>
    val s = (a + b + c) / 2
    math.sqrt(s * (s-a) * (s-b) * (s-c))

// Adding a new case: compiler warns about non-exhaustive matches
case class Square(side: Double) extends Shape
// area now needs a Square case!

// Case objects for singletons (no parameters)
sealed trait Status
case object Active extends Status
case object Inactive extends Status
case object Pending extends Status

// Option and List are ADTs:
// sealed trait Option[+A]
// case class Some[A](value: A) extends Option[A]
// case object None extends Option[Nothing]

Fonctionnalités des case classes

Les case classes peuvent avoir des méthodes, un apply personnalisé (pour validation), implémenter des traits et être génériques. Un apply personnalisé dans l'objet compagnon peut valider avant construction (rendre les états illégaux non représentables). Les case classes peuvent implémenter des traits pour le polymorphisme. Le constructeur privé (case class Email private) force l'utilisation de l'apply du compagnon pour la validation. Cela combine la modélisation de données avec l'encapsulation—des constructeurs qui ne peuvent pas échouer silencieusement.

scala
// Case class with methods
case class Vec2(x: Double, y: Double) {
  def +(other: Vec2): Vec2 = Vec2(x + other.x, y + other.y)
  def magnitude: Double = math.sqrt(x*x + y*y)
  def dot(other: Vec2): Double = x*other.x + y*other.y
}

Vec2(1, 2) + Vec2(3, 4)  // Vec2(4.0, 6.0)
Vec2(3, 4).magnitude  // 5.0

// Case class with custom apply (validation)
case class Email private (value: String)
object Email {
  def apply(value: String): Email = {
    require(value.contains("@"), "invalid email")
    new Email(value)  // bypass public apply
  }
}
// Email("notanemail")  // IllegalArgumentException
Email("[email protected]")  // works

// Case class implementing trait
trait Jsonable {
  def toJson: String
}
case class User(name: String, age: Int) extends Jsonable {
  def toJson: String = s"""{"name":"$name","age":$age}"""
}

// Case class with type parameters
case class Box[A](value: A) {
  def map[B](f: A => B): Box[B] = Box(f(value))
}

Pattern matching en profondeur

Le pattern matching est l'outil puissant de déstructuration de Scala. Il supporte : les patterns littéraux (0, "hello"), patterns de type (s: String), patterns de case class (User(name, age)), gardes (if condition), patterns imbriqués et la liaison (@). Le compilateur vérifie l'exhaustivité pour les types sealed. Les patterns sont essayés de haut en bas. Utilisez @ pour lier toute la valeur tout en extrayant des parties. Le pattern matching remplace les chaînes if-else, vérifications de type et déstructuration par une syntaxe unifiée et lisible.

scala
// Various pattern types
val x: Any = (1, "hello")

x match
  case (a: Int, b: String) => s"int $a, string $b"
  case (a, b) => s"pair: $a, $b"
  case _ => "other"

// Guards
def classify(n: Int): String = n match
  case n if n < 0 => "negative"
  case 0 => "zero"
  case n if n % 2 == 0 => "even positive"
  case _ => "odd positive"

// Case class patterns (nested)
case class Point(x: Int, y: Int)
case class Shape(center: Point, size: Int)

def isOrigin(s: Shape): Boolean = s match
  case Shape(Point(0, 0), _) => true
  case _ => false

// Named patterns (bind the whole while extracting)
case class User(name: String, age: Int)
def describe(u: User): String = u match
  case u @ User(name, age) if age < 18 => s"$name is a minor"
  case User(name, _) => s"$name is an adult"

// Type patterns
def handle(x: Any): String = x match
  case s: String => s"string: $s"
  case n: Int => s"int: $n"
  case list: List[_] => s"list of size ${list.size}"
  case None => "none"
  case _ => "unknown"

Extracteurs et patterns personnalisés

Les extracteurs (unapply) permettent le pattern matching sur n'importe quel type, pas seulement les case classes. Cela découple le pattern de matching de la représentation des données. Les regex sont des extracteurs (les groupes deviennent des liaisons). Vous pouvez écrire des extracteurs pour des types externes (JSON, URLs) sans les modifier. unapply retourne Option[(T1, T2, ...)] pour l'extraction, ou Boolean pour la correspondance simple. Les extracteurs rendent le pattern matching extensible à n'importe quelle source de données. Les case classes auto-génèrent unapply ; les extracteurs personnalisés ajoutent la correspondance pour les types non-case-class.

scala
// Custom extractor via unapply
object Email {
  def unapply(str: String): Option[(String, String)] = {
    val parts = str.split("@")
    if (parts.length == 2) Some((parts(0), parts(1)))
    else None
  }
}

// Use in pattern matching
"[email protected]" match
  case Email(user, domain) => s"user: $user, domain: $domain"
  case _ => "not an email"

// Boolean extractor (no extracted values)
object Even {
  def unapply(n: Int): Boolean = n % 2 == 0
}

5 match
  case Even() => "even"
  case _ => "odd"

// Extractor with variable arity
object Pair {
  def unapply[A, B](t: (A, B)): Option[(A, B)] = Some(t._1, t._2)
}

// Regex as extractor
val Date = "(\\d{4})-(\\d{2})-(\\d{2})".r
"2024-01-15" match
  case Date(year, month, day) => s"$year/$month/$day"
  case _ => "not a date"

// Practical: parse without case classes
object Json {
  def unapply(s: String): Option[Any] =
    scala.util.Try(ujson.read(s)).toOption
}
19

Collections en profondeur

Collections immuables

Les collections Scala sont immuables par défaut. List est une liste chaînée (O(n) accès aléatoire). Vector est un arbre avec accès O(log n). Map et Set sont basés sur le hash. Les opérations retournent de nouvelles collections, partageant la structure pour l'efficacité.

scala
val list = List(1, 2, 3)
val vector = Vector(1, 2, 3)  // Fast random access
val set = Set(1, 2, 3)
val map = Map("a" -> 1, "b" -> 2)
// All immutable: operations return new collections
val updated = map + ("c" -> 3)  // New map

Opérations de collection

map transforme les éléments. filter sélectionne. reduce combine. grouped chunk. flatten fusionne les collections imbriquées. flatMap map et aplatit. Tous retournent de nouvelles collections. Les collections paresseuses utilisent .view ou .iterator pour l'efficacité.

scala
val nums = (1 to 10).toList
val doubled = nums.map(_ * 2)
val evens = nums.filter(_ % 2 == 0)
val sum = nums.reduce(_ + _)
val grouped = nums.grouped(3).toList  // List(List(1,2,3), List(4,5,6), ...)
val flat = List(List(1,2), List(3,4)).flatten  // List(1,2,3,4)

Pattern matching de collections

Le pattern matching fonctionne sur les collections. :: (cons) correspond à head et tail. List(a, b, c) correspond exactement à 3 éléments. Nil correspond à la liste vide. _ est wildcard. Utile pour l'analyse et la déstructuration. La correspondance exhaustive prévient les bugs.

scala
val list = List(1, 2, 3)
list match {
    case head :: tail => println(s"Head: $head")
    case Nil => println("Empty")
}
List(1, 2, 3) match {
    case List(a, b, c) => println(s"$a, $b, $c")
    case _ => println("Other")
}

Collections mutables

Les collections mutables (ArrayBuffer, mutable.Set, mutable.Map) modifient sur place. Plus rapides pour les mises à jour fréquentes mais pas thread-safe. Utilisez lorsque la performance compte et l'immutabilité n'est pas nécessaire. Convertissez en immuable avec .toMap, .toSet.

scala
import scala.collection.mutable
val buffer = mutable.ArrayBuffer(1, 2, 3)
buffer += 4  // Add element
buffer -= 2  // Remove element
buffer(0) = 10  // Update
val mset = mutable.Set(1, 2, 3)
val mmap = mutable.Map("a" -> 1)
mmap("b") = 2

Collections paresseuses

view crée une vue paresseuse : les opérations sont différées jusqu'à être forcées. Utile pour les opérations chaînées sur de grandes collections. LazyList (Scala 2.13+) est une séquence paresseuse. Les séquences infinies sont possibles avec l'évaluation paresseuse. Forcez avec .toList, .toArray.

scala
val lazyView = (1 to 1000000).view.map(_ * 2).filter(_ > 100)
// No computation yet
val first = lazyView.head  // Only computes first
val result = lazyView.take(10).toList  // Only 10 elements processed
// LazyList (formerly Stream)
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _)
20

Implicites

Paramètres implicites

Les paramètres implicites sont passés automatiquement lorsqu'ils sont dans la portée. Le compilateur recherche une valeur implicite correspondante. Utilisé pour la configuration, les type classes et le contexte. Peut être surchargé explicitement. Plusieurs implicites doivent avoir des types distincts.

scala
def connect(url: String)(implicit timeout: Int): Unit = {
    println(s"Connecting to $url with timeout $timeout")
}
implicit val defaultTimeout: Int = 5000
connect("http://example.com")  // Uses 5000
connect("http://example.com")(3000)  // Explicit override

Conversions implicites

Les conversions implicites convertissent automatiquement les types. Peuvent être dangereuses (conversions inattendues). implicit class ajoute des méthodes d'extension. Scala 3 utilise given/using et extension pour la clarté. Préférez les méthodes d'extension aux conversions brutes.

scala
import scala.language.implicitConversions
implicit def intToString(n: Int): String = n.toString
val s: String = 42  // Converts via intToString
// Extension methods (Scala 3 preferred)
implicit class RichInt(val n: Int) extends AnyVal {
    def squared: Int = n * n
}
5.squared  // 25

Type classes

Les type classes sont un pattern pour le polymorphisme ad hoc. Un trait définit le comportement, les instances fournissent les implémentations pour des types spécifiques. La résolution implicite trouve l'instance. Plus flexible que l'héritage. Courant dans Cats et Shapeless.

scala
trait Show[A] {
    def show(a: A): String
}
object Show {
    implicit val intShow: Show[Int] = (a: Int) => a.toString
    implicit val stringShow: Show[String] = (a: String) => a
    def apply[A](a: A)(implicit s: Show[A]): String = s.show(a)
}
Show(42)  // "42"
Show("hello")  // "hello"

Context bounds

Les context bounds [A: TypeClass] sont du sucre syntaxique pour les paramètres implicites. L'instance de type class est disponible via implicitly. Syntaxe plus propre pour les contraintes de type class. Courant avec Ordering, Numeric et les type classes personnalisées.

scala
def max[A: Ordering](a: A, b: A): A = {
    val ord = implicitly[Ordering[A]]
    if (ord.gt(a, b)) a else b
}
// Equivalent to:
def max2[A](a: A, b: A)(implicit ord: Ordering[A]): A =
    if (ord.gt(a, b)) a else b

Given/Using (Scala 3)

Scala 3 remplace les implicites par given/using pour la clarté. given définit une instance, using déclare un paramètre. extension remplace implicit class. Plus explicite et lisible. Les outils de migration convertissent les implicites Scala 2.

scala
// Scala 3 syntax
given defaultTimeout: Int = 5000
def connect(url: String)(using timeout: Int): Unit =
    println(s"Connecting with $timeout")
connect("http://example.com")  // Uses given
// Extension methods
extension (n: Int)
    def squared: Int = n * n
21

Concurrence

Future

Future représente un calcul asynchrone. onComplete gère la completion. map/flatMap chaînent les opérations. ExecutionContext fournit les threads. Les Futures sont immuables et one-shot. Utilisez for-comprehension pour plusieurs futures.

scala
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f: Future[Int] = Future {
    Thread.sleep(1000)
    42
}
f.onComplete {
    case Success(v) => println(v)
    case Failure(e) => println(e)
}
// Map/flatMap for chaining
val f2 = f.map(_ * 2).flatMap(x => Future(x + 1))

for-comprehension avec Futures

for-comprehension se désucre en flatMap/map. Séquentiel : chaque étape attend la précédente. Pour l'exécution parallèle, démarrez les futures avant le for. Beaucoup plus lisible que les callbacks imbriqués. Fonctionne avec n'importe quelle monade (Future, Option, List).

scala
def getUser(id: Int): Future[User] = ...
def getOrders(user: User): Future[List[Order]] = ...
val result: Future[List[Order]] = for {
    user <- getUser(1)
    orders <- getOrders(user)
} yield orders
// Equivalent to flatMap/map chaining

Futures parallèles

Démarrer les futures avant le for-comprehension les exécute en parallèle. Future.sequence convertit List[Future[T]] en Future[List[T]]. Future.traverse map et sequence en une étape. zip combine deux futures. Tous se terminent lorsque le plus lent finit.

scala
val f1 = Future { compute1() }
val f2 = Future { compute2() }
val combined: Future[(Int, Int)] = for {
    r1 <- f1
    r2 <- f2
} yield (r1, r2)
// Or: Future.sequence(List(f1, f2))
// Or: Future.traverse(list)(compute)

Actors (Akka)

Les actors Akka encapsulent l'état et communiquent via des messages. Pas d'état mutable partagé. Chaque actor traite un message à la fois. ! (tell) envoie fire-and-forget. ? (ask) retourne un Future. La supervision gère les échecs. Idéal pour les systèmes avec état concurrents.

scala
import akka.actor.*
class Counter extends Actor {
    var count = 0
    def receive = {
        case "inc" => count += 1
        case "get" => sender() ! count
    }
}
val system = ActorSystem("mySystem")
val counter = system.actorOf(Props[Counter], "counter")
counter ! "inc"
counter ! "get"

Cats Effect IO

Cats Effect IO est une monade IO purement fonctionnelle. Référentiellement transparent : IO(println("x")) est une valeur. Se compose avec for-comprehension. Annulable et resource-safe. unsafeRunSync s'exécute au bord de votre programme. Alternative à Future avec une meilleure sémantique.

scala
import cats.effect.IO
val program: IO[Int] = for {
    _ <- IO(println("Start"))
    result <- IO.pure(42)
    _ <- IO(println(s"Got $result"))
} yield result
// Run at end of world
program.unsafeRunSync()
// Referentially transparent, cancellable
22

Pattern matching en profondeur

Case classes

Les case classes auto-génèrent equals, hashCode, toString et apply/unapply. Parfaites pour le pattern matching. Le pattern matching les déstructure. Immuables par défaut. Utilisez pour les algebraic data types. copy() crée des copies modifiées.

scala
case class Point(x: Int, y: Int)
val p = Point(1, 2)
p match {
    case Point(0, 0) => "origin"
    case Point(0, _) => "on y-axis"
    case Point(x, 0) => s"on x-axis at $x"
    case Point(x, y) => s"at ($x, $y)"
}

Sealed traits

Les sealed traits restreignent les sous-types au même fichier. Le compilateur vérifie l'exhaustivité dans le pattern matching. Ajouter un nouveau sous-type provoque des avertissements dans toutes les correspondances. Idéal pour les hiérarchies de types fermées. Combiné avec les case classes pour les ADT.

scala
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Square(side: Double) extends Shape
case class Rectangle(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
    case Circle(r) => math.Pi * r * r
    case Square(s) => s * s
    case Rectangle(w, h) => w * h
}  // Compiler warns if not exhaustive

Gardes et extracteurs

Les gardes (if) ajoutent des conditions aux patterns. Les extracteurs personnalisés (unapply) permettent le pattern matching sur n'importe quel type. unapply retourne Option pour indiquer la correspondance. Les extracteurs découplent la correspondance du type. Puissant pour les DSL et l'analyse.

scala
def classify(n: Int): String = n match {
    case x if x < 0 => "negative"
    case 0 => "zero"
    case x if x % 2 == 0 => "even"
    case _ => "odd"
}
// Custom extractor
object Even {
    def unapply(n: Int): Option[Int] =
        if (n % 2 == 0) Some(n / 2) else None
}
4 match { case Even(half) => s"half is $half" }

Fonctions partielles

Les fonctions partielles sont définies uniquement pour certaines entrées. isDefinedAt vérifie. collect applique uniquement où défini. Utile pour les callbacks et le routage. orElse combine les fonctions partielles. lift convertit en fonction totale retournant Option.

scala
val pf: PartialFunction[Int, String] = {
    case 1 => "one"
    case 2 => "two"
}
pf.isDefinedAt(1)  // true
pf.isDefinedAt(3)  // false
// Collect = filter + map
List(1, 2, 3, 1).collect(pf)  // List("one", "two", "one")

Types de pattern matching

Le pattern matching fonctionne sur les types, mais l'effacement de type affecte les génériques. List(a, b) correspond à une liste de 2 éléments. List(_*) correspond à n'importe quelle liste. Évitez de matcher sur des types génériques comme List[Int] (effacé). Utilisez les type tags pour les infos de type à l'exécution.

scala
def describe(x: Any): String = x match {
    case i: Int => s"Int: $i"
    case s: String => s"String: $s"
    case List(a, b) => s"Two-element list: $a, $b"
    case List(_*) => "List with elements"
    case Some(v) => s"Some: $v"
    case None => "None"
    case _ => "Unknown"
}
23

Programmation fonctionnelle

Fonctions d'ordre supérieur

Les fonctions d'ordre supérieur prennent ou retournent des fonctions. Le currying sépare les fonctions multi-args en chaînes mono-arg. L'application partielle fixe certains arguments. Permet la composition et la réutilisation de fonctions. _ crée une fonction partiellement appliquée.

scala
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(_ + 3, 5)  // 11
// Currying
def add(a: Int)(b: Int): Int = a + b
val add5 = add(5) _  // Partial application
add5(3)  // 8

Option et Either

Option représente les valeurs optionnelles : Some ou None. Évite null. getOrElse fournit une valeur par défaut. Either représente le succès (Right) ou l'échec (Left). Meilleur que les exceptions pour les erreurs attendues. Tous deux sont des monades : map, flatMap, for-comprehension.

scala
def find(id: Int): Option[String] =
    if (id > 0) Some("Alice") else None
find(1).getOrElse("Unknown")  // "Alice"
find(-1).getOrElse("Unknown")  // "Unknown"
// Either for error handling
def parse(s: String): Either[String, Int] =
    try Right(s.toInt)
    catch { case _: Exception => Left(s"Not a number: $s") }

Composition de fonctions

compose chaîne les fonctions de droite à gauche (comme les maths). andThen chaîne de gauche à droite (plus lisible). Tous deux créent de nouvelles fonctions. Utile pour construire des pipelines. Les fonctions sont des valeurs first-class dans Scala.

scala
val addOne: Int => Int = _ + 1
val double: Int => Int = _ * 2
// Compose (right to left)
val f = addOne compose double  // double then addOne
f(3)  // 7
// AndThen (left to right)
val g = addOne andThen double  // addOne then double
g(3)  // 8

Récursion et récursion terminale

La récursion terminale est optimisée en une boucle par le compilateur. L'appel récursif doit être la dernière opération. L'annotation @tailrec vérifie cela à la compilation. Le pattern accumulateur porte l'état. Prévient le stack overflow pour la récursion profonde.

scala
// Not tail-recursive: stack overflow for large n
def factorial(n: Int): Int =
    if (n <= 1) 1 else n * factorial(n - 1)
// Tail-recursive: optimized to loop
import scala.annotation.tailrec
@tailrec
def factorial(n: Int, acc: Int = 1): Int =
    if (n <= 1) acc else factorial(n - 1, n * acc)

Monades

Les monades ont flatMap et unit (pure/pure). Elles chaînent les opérations avec contexte (Option : absence, List : non-déterminisme, Future : async). for-comprehension se désucre en flatMap/map. Les lois monadiques garantissent une composition correcte. Cats fournit la type class Monad.

scala
// Monad laws: left identity, right identity, associativity
// Option is a monad:
Some(5).flatMap(x => Some(x + 1))  // Some(6)
None.flatMap(x => Some(x + 1))  // None
// List is a monad:
List(1, 2).flatMap(x => List(x, x * 10))  // List(1, 10, 2, 20)
// for-comprehension is monadic sugar:
for {
    x <- Some(5)
    y <- Some(x + 1)
} yield y  // Some(6)
24

Pièges courants

Null vs Option

Scala a null pour la compatibilité Java mais c'est découragé. Option représente explicitement l'absence. Le pattern matching force à gérer None. Utilisez .toOption sur Try pour les exceptions. Évitez null dans le code Scala ; réservez-le pour l'interop Java.

scala
// BAD: null
def find(id: Int): String =
    if (id > 0) "Alice" else null
// GOOD: Option
def find(id: Int): Option[String] =
    if (id > 0) Some("Alice") else None
// Scala avoids null; use Option
// NullPointer exceptions are rare in idiomatic Scala

Var vs Val

val est immuable (valeur), var est mutable (variable). Préférez val pour un code plus sûr et plus prévisible. L'état mutable complique le raisonnement et la concurrence. Utilisez var uniquement pour la performance locale ou lorsque vraiment nécessaire. Les collections sont immuables par défaut.

scala
var x = 1  // Mutable
x = 2  // OK
val y = 1  // Immutable
// y = 2  // Error
// Prefer val for immutability
// Use var only when necessary
// Mutable state causes bugs in concurrent code

Égalité

Le == de Scala appelle equals (égalité de valeur), contrairement à Java. eq vérifie l'égalité de référence. ne est la négation de eq. Utilisez toujours == pour la comparaison de valeur. Les case classes ont un equals correct. Pour les classes personnalisées, surchargez equals et hashCode.

scala
val a = List(1, 2)
val b = List(1, 2)
a == b  // true (value equality)
a eq b  // false (reference equality)
// In Java: a == b compares references
// In Scala: == calls equals (value)
// Use eq for reference equality (rarely needed)

Ambiguïté implicite

Plusieurs implicites du même type causent des erreurs d'ambiguïté. Gardez la portée implicite propre. Utilisez des types spécifiques (newtypes) pour distinguer. Le given/using de Scala 3 est plus clair. Évitez les conversions implicites ; elles causent un comportement surprenant.

scala
implicit val s1: String = "hello"
implicit val s2: String = "world"
def greet(implicit s: String) = println(s)
greet  // Error: ambiguous implicit values
// Fix: only one implicit in scope
// Or: pass explicitly
greet(s1)

By-name vs by-value

Les paramètres by-name (=> T) sont évalués paresseusement, à chaque utilisation. Les paramètres by-value sont évalués une fois avant l'appel. By-name permet les structures de contrôle personnalisées (unless, while). Peut causer des évaluations multiples ; cachez avec lazy val si nécessaire.

scala
// By-value: evaluated once
def log(msg: String) = println(msg)
log(expensiveComputation())  // Evaluated before call
// By-name: evaluated each use
def log(msg: => String) = println(msg)
log(expensiveComputation())  // Evaluated only if called
// Useful for lazy evaluation and control flow
def unless(cond: Boolean)(body: => Unit): Unit =
    if (!cond) body
25

Actors Akka

Système d'actors

ActorSystem est le conteneur des actors. actorOf crée des actors avec un nom. ! (tell) envoie un message asynchrone. ? (ask) envoie et retourne un Future. Les actors sont identifiés par chemin. terminate arrête le système proprement.

scala
import akka.actor.*
val system = ActorSystem("MySystem")
val actor = system.actorOf(Props[MyActor], "myActor")
actor ! "Hello"  // Fire-and-forget
val future = actor ? "Query"  // Ask pattern
system.terminate()

Cycle de vie d'actor

preStart s'exécute à la création, postStop à l'arrêt. preRestart/postRestart gèrent la supervision. L'actor est arrêté avant le redémarrage, puis démarré à neuf. L'état interne est perdu au redémarrage. Utilisez preStart pour l'initialisation et postStop pour le nettoyage.

scala
class MyActor extends Actor {
    override def preStart(): Unit = println("Starting")
    override def postStop(): Unit = println("Stopped")
    override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
        println("Restarting")
        super.preRestart(reason, message)
    }
    def receive = {
        case "ping" => sender() ! "pong"
    }
}

Supervision

La supervision définit comment les échecs sont gérés. Resume : continuer avec le même état. Restart : recréer avec un état neuf. Stop : terminer définitivement. Escalate : laisser le parent gérer. OneForOneStrategy affecte uniquement l'enfant échoué. AllForOneStrategy affecte tous les enfants.

scala
class Supervisor extends Actor {
    override val supervisorStrategy = OneForOneStrategy() {
        case _: ArithmeticException => Resume  // Continue
        case _: NullPointerException => Restart  // Restart actor
        case _: Exception => Stop  // Stop actor
    }
    def receive = { case _ => }
}

Routers

Les routers distribuent les messages à plusieurs actors. Pool crée et gère les workers. Group utilise des actors existants. Stratégies : RoundRobin, Random, SmallestMailbox, ConsistentHashing, ScatterGatherFirst. Les routers améliorent le débit en parallélisant le travail.

scala
// Pool router: creates workers
val router = system.actorOf(
    RoundRobinPool(5).props(Props[Worker]),
    "router"
)
router ! "work"  // Distributed to one of 5 workers
// Group router: uses existing actors
val group = system.actorOf(
    RoundRobinGroup(paths).props(),
    "group"
)

Persistance

PersistentActor sauvegarde les événements dans un journal (event sourcing). Les commandes sont validées, puis persistées comme événements. Au redémarrage, les événements sont rejoués via receiveRecover. L'état est reconstruit depuis les événements. Permet la récupération après crash. Utilisez EventSourcedBehavior pour Akka Typed.

scala
class Counter extends PersistentActor {
    var count = 0
    override def persistenceId = "counter-1"
    override def receiveCommand = {
        case "inc" => persist(Incremented) { event =>
            count += 1
        }
    }
    override def receiveRecover = {
        case Incremented => count += 1
    }
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.