Le carnet On est
Chapitre 1 · encre bleue

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.

~ 35 min Source : Scala_01 — Basics (Zaouche, Dujol) 11 flashcards · 6 QCM

📄 Ressources du cours

📥 Télécharger les slides  ·  🔗 Ouvrir dans un nouvel onglet  ·  📝 Ouvrir le TD

🎥 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

Définition fondatrice (Mary Rose Cook) « Le code fonctionnel se caractérise par une seule chose : l'absence d'effet de bord. Il ne s'appuie pas sur des données hors de la fonction courante, et ne modifie pas de donnée existant hors de cette fonction. » Toute autre caractéristique « fonctionnelle » se déduit de cette propriété. Sers-toi de ça comme fil conducteur.

Concrètement, le passage de l'impératif au fonctionnel se fait par cinq substitutions :

ImpératifFonctionnelRaison
VariableConstanteModifier une variable est un effet de bord.
InstructionExpression (typée)Seul intérêt d'une instruction : l'effet de bord qu'elle produit.
ProcédureFonction pureSeul intérêt d'une procédure : l'effet de bord qu'elle produit.
BoucleRécursivitéUne boucle ≡ instructions avec modification de variable.
+ Toute fonction est une (typée) expression.
🔑 À retenir
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 :

Équivalence Church ↔ Turing
  • 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.
Avantages du fonctionnel
  • Moins d'éléments ⇒ plus simple, plus intuitif
  • Analyse plus facile, tests unitaires plus simples
  • Plus proche de la description mathématique
Inconvénients
  • 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-2004Scala (Martin Odersky)
  • 2005 — F# (basé sur C#)

Du côté des langages impératifs, l'adoption progressive des features fonctionnelles :

AnnéeLangageNote
1972C
1985C++OO ; 2011 : fonctions anonymes (lambdas)
1995JavaOO ; 2014 : Java 8 intègre les fonctions anonymes
2000C#OO
2011KotlinOO avec éléments fonctionnels
💸 Effet salaire (StackOverflow Survey 2020)
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

Fonction pure ≡ fonction sans effet de bord
  • Paramètres en entrée seulement
  • Résultat dépendant uniquement des paramètres de la fonction
Similaire à une fonction au sens mathématique (si un type est associé à son ensemble de valeurs).

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

Récursive
Fonction qui s'appelle elle-même.
Cas de base
Pour terminer, une fonction récursive doit contenir au moins un cas de base, c-à-d une évaluation non récursive.
Récursive terminale (tail-recursive)
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

Typage statique Les types sont liés aux variables.
Exemples : Java, C++, Scala.
Typage dynamique Les types sont liés aux valeurs des variables.
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é

Créé en 2003 par Martin Odersky
  • Professeur à l'École Polytechnique Fédérale de Lausanne
  • Co-designer des Generics Java (Java 5)
  • Auteur original du compilateur javac de référence
📜 Licence & version
Apache 2.0 · Version cours : 2.13.3 (25 juin 2020) · Scala 3 (« Dotty ») disponible en test fin 2020.
⚙ Caractéristiques
• 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

DomaineFrameworks
Acteurs / messagesAkka (voir chapitre 6)
WebPlay!, Lift, Scalatra
TestsScalaTest, 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)

Features de sbt
  • 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 :

CommandeAction
cleanSupprime tous les fichiers générés dans target/
compileCompile toutes les sources de main/
testCompile test/ et lance les tests
packageProduit une archive JAR
run argsExécute la classe principale avec args
helpListe les sous-commandes
help commandeAide 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 de class)
  • Le trait App définit lui-même main : pas besoin de la déclarer si l'objet extends App ; le corps entier de l'objet devient le corps de main.

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

En Scala, tout type de base est une classe. Tout type de base est sous-classe de AnyVal.
CatégorieTypes
EntiersByte, Short, Int, Long
FlottantsFloat, Double
AutresBoolean, Char
SpéciauxUnitvoid 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).

⚠ Division entière Comme en Java, 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.
📌 Préférer val
La 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

Évaluation standard
  • 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
Expression if (cond) expr1 else expr2
  • La condition cond doit être entre parenthèses
  • Type du résultat : supertype commun de expr1 et expr2

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égorieClasseDescription
SupertypesAnyValSupertype de tous les types de base
AnyRefSupertype de toutes les autres classes (Java incluses). Défaut pour les classes user-defined.
AnySupertype de toute classe (donc de AnyVal et AnyRef)
SubtypesNothingSubtype de n'importe quelle classe
NullSubtype de toute classe non basique
AliasStringAlias vers java.lang.String
scala.Any scala.AnyVal scala.AnyRef ≡ java.lang.Object Int · Double · Boolean Char · Unit · Byte · … String · List · Seq Iterable · (user-defined) scala.Null scala.Nothing

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

Tuple ≡ enregistrement immutable à champs numérotés
  • 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]

But d'Option[T] Fournir une façon d'ajouter une valeur « pas de valeur » au type T.

Deux types d'expression :

  • Some(x) — où x est de type T (simple encapsulation)
  • None — pour le cas « pas de valeur »
⚠ Conçu pour remplacer les exceptions
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 StringInt

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.

Une fonction pure, c'est…
Une fonction sans effet de bord : paramètres en entrée seulement, résultat dépendant uniquement des paramètres.
Cinq transformations impératif → fonctionnel ?
Variable → Constante · Instruction → Expression typée · Procédure → Fonction pure · Boucle → Récursivité · Toute fonction est une expression typée.
Qu'est-ce qu'une fonction tail-recursive ?
Fonction récursive telle que tout appel récursif est un appel direct de la fonction. Consomme moins de mémoire (le compilateur peut éliminer la pile).
Différence val / var ?
val = contenu immuable (≡ final Java). var = contenu mutable. La PF favorise val.
Que fait lazy val ?
Évaluation reportée au premier usage réel de la valeur (en dehors de la déclaration). Ensuite, reste inchangé.
Typage statique vs dynamique ?
Statique : types liés aux variables (Scala, Java, C++). Dynamique : types liés aux valeurs (Python, JS, Ruby).
Que renvoie 1 / 2 en Scala ?
0 : Int (division entière, comme en Java). Pour obtenir 0.5 : 1.toDouble / 2. Convertir avant la division.
Type de if (cond) expr1 else expr2 ?
Le supertype commun de expr1 et expr2. Ex : if (true) 42 else "42"Any.
Sommet et bas de la hiérarchie Scala ?
Sommet : Any (parent de AnyVal et AnyRef). Bas : Nothing (subtype de toute classe).
À quoi sert Option[T] ?
Ajouter une valeur « pas de valeur » au type T. Deux constructions : Some(x) ou None. Remplace les exceptions.
Quel trait fait que tout le corps d'un object devient le corps de main ?
Le trait 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.

1.Lequel est vraiment un effet de bord ?
  • Calculer le carré d'un nombre
  • Modifier une variable globale
  • Retourner un tuple
  • Lire un paramètre d'entrée
Voir la section du chapitre pour la justification détaillée.
2.Une fonction récursive doit absolument avoir…
  • Un lazy val caché
  • Au moins deux paramètres
  • Au moins un cas de base
  • Être tail-recursive
Voir la section du chapitre pour la justification détaillée.
3.scala> val x = "Hello" ; scala> x = "World" donne…
  • x: String = World
  • x: String = HelloWorld
  • Une erreur de réassignation à compile-time
  • Une exception à l'exécution
Voir la section du chapitre pour la justification détaillée.
4.lazy val z = { println("hello") ; 5 } puis z + z affiche « hello » combien de fois ?
  • 0 fois
  • 1 fois
  • 2 fois
  • Erreur de compilation
Voir la section du chapitre pour la justification détaillée.
5.Quel est le supertype commun de Int et String ?
  • Any
  • AnyVal
  • AnyRef
  • Nothing
Voir la section du chapitre pour la justification détaillée.
6.À partir de val t = (1, "deux", 3.0), comment accède-t-on à "deux" ?
  • t[1]
  • t.1
  • t._1
  • t._2
Voir la section du chapitre pour la justification détaillée.

Score : 0 / 6 ·

❓ Q/R

Pourquoi le fonctionnel exige-t-il « pas d'effet de bord » ?
Sans effet de bord, une fonction est déterministe : même entrée ⇒ même sortie. Donc :
  • 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 ?
Presque jamais en pratique, sauf pour la lisibilité dans un cas pédagogique simple. La version TR est quasi-toujours préférable car elle n'empile pas les appels. Pour une factorielle d'un grand 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 ?
Trois raisons :
  • 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 Int dans le Some, il faut faire un match ou un .getOrElse(…).
  • Composition : on peut chaîner facilement plusieurs Option via map / flatMap sans try/catch imbriqués (vu au CM2).
Limite : 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 ?
Parce que 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 devient Double, le second est promu)
  • 1.0 / 2 = 0.5 ✓ (idem)