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.
📄 Ressources du cours
🎥 Vidéos recommandées
-
🇫🇷
Ordre dans R encadrement . Ordre et addition. Ordre et multiplication. Ordre et inverse.
Maths.idrissi · 08:06 · 9k vues — Ordre dans R encadrementContenus du ChapitreL’ordre dans l’ensemble R Ordre et addition. Ordre et multiplication. Ordre et inverse. Inégalité sur les carrés ...
-
🇫🇷
Dérivées d'ordre supérieur
Youness Lotfi · 07:46 · 8k vues — Dans cette capsule vidéo nous introduirons comment calculer les dérivées d’ordre supérieur d’une fonction en utilisant la définition de la dérivée.
-
🇬🇧
7 Functional Programming Techniques EVERY Developer Should Know
ArjanCodes · 21:35 · 36k vues — In this video, I'll walk you through 7 functional programming techniques and demonstrate how they work. Although Python is not a purely functional language, ...
1. Déclaration : def
def functionName(typedParameter*) [:returnType] = {
functionBody
}
functionNamedoit 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 !
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 !
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
- Paramètre toujours évalué à l'appel de la fonction
- Paramètre évalué une seule fois
=> 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
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).
@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
- 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
val ⇒ type 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 ?
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
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
// 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
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
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
Toutes les possibilités doivent être couvertes. Le wildcard
_ capture toutes les restantes en un seul cas.
Les patterns sont traités dans l'ordre d'écriture. Seul le premier match est considéré.
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
def f(p : T) [: R] = body. Type obligatoire sur les paramètres, type de retour inférable.s"…" ?$ par son toString. Ex : s"1+1=${1+1}".=>) : évalué à chaque usage, ou jamais. Utile pour évals coûteuses ou throwable.@tailrec ?scala.annotation.tailrec.val add2 = _ + _ (typé) ?_ est un placeholder pour chaque nouveau paramètre. Donc val add2 : (Int, Int) => Int = _ + _ est (x, y) => x + y.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).val add2 = transvect2(_, 1, _).match ?_ capture le reste) · ② Séquentialité (premier match gagne) · ③ Unicité (un identifiant 1 seule fois, sinon if guard).case (x, x) => … échoue ?case (x, y) if x == y => ….val générique ?val isNone_Int = isNone[Int](_).✎ Quiz éclair
def sum(a:Int = 1, b:Int = 2) = a + b. Que fait sum(5) ?def slow = { Thread.sleep(5000) ; true } ; def f(x:Boolean, y: => Boolean) = x && y ; f(false, slow) prend combien de temps ?val add : (Int, Int) => Int = _ + _ ; add.apply(3, 4) donne ?(2, 3) match { case (1, _) => "un" ; case (_, 3) => "trois" ; case _ => "autre" }val twice = (f : Int => Int) => (x : Int) => f(f(x)). Combien de paramètres twice prend-il ?if guard dans match ?def isSome[T](o:Option[T]) = o != None ; val isSome_Int = isSome[Int](_). isSome_Int est de type :Score : 0 / 7 ·
❓ Q/R
Pourquoi le by-name n'est pas le défaut ?
Pourquoi val f : Int => Int et def f(x : Int) ne sont pas la même chose ?
valdéfinit une valeur fonction : un objet de typeFunction1[Int, Int]avec une méthodeapply. C'est un first-class citizen.defdéfinit une méthode attachée à un contexte (REPL, classe, objet). Une méthode n'est pas une valeur fonction au départ.
_ ou par eta-expansion) avant d'être utilisée comme une valeur.
Quand utiliser require ?
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 ?
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
matchpeut être assigné, retourné, passé
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 ?
_ 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