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).
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.StringInterpolation 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.
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 stringTuples
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é.
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.0Infé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é.
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 inferredUnit 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.
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")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.
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")) // trueChaî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.
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
WorldConstruction 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).
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.
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) // 0Regex
.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.
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#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.
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.
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.
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.
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é.
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, fastestFlux 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.
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 neededFor 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.
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.
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.
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 immutabilityTry / 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.
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) // 0Fonctions
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).
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 argLambda (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.
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 parameterFonctions 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.
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-likeCurrying 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.
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.
// 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 }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.
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) // trueCase 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.
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, companionTraits (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.
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 linearizationObject (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.
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.
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 mixinsCollections 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é.
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, 5FlatMap 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.
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.
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.
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.
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(_ + _)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.
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.
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 patternsCorrespondance 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.
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.
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.
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")
}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.
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.
// 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.
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 scopeType 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.
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 polymorphismMé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'.
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 * nConcurrence 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.
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-comprehensionsComposition 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.
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.
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 collectionsPromise (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.
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 sideSync 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.
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 insteadImplicites 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.
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.
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 // convertedPriorité 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.
// 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.
// 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'.
// 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
}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.
// 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.
// 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.sumType 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.
// 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.
// 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 correctlyDé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 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 automaticallyFor 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.
// 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, 3For 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.
// 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 compositionFor 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.
// 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.
// 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 doubledFor 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é.
// 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 yieldScala 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 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.lengthClauses 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 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 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(" ").toListEnums 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 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 + 1Dé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 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"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 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.
// 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.
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.implicitConversionsProgrammation 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.
// 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 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 + 32Tests (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.
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 availableAssertions 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é.
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.
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.
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.
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"))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.
// 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) // 7Currying 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'.
// 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.
// 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 programDonné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)).
// 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.
// 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)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).
// 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.
// 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.
// 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.
// 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.
// 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
}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é.
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 mapOpé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é.
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.
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.
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") = 2Collections 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.
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(_ + _)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.
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 overrideConversions 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.
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 // 25Type 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.
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.
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 bGiven/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 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 * nConcurrence
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.
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).
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 chainingFutures 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.
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.
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.
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, cancellablePattern 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.
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.
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 exhaustiveGardes 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.
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.
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.
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"
}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.
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) // 8Option 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.
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.
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) // 8Ré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.
// 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.
// 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)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.
// 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 ScalaVar 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.
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.
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.
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.
// 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) bodyActors 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.
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.
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.
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.
// 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.
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
}
}Snippets Scala associés
Copy-paste ready code for common tasks.
Filtrage par Motif
Déstructurer des valeurs et filtrer des cas en Scala.
Case Classes
Classes de données immuables avec equals/hashCode/toString auto-générés.
Opérations sur Collections
Opérations fonctionnelles : map, filter, fold, groupBy.
Traits et Mixins
Composer des comportements avec des traits à implémentation par défaut.
Futures et Async
Calcul asynchrone avec Future et ExecutionContext.
Implicits (Given/Using en Scala 3)
Dérivation de type classes et passage de contexte via implicits.
Akka Actors (Pekko)
Concurrence par passage de messages avec le modèle actor.
Type Classes (style Cats)
Polymorphisme ad-hoc via type classes.
Was this helpful?