Le paradigme fonctionnel
Pas d'effet de bord. Tout est expression typée. Variables → constantes, instructions → expressions, boucles → récursions. Scala comme terrain de jeu.
📄 Ressources du cours
🎥 Vidéos recommandées
-
🇫🇷
Voyage en terre inconnue, épisode 1 : Scala - Functional Programming
Montpellier Tech Hub · 01:03:24 · 2k vues — Scala est un langage de programmation multi-paradigme, associant la programmation objet à la programmation fonctionnelle, qui prône une écriture concise, élé...
-
🇫🇷
Tutoriel - programmation en Scala
Juvénal CHOKOGOUE · 58:47 · 1k vues — Vous souhaitez travailler dans le Big Data ? Vous souhaitez maîtriser Spark pour le Big Data avec Scala ? Nous vous offrons gratuitement cette formation de 2...
-
🇬🇧
Scala Tutorial - Elements of Functional Programming - Part 2
Learning Journal · 20:37 · 30k vues — Spark Programming and Azure Databricks ILT Master Class by Prashant Kumar Pandey - Fill out the google form for Course inquiry.https://forms.gle/Nxk8dQUPq4o...
1. Le paradigme fonctionnel ≡ pas d'effet de bord
Concrètement, le passage de l'impératif au fonctionnel se fait par cinq substitutions :
| Impératif | → | Fonctionnel | Raison |
|---|---|---|---|
| → | Constante | Modifier une variable est un effet de bord. | |
| → | Expression (typée) | Seul intérêt d'une instruction : l'effet de bord qu'elle produit. | |
| → | Fonction pure | Seul intérêt d'une procédure : l'effet de bord qu'elle produit. | |
| → | Récursivité | Une boucle ≡ instructions avec modification de variable. | |
| + Toute fonction est une (typée) expression. | |||
Le programme fonctionnel ne décrit pas quoi faire mais quoi calculer. La liste des cinq substitutions ci-dessus est la grille de lecture de tout le cours.
2. « Moins d'éléments = moins expressif ? »
Question naturelle : si on retire variables, instructions, procédures et boucles, que reste-t-il ? Réponse :
- 1930-1936 — Alonzo Church définit le λ-calcul : représentation des langages fonctionnels.
- 1936-1937 — Alan Turing définit les machines de Turing : représentation des ordinateurs actuels.
- 1936-1937 — Turing prouve l'équivalence des deux représentations.
- Moins d'éléments ⇒ plus simple, plus intuitif
- Analyse plus facile, tests unitaires plus simples
- Plus proche de la description mathématique
- Plus de ressources nécessaires (s'améliore)
- Limites d'applicabilité (I/O reste un effet de bord)
Historique des langages fonctionnels
- 1958 — LISP (LISt Processor) ; 1970 : Scheme
- 1973 — ML (Meta Language) ; 1985 : CaML, 1996 : OCaML, 1990 : Standard ML
- 1990 — Haskell
- 2003-2004 — Scala (Martin Odersky)
- 2005 — F# (basé sur C#)
Du côté des langages impératifs, l'adoption progressive des features fonctionnelles :
| Année | Langage | Note |
|---|---|---|
| 1972 | C | — |
| 1985 | C++ | OO ; 2011 : fonctions anonymes (lambdas) |
| 1995 | Java | OO ; 2014 : Java 8 intègre les fonctions anonymes |
| 2000 | C# | OO |
| 2011 | Kotlin | OO avec éléments fonctionnels |
Scala se classe 1er en médiane mondiale : 76 000 $ (global), 150 000 $ (USA). Raison : Reactive Manifesto — les langages classiques (C++, Java, C#) peinent à couvrir les nouveaux besoins : haute disponibilité, scalabilité, concurrence, distribué.
3. Récap théorique
Fonction pure
- Paramètres en entrée seulement
- Résultat dépendant uniquement des paramètres de la fonction
Une telle fonction est une constante typée. Son type est donné par :
- les types de ses paramètres ;
- et le type de son résultat.
Fonction récursive
Fonction qui s'appelle elle-même.
Pour terminer, une fonction récursive doit contenir au moins un cas de base, c-à-d une évaluation non récursive.
Fonction récursive telle que tout appel récursif est un appel direct de la fonction elle-même. Elle consomme moins de mémoire qu'une récursive non terminale.
Exemple : factorielle
Non tail-recursive :
Function factorial(n : integer) : integer
If n <= 0 then
Return 1
Else
Return n * factorial(n - 1)
EndIf
EndFunction
Tail-recursive :
Function factorialTR(n, a : integer) : integer
If n = 0 then
Return a
Else
Return factorialTR(n - 1, n * a)
EndIf
EndFunction
Function factorial(n : integer) : integer
Return factorialTR(n, 1)
EndFunction
Dans la version TR, l'appel récursif factorialTR(n-1, n*a) est la dernière opération — le compilateur peut transformer la récursion en boucle (élimination de la pile d'appels).
4. Typage statique vs dynamique
Exemples : Java, C++, Scala.
Exemples : JavaScript, Python, Ruby, Groovy.
Conséquence pratique : en Scala, on déclare un val x : Int et le compilateur refusera x = "42" au moment de la compilation, pas de l'exécution.
5. Scala — fiche d'identité
- Professeur à l'École Polytechnique Fédérale de Lausanne
- Co-designer des Generics Java (Java 5)
- Auteur original du compilateur
javacde référence
Apache 2.0 · Version cours : 2.13.3 (25 juin 2020) · Scala 3 (« Dotty ») disponible en test fin 2020.
• Langage fonctionnel ET orienté objet
• Strong static type checking
• Encodage UTF-8 par défaut
• Langage JVM ⇒ génère du bytecode Java
Frameworks fondés sur Scala
| Domaine | Frameworks |
|---|---|
| Acteurs / messages | Akka (voir chapitre 6) |
| Web | Play!, Lift, Scalatra |
| Tests | ScalaTest, ScalaCheck, Specs2 |
| Calcul distribué | Spark |
6. Environnement de développement
Console (REPL)
Appel scala sans paramètre lance un REPL :
$ scala
Welcome to Scala 2.11.12 (OpenJDK 64-Bit Server VM, Java 11.0.8).
Type in expressions for evaluation. Or try :help.
scala> 1 + 1
res0: Int = 2
scala> "42"
res1: String = 42
Option -Dscala.color active la coloration syntaxique sur les résultats. Commandes spéciales :
:help:load fichier— charge le contenu d'un fichier comme code Scala:quit(ou Ctrl+D) — quitte la console
sbt (Simple Build Tool)
- Configuration minimale pour projets simples
- Support de Scala (et des tests)
- Tâches écrites en DSL Scala
- Dépendances via Ivy
- Mode interactif (console) disponible
Structure d'un projet sbt :
Working directory/
├── build.sbt (configuration)
├── lib/ (JARs)
└── src/
├── main/
│ ├── java/ (sources Java)
│ ├── scala/ (sources Scala)
│ └── resources/ (fichiers de données)
└── test/
├── java/ (tests Java)
├── scala/ (tests Scala)
└── resources/
Exemple de build.sbt :
organization := "fr.cyu"
name := "HelloWorld"
version := "1.0-SNAPSHOT"
// Une dépendance pour les tests
libraryDependencies += "junit" % "junit" % "4.8" % "test"
// Plusieurs dépendances
libraryDependencies ++= Seq("org.scala-lang" % "scala-xml" % "2.11.0-M4",
"joda-time" % "joda-time" % "2.9.5")
Sous-commandes sbt utiles :
| Commande | Action |
|---|---|
clean | Supprime tous les fichiers générés dans target/ |
compile | Compile toutes les sources de main/ |
test | Compile test/ et lance les tests |
package | Produit une archive JAR |
run args | Exécute la classe principale avec args |
help | Liste les sous-commandes |
help commande | Aide d'une commande |
Toutes ces sous-commandes peuvent être invoquées (1) en paramètre du terminal (sbt compile) ou (2) directement dans la console sbt.
7. Éléments de base de Scala
Main class… ou plutôt main object
Même objectif qu'en Java (point d'entrée avec une méthode main, arguments capturés dans un Array[String]), mais quelques différences :
- Une seule instance ⇒ déclaration
object(au lieu declass) - Le trait
Appdéfinit lui-mêmemain: pas besoin de la déclarer si l'objetextends App; le corps entier de l'objet devient le corps demain.
Version classique :
object HelloWorld {
def main(args : Array[String]) {
println("Hello World !")
}
}
Avec App :
object HelloWorld extends App {
println("Hello, world!")
}
Non recommandé en Scala 3 (raisons de performance).
Types de base
AnyVal.
| Catégorie | Types |
|---|---|
| Entiers | Byte, Short, Int, Long |
| Flottants | Float, Double |
| Autres | Boolean, Char |
| Spéciaux | Unit ≡ void en Java · Nothing : type sans valeur |
| … (46 types de base au total) | |
Les types de base de Scala encapsulent leurs homologues Java ⇒ toutes les opérations Java sont disponibles, avec quelques subtilités (notamment les conversions implicites, qui deviennent moins implicites en Scala 3).
1 / 2 renvoie 0 : Int. Convertir avant la division :
scala> 1 / 2
res0: Int = 0
scala> (1 / 2).toDouble
res1: Double = 0.0 // trop tard !
scala> 1.toDouble / 2
res2: Double = 0.5 // correct
val vs var
val — Valeur
Contenu immuable. Équivalent du final en Java.
var — Variable
Contenu mutable. Équivalent de la variable classique.
valLa programmation fonctionnelle favorise les valeurs. Utilise
var uniquement quand tu as une bonne raison.
// Variable en Scala / Variable en Java
var i : Int = 7 | int i = 7;
var msg : String = "Hello world !" | String msg = "Hello world !";
// Valeur en Scala / Valeur en Java
val i : Int = 7 | final int i = 7;
val msg : String = "Hello world !" | final String msg = "Hello world !";
Inférence de type (contrairement à Java) : le type n'est pas toujours requis (mais recommandé).
scala> val answer : String = "42"
answer: String = 42
scala> val answer = "42" // type inféré
answer: String = 42
scala> val answer : Int = "42"
<console>:11: error: type mismatch;
found : String("42")
required: Int
Évaluation standard vs lazy val
- Un bloc = séquence d'instructions/expressions ; son résultat est la dernière expression.
- Les déclarations sont évaluées immédiatement, dès qu'elles sont déclarées.
lazy val — évaluation paresseuse
- Évaluation au premier usage réel en dehors de la déclaration.
- Ensuite, reste inchangé (c'est toujours un
val).
scala> val x = {println("x is set") ; 3 + 3}
x is set // evalue immediatement
x: Int = 6
scala> lazy val x = {println("x is set") ; 3 + 3}
x: Int = <lazy> // pas encore evalue
scala> x + 3
x is set // evalue maintenant
res0: Int = 9
scala> x
res1: Int = 6 // reste inchange, pas re-evalue
Combiné à un if :
scala> lazy val x = {println("x is set") ; 3 + 3}
x: Int = <lazy>
scala> if (false) x else 0
res0: Int = 0 // x pas evalue
scala> if (true) x else 0
x is set // x evalue ici
res1: Int = 6
⇒ Évaluation dynamique, bien que le type checking reste statique.
Tout est expression… donc tout est typé
scala> val e1 = 1
e1: Int = 1
scala> val e0 = println(e1)
1
e0: Unit = () // type Unit, valeur ()
scala> if (e1 == e2) e1 else e2
res2: Int = 2
if (cond) expr1 else expr2
- La condition
conddoit être entre parenthèses - Type du résultat : supertype commun de
expr1etexpr2
Un bloc crée aussi un scope local : les valeurs déclarées à l'intérieur ne fuitent pas dehors.
scala> val e = {
val cmPerInch = 2.54
15 * cmPerInch
}
e: Double = 38.1
scala> cmPerInch
<console>:12: error: not found: value cmPerInch
Hiérarchie des classes Scala
| Catégorie | Classe | Description |
|---|---|---|
| Supertypes | AnyVal | Supertype de tous les types de base |
AnyRef | Supertype de toutes les autres classes (Java incluses). Défaut pour les classes user-defined. | |
Any | Supertype de toute classe (donc de AnyVal et AnyRef) | |
| Subtypes | Nothing | Subtype de n'importe quelle classe |
Null | Subtype de toute classe non basique | |
| Alias | String | Alias vers java.lang.String |
La hiérarchie Scala : Any au sommet, Nothing tout en bas. Les pointillés représentent les relations « sous-type de ».
Un val x : Any = "abc" est valide (upcasting implicite). Mais val y : String = x renvoie une erreur (downcasting refusé en static typing).
8. Tuples & Option[T]
Tuples
- Syntaxe issue des mathématiques
- Nombre fixe de composants : max 22 en Scala 2 (limite retirée en Scala 3)
- Types des composants peuvent être différents
- Très pratique pour qu'une fonction retourne plusieurs valeurs
scala> val t = ("The answer", 42, "Douglas Adams")
t: (String, Int, String) = (The answer,42,Douglas Adams)
scala> t._2 // acces 1-indexed
res0: Int = 42
scala> t._4
<console>:13: error: value _4 is not a member of (String, Int, Double)
Option[T]
Option[T]
Fournir une façon d'ajouter une valeur « pas de valeur » au type T.
Deux types d'expression :
Some(x)— oùxest de typeT(simple encapsulation)None— pour le cas « pas de valeur »
L'absence de résultat devient un comportement normal (avec
None).
- Gestion des erreurs plus facile (notamment pour les fonctions génériques)
- L'erreur peut être transmise à la fonction appelante
- Limite : peut ne pas suffire pour différencier plusieurs erreurs
Exemple : conversion String → Int
scala> def toInt(s : String) : Option[Int] = {
try {
Some(Integer.parseInt(s.trim))
} catch {
case e : Exception => None
}
}
toInt: (s: String)Option[Int]
scala> toInt("42")
res0: Option[Int] = Some(42)
scala> toInt("The answer")
res1: Option[Int] = None
★ Réviser le chapitre
🃏 Flashcards
Clique pour retourner la carte. Espace pour la carte focus.
val / var ?val = contenu immuable (≡ final Java). var = contenu mutable. La PF favorise val.lazy val ?1 / 2 en Scala ?0 : Int (division entière, comme en Java). Pour obtenir 0.5 : 1.toDouble / 2. Convertir avant la division.if (cond) expr1 else expr2 ?expr1 et expr2. Ex : if (true) 42 else "42" → Any.Any (parent de AnyVal et AnyRef). Bas : Nothing (subtype de toute classe).Option[T] ?T. Deux constructions : Some(x) ou None. Remplace les exceptions.object devient le corps de main ?App. object HelloWorld extends App { … }. Non recommandé en Scala 3 (perf).✎ Quiz éclair
Une seule bonne réponse par question. Le score s'affiche en bas.
scala> val x = "Hello" ; scala> x = "World" donne…lazy val z = { println("hello") ; 5 } puis z + z affiche « hello » combien de fois ?Int et String ?val t = (1, "deux", 3.0), comment accède-t-on à "deux" ?Score : 0 / 6 ·
❓ Q/R
Pourquoi le fonctionnel exige-t-il « pas d'effet de bord » ?
- Le code devient plus facile à raisonner (équivalence avec une fonction mathématique).
- Les tests unitaires sont triviaux : on appelle avec une entrée, on vérifie la sortie. Pas d'état caché à mocker.
- Le code devient plus facile à paralléliser : pas d'état partagé à synchroniser (cf. Reactive Manifesto, Akka au CM6).
Quand préférer une fonction non-tail-recursive à une tail-recursive ?
n, la version naïve provoque un StackOverflowError ; la TR non. La règle pratique : si le calcul peut être profond, on porte en TR via un accumulateur.
Pourquoi Option[T] au lieu d'Exception ?
- Le type est explicite : à la lecture de
def toInt(s: String): Option[Int], on sait que l'absence est possible. Avec une exception, c'est invisible dans la signature. - Le compilateur force la prise en compte : pour utiliser le
Intdans leSome, il faut faire un match ou un.getOrElse(…). - Composition : on peut chaîner facilement plusieurs
Optionviamap/flatMapsanstry/catchimbriqués (vu au CM2).
None ne porte pas d'info sur la raison de l'échec. Pour ça, on utilisera plus tard Either[L, R] ou Try[T].
Pourquoi 1/2 = 0 et pas 0.5 en Scala ?
1 et 2 sont inférés en Int. L'opérateur / sur deux Int est la division entière, comme en Java/C/C++. La conversion en Double doit avoir lieu avant la division :
(1 / 2).toDouble=0.0❌ (la division entière a déjà eu lieu)1.toDouble / 2=0.5✓ (un opérande devientDouble, le second est promu)1.0 / 2=0.5✓ (idem)