Le carnet On est
Chapitre 2 · encre verte

Les fonctions

Déclarer, paramétrer, passer en valeur ou en nom. Une fonction = un objet de première classe. Pattern matching pour décomposer.

~ 45 min Source : Scala_02 — Functions (Zaouche, Dujol) 13 flashcards · 7 QCM

📄 Ressources du cours

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

🎥 Vidéos recommandées

1. Déclaration : def

Syntaxe
def functionName(typedParameter*) [:returnType] = {
   functionBody
}
  • functionName doit commencer par une minuscule
  • Le type est obligatoire pour chaque paramètre d'entrée
  • Le type de retour peut être inféré ⇒ pas toujours requis
  • Le corps est un bloc ⇒ son type est le type de la dernière instruction
scala> def sum2(a : Int, b : Int) : Int = a + b
sum2: (a: Int, b: Int)Int

scala> def sum2(a : Int, b : Int) = a + b      // type infere
sum2: (a: Int, b: Int)Int

scala> def sayHello(name : String) = println(s"Hello $name !")
sayHello: (name: String)Unit

scala> sayHello("world")
Hello world !
Interpolation de chaîne s"…" Toute expression préfixée par $ est remplacée par son toString :
scala> println(s"1 + 1 is ${1 + 1}.")
1 + 1 is 2.

2. Paramètres

Valeur par défaut

scala> def sayHello(name : String = "world") = println(s"Hello $name !")
sayHello: (name: String)Unit

scala> sayHello()
Hello world !

scala> sayHello("everybody")
Hello everybody !
⚠ Plusieurs valeurs par défaut : NON recommandé Le compilateur essaie de matcher les arguments dans l'ordre déclaré ⇒ ambiguïtés.
scala> def greet(fr : Boolean = false, name : String = "world") =
         { val g = if (fr) "Bonjour" else "Hello" ; println(s"$g $name !") }

scala> greet()
Hello world !

scala> greet("everybody")
<console>:13: error: type mismatch;
 found   : String("everybody")
 required: Boolean
Le compilateur attend un Boolean (le premier paramètre, fr) et reçoit un String.

Évaluation : by value vs by name

By value (comportement par défaut)
  • Paramètre toujours évalué à l'appel de la fonction
  • Paramètre évalué une seule fois
By name (ajouter => avant le type)
  • Paramètre évalué à chaque utilisation réelle
  • Peut être évalué plus d'une fois… ou jamais

Particulièrement utile pour des évaluations longues ou potentiellement throwable.

// By value : le 1/0 est evalue meme si x = false
scala> def and2(x : Boolean, y : Boolean) = if (x) y else false
scala> and2(false, 1/0 == 0)
java.lang.ArithmeticException: / by zero

// By name : 1/0 jamais evalue car x = false
scala> def and2(x : Boolean, y : => Boolean) = if (x) y else false
scala> and2(false, 1/0 == 0)
res3: Boolean = false
🔑 À retenir
Les opérateurs logiques de Scala (&&, ||) sont conçus en by-name — c'est exactement ce qui permet le court-circuit :
false && slow(true) renvoie false immédiatement.

Autres features

require (préconditions)
require(condition, message)
Si la condition n'est pas remplie, lance une IllegalArgumentException avec le message.
Non fonctional-compliant (utilise une exception).
Annotation @tailrec
import scala.annotation.tailrec
Indique que la fonction annotée est tail-recursive. Permet au compilateur de l'optimiser (et de t'avertir si elle ne l'est pas vraiment).

3. Tout est citoyen de première classe

« First-class citizen » — un des piliers de la PF Une fonction a le même rang (« citoyenneté ») que n'importe quel autre objet. Elle peut être utilisée comme :
  • une valeur ;
  • une variable ;
  • un paramètre d'une autre fonction ;
  • un résultat d'une autre fonction ;

Fonction comme valeur

scala> val nextInt = (x : Int) => x + 1
nextInt: Int => Int = <function1>

scala> nextInt(2)
res0: Int = 3

// Avec type explicite
scala> val nextInt : Int => Int = x => x + 1

// Avec wildcard _ (placeholder pour chaque parametre)
scala> val nextInt : Int => Int = _ + 1

scala> val add2 : (Int, Int) => Int = _ + _    // 2 wildcards = 2 parametres
scala> add2(2, 3)
res: Int = 5
⚠ Fonction récursive déclarée en valtype de retour explicite obligatoire
scala> val fact : Int => Int =
         x => if (x <= 0) 1 else x * fact(x - 1)
scala> fact(5)
res0: Int = 120

Comment ça fonctionne sous le capot ?

📌 Apply
Toute fonction déclarée en val est un objet possédant une méthode apply de même signature :
function(parameters) ≡ function.apply(parameters)
scala> val nextInt : Int => Int = x => x + 1

scala> nextInt(2)         // res0: Int = 3
scala> nextInt.apply(2)   // res1: Int = 3   --- equivalent

Différences avec une fonction def

Une fonction def est une méthode, pas une valeur fonction. Elle ne peut pas être appelée via .apply directement ; il faut la convertir.
scala> def nextInt(x : Int) = x + 1
scala> nextInt.apply(2)
<console>:13: error: missing argument list for method nextInt
Unapplied methods are only converted to functions when a function type is expected.

// Conversion explicite avec _
scala> val nextInt_val = nextInt(_)
nextInt_val : Int => Int = <function1>
scala> nextInt_val.apply(2)
res0: Int = 3

Fonctions imbriquées (nesting)

Une fonction peut être déclarée à l'intérieur d'une autre via def ou val. Son scope est limité à la fonction englobante. Classique pour encapsuler le tail-recursion :

scala> def factorial(n : Int) = {
         def factorialTR(n : Int, acc : Int) : Int =
            if (n <= 0) acc else factorialTR(n - 1, acc * n)
         factorialTR(n, 1)
       }
factorial: (n: Int)Int

scala> factorialTR                // hors du scope
<console>:13: error: not found: value factorialTR

4. Fonction en paramètre, fonction en résultat

En paramètre

scala> def applyTwice(f : Int => Int, x : Int) = f(f(x))
applyTwice: (f: Int => Int, x: Int)Int

// 1) Fonction anonyme
scala> applyTwice(_ + 1, 2)
res0: Int = 4

// 2) val-declared
scala> val nextInt = (x : Int) => x + 1
scala> applyTwice(nextInt, 2)
res1: Int = 4

// 3) def-declared (conversion implicite)
scala> def nextInt(x : Int) = x + 1
scala> applyTwice(nextInt, 2)
res2: Int = 4

En résultat & curryfication

scala> def twice(f : Int => Int) : Int => Int = x => f(f(x))
twice: (f: Int => Int)Int => Int

scala> val plusTwo = twice(_ + 1)
plusTwo: Int => Int = <function1>

scala> plusTwo(2)
res0: Int = 4

// Application complete en une fois
scala> twice(_ + 1)(2)
res1: Int = 4

Version full def-declared avec notation curryfiée (paramètres séparés en plusieurs listes) :

scala> def twice(f : Int => Int)(x : Int) = f(f(x))
twice: (f: Int => Int)(x: Int)Int

scala> val plusTwo = twice(_ + 1)(_)    // application partielle
plusTwo: Int => Int = <function1>

scala> twice(_ + 1)(2)              // application totale
res2: Int = 4

5. Application partielle

Partially applied function Construire une fonction à partir d'une autre fonction en fixant certains (mais pas tous) paramètres.
// Fonction 3 parametres : x + y * z
scala> val transvect2 : (Int, Int, Int) => Int = _ + _ * _
transvect2: (Int, Int, Int) => Int = <function3>

// On fixe le 2e parametre a 1 -> reste 2 parametres
scala> val add2 = transvect2(_ : Int, 1, _ : Int)
add2: (Int, Int) => Int = <function2>

// On fixe encore le 2e -> reste 1 parametre
scala> val nextInt = add2(_ : Int, 1)       // ou transvect2(_ : Int, 1, 1)
nextInt: Int => Int = <function1>

scala> nextInt(2)
res0: Int = 3

Le type doit être donné explicitement pour les paramètres restés sous forme de wildcard (le compilateur a besoin de savoir leur type).

6. Méthodes génériques (polymorphes)

scala> def isNone[T](o : Option[T]) = (o == None)
isNone: [T](o: Option[T])Boolean

scala> isNone(None)
res0: Boolean = true

scala> isNone(Some(42))
res1: Boolean = false

scala> isNone(Some("42"))
res2: Boolean = false      // T infere automatiquement
⚠ Une fonction déclarée en val ne peut pas être générique. Mais on peut « figer » le type via une partial application :
scala> val isNone_Int = isNone[Int](_)
isNone_Int: Option[Int] => Boolean = <function1>

7. Pattern matching — match

Syntaxe générale
expression match {
   case pattern₁ => result₁
   case pattern₂ => result₂
   ...
   case _        => defaultResult
}

Fonctionnement :

  • L'expression est comparée à chaque patternᵢ
  • S'il y a match, le resultᵢ correspondant est retourné
  • Sinon, c'est defaultResult
  • Le type du résultat est le supertype commun de tous les résultats

Trois règles d'or

① Exhaustivité
Toutes les possibilités doivent être couvertes. Le wildcard _ capture toutes les restantes en un seul cas.
② Séquentialité
Les patterns sont traités dans l'ordre d'écriture. Seul le premier match est considéré.
③ Unicité
Un identifiant n'apparaît qu'une seule fois dans un pattern. Utiliser un if guard pour expliciter les liens.

Exemples

scala> (1, 2, 3) match {
         case (1, 2, _) => 2
         case (1, _, _) => 1
         case _         => 0
       }
res0: Int = 2

Pattern unreachable :

scala> (1, 2, 3) match {
         case (1, _, _) => 1
         case (1, 2, _) => 2     // jamais atteint
         case _         => 0
       }
<console>:15: warning: unreachable code
         case (1, 2, _) => 2
res1: Int = 1

Unicité — l'identifiant x ne peut pas apparaître deux fois :

scala> (2, 2) match {
         case (x, x) => x
         case _      => 0
       }
<console>:14: error: x is already defined as value x

// Solution : utiliser un if guard
scala> (2, 2) match {
         case (x, y) if x == y => x
         case _              => 0
       }
res0: Int = 2

Réviser le chapitre

🃏 Flashcards

Syntaxe minimale d'une fonction Scala ?
def f(p : T) [: R] = body. Type obligatoire sur les paramètres, type de retour inférable.
À quoi sert s"…" ?
Interpolation : remplace toute expression préfixée par $ par son toString. Ex : s"1+1=${1+1}".
By value vs by name ?
By value (défaut) : évalué à l'appel, une fois. By name (=>) : évalué à chaque usage, ou jamais. Utile pour évals coûteuses ou throwable.
Que fait @tailrec ?
Annotation indiquant qu'une fonction doit être tail-recursive. Compilation échoue sinon. Import : scala.annotation.tailrec.
Que veut dire « first-class citizen » ?
Une fonction a le même rang qu'un objet. Elle peut être valeur, variable, paramètre, résultat d'une autre fonction.
Que fait val add2 = _ + _ (typé) ?
Le _ est un placeholder pour chaque nouveau paramètre. Donc val add2 : (Int, Int) => Int = _ + _ est (x, y) => x + y.
Différence def f(x:Int) et val f : Int => Int ?
def = méthode (besoin de conversion explicite pour devenir fonction). val = objet fonction avec méthode apply. f(x) ≡ f.apply(x).
Définition d'une application partielle ?
Construire une nouvelle fonction à partir d'une autre en fixant certains paramètres. Ex : val add2 = transvect2(_, 1, _).
Pourquoi val récursive doit avoir un type de retour explicite ?
Le compilateur a besoin du type pour résoudre la référence à elle-même dans sa définition (qu'il ne peut pas inférer en cours d'analyse).
Les 3 règles d'or du match ?
Exhaustivité (tout couvrir, _ capture le reste) · ② Séquentialité (premier match gagne) · ③ Unicité (un identifiant 1 seule fois, sinon if guard).
Pourquoi case (x, x) => … échoue ?
Règle d'unicité. Un identifiant ne peut apparaître qu'une fois. Utiliser case (x, y) if x == y => ….
Peut-on déclarer une fonction val générique ?
Non. Mais on peut « figer » un type via partial application : val isNone_Int = isNone[Int](_).
À quoi sert le nesting de fonctions ?
Encapsuler les fonctions auxiliaires (typiquement la version tail-recursive) dont le scope ne doit pas fuir.

✎ Quiz éclair

1.def sum(a:Int = 1, b:Int = 2) = a + b. Que fait sum(5) ?
  • Erreur : trop peu d'arguments
  • 7 (a=5, b=2)
  • 6 (a=1, b=5)
  • Erreur d'ambiguïté
Voir la section du chapitre pour la justification détaillée.
2.def slow = { Thread.sleep(5000) ; true } ; def f(x:Boolean, y: => Boolean) = x && y ; f(false, slow) prend combien de temps ?
  • 5 secondes
  • 10 secondes
  • Instantané (y évalué seulement si x=true)
  • Erreur de compilation
Voir la section du chapitre pour la justification détaillée.
3.val add : (Int, Int) => Int = _ + _ ; add.apply(3, 4) donne ?
  • Erreur : pas de méthode apply
  • <function2>
  • 12
  • 7
Voir la section du chapitre pour la justification détaillée.
4.(2, 3) match { case (1, _) => "un" ; case (_, 3) => "trois" ; case _ => "autre" }
  • "un"
  • "autre"
  • "trois"
  • Erreur d'ambiguïté
Voir la section du chapitre pour la justification détaillée.
5.val twice = (f : Int => Int) => (x : Int) => f(f(x)). Combien de paramètres twice prend-il ?
  • 1 (puis retourne une fonction qui en prend 1)
  • 2
  • 0
  • Variable
Voir la section du chapitre pour la justification détaillée.
6.Quel est le bon usage d'un if guard dans match ?
  • Forcer l'ordre des patterns
  • Remplacer le _ par défaut
  • Vérifier une égalité entre deux composants extraits
  • Optimiser la compilation
Voir la section du chapitre pour la justification détaillée.
7.def isSome[T](o:Option[T]) = o != None ; val isSome_Int = isSome[Int](_). isSome_Int est de type :
  • Option[Int]
  • Int => Boolean
  • [T] Option[T] => Boolean
  • Option[Int] => Boolean
Voir la section du chapitre pour la justification détaillée.

Score : 0 / 7 ·

❓ Q/R

Pourquoi le by-name n'est pas le défaut ?
Parce que le by-value est plus prévisible : on évalue une fois, à un endroit connu. Le by-name peut évaluer 0, 1 ou N fois selon le code interne ⇒ surcoût caché et timing peu lisible. On l'active uniquement quand on veut explicitement contrôler ce comportement (court-circuit, paresse, évaluation throwable).
Pourquoi val f : Int => Int et def f(x : Int) ne sont pas la même chose ?
  • val définit une valeur fonction : un objet de type Function1[Int, Int] avec une méthode apply. C'est un first-class citizen.
  • def définit une méthode attachée à un contexte (REPL, classe, objet). Une méthode n'est pas une valeur fonction au départ.
Conséquence : la méthode doit être convertie (avec _ ou par eta-expansion) avant d'être utilisée comme une valeur.
Quand utiliser require ?
Quand on veut imposer une précondition au point d'entrée d'une fonction qui ne peut pas être satisfaite par le typage statique seul. Exemple : def sqrt(x : Double) = { require(x >= 0, "x must be non-negative") ; … }. Le cours note que c'est non functional-compliant (lance une exception, donc effet de bord) — préférer Option ou Try en code pur.
Pourquoi le pattern matching est si présent en FP ?
Parce qu'il remplace les switch à la C avec des super-pouvoirs :
  • Décomposition structurelle (extraire les champs d'un tuple, d'une case class, d'une collection)
  • Tests de type intégrés
  • Exhaustivité vérifiée à la compilation
  • Composable comme une expression — le résultat du match peut être assigné, retourné, passé
On le retrouvera massivement au CM3 (case classes), CM4 (sur les List : head :: tail), et au CM6 (sur les messages d'acteurs Akka).
Comment éviter la duplication du wildcard _ + _ quand on veut accéder deux fois au même paramètre ?
Chaque _ est un nouveau paramètre. Donc val sq : Int => Int = _ * _ est une erreur (deux paramètres distincts). Pour un même paramètre utilisé deux fois, il faut nommer :
val sq : Int => Int = x => x * x