2. Le Type Option
Le Milliard de Dollars
En 1965, Tony Hoare invente null pour le langage ALGOL. En 2009, il présente ses excuses devant une conférence de 2000 développeurs :
"I call it my billion-dollar mistake. It has caused a billion dollars of pain and damage in the last forty years."
null est dangereux parce qu'il est invisible : n'importe quelle variable peut en être un, et le compilateur ne vérifie rien. Le crash n'arrive qu'à l'exécution, parfois en production. Le déréférencement de pointeur null (CWE-476) figure chaque année parmi les vulnérabilités les plus fréquentes du classement MITRE CWE Top 25, aux côtés des débordements de tampon et des injections.
La réponse moderne - adoptée par Rust, Swift, Kotlin, Haskell, Gleam, et progressivement par Java et TypeScript - est de rendre l'absence explicite dans le type. Si une fonction peut ne rien retourner, son type de retour le dit. Le compilateur refuse alors tout code qui ignorerait ce cas.
C'est exactement ce que fait Option.
Le Problème
Que Retourner Quand Ça Échoue ?
// Comment écrire cette fonction ?
pub fn dernierElement(lst: Liste(a)) -> ???
// Problème : Que retourner si la liste est vide ?
// Pas d'élément à retourner !
En d'autres langages :
- Python/Java :
Noneounull→ crashes possibles - C : Valeur spéciale (-1, 0) → ambiguë
- Exceptions : Lourd et imprévisible
En Gleam : Le type Option !
Définition de Option
Le Type
Lecture : Un Option(a) est SOIT None (rien), SOIT Some contenant une valeur de type a.
Exemples de Valeurs
// Option(Int)
None // Pas d'entier
Some(42) // L'entier 42
// Option(String)
None // Pas de chaîne
Some("hello") // La chaîne "hello"
// Option(Bool)
None // Pas de booléen
Some(True) // Le booléen True
Intuition
Option = Boîte Optionnelle
Analogie :
- Boîte fermée avec un cadeau →
Some(valeur) - Boîte vide →
None
En Comparaison avec d'Autres Langages
| Langage | Équivalent | Problème |
|---|---|---|
| Python | None |
Pas typé, crash possible |
| Java | null |
NullPointerException |
| JavaScript | null/undefined |
Confusion |
| Rust | Option<T> |
Même concept ! ✅ |
| Haskell | Maybe a |
Même concept ! ✅ |
| Elm | Maybe a |
Même concept ! ✅ |
| Gleam | Option(a) |
Sûr à la compilation ✅ |
Utilisation avec les Listes
Exemple 1 : premier
// Renvoie le premier élément d'une liste
pub fn premier(lst: Liste(a)) -> Option(a) {
case lst {
Vide -> None // Pas d'élément
Cons(tete, _) -> Some(tete) // On a un élément !
}
}
Utilisation :
Exemple 2 : dernierElement
// Renvoie le dernier élément d'une liste
pub fn dernierElement(lst: Liste(a)) -> Option(a) {
case lst {
Vide -> None // Liste vide
Cons(tete, Vide) -> Some(tete) // Un seul élément
Cons(_, queue) -> dernierElement(queue) // Continue la recherche
}
}
Utilisation :
dernierElement(Cons(1, Cons(2, Cons(3, Vide))))
// -> Some(3)
dernierElement(Vide)
// -> None
dernierElement(Cons(42, Vide))
// -> Some(42)
Trace d'exécution :
dernierElement [1, 2, 3]
= dernierElement [2, 3] (pas le dernier)
= dernierElement [3] (pas le dernier)
= Some(3) (un seul élément !)
Pattern Matching sur Option
Déconstruire un Option
case monOption {
None -> ... // Cas où il n'y a rien
Some(valeur) -> ... // Cas où on a une valeur
}
Exemple : Afficher un Résultat
import gleam/int
pub fn afficherResultat(resultat: Option(Int)) -> String {
case resultat {
None -> "Aucun résultat"
Some(n) -> "Le résultat est : " <> int.to_string(n)
}
}
Utilisation :
afficherResultat(Some(42))
// -> "Le résultat est : 42"
afficherResultat(None)
// -> "Aucun résultat"
Fonctions Utiles sur Option
option.unwrap : Valeur par Défaut
import gleam/option
// Si None, utilise la valeur par défaut
option.unwrap(Some(5), or: 0)
// -> 5
option.unwrap(None, or: 0)
// -> 0
option.unwrap(Some("Alice"), or: "inconnu")
// -> "Alice"
option.unwrap(None, or: "inconnu")
// -> "inconnu"
Utilisation pratique :
// Obtenir le premier élément ou 0 par défaut
pub fn premierOuZero(lst: Liste(Int)) -> Int {
premier(lst)
|> option.unwrap(or: 0)
}
premierOuZero(Cons(1, Cons(2, Cons(3, Vide))))
// -> 1
premierOuZero(Vide)
// -> 0
option.map : Transformer la Valeur
// Applique une fonction si Some, sinon reste None
option.map(Some(5), fn(x) { x * 2 })
// -> Some(10)
option.map(None, fn(x) { x * 2 })
// -> None
option.map(Some("hello"), string.length)
// -> Some(5)
Cas d'usage :
// Doubler le premier élément
pub fn doublerPremier(lst: Liste(Int)) -> Option(Int) {
premier(lst)
|> option.map(fn(x) { x * 2 })
}
doublerPremier(Cons(5, Cons(10, Cons(15, Vide))))
// -> Some(10)
doublerPremier(Vide)
// -> None
Erreurs Courantes
❌ Oublier le Some
// ERREUR
pub fn premier(lst: Liste(a)) -> Option(a) {
case lst {
Vide -> None
Cons(tete, _) -> tete // Manque Some !
}
}
// ✅ CORRECT
pub fn premier(lst: Liste(a)) -> Option(a) {
case lst {
Vide -> None
Cons(tete, _) -> Some(tete)
}
}
❌ Oublier le Cas None
// ERREUR : Pattern matching incomplet
pub fn utiliser(m: Option(Int)) -> Int {
case m {
Some(x) -> x * 2
// Manque le cas None ! Le compilateur refuse.
}
}
// ✅ CORRECT
pub fn utiliser(m: Option(Int)) -> Int {
case m {
Some(x) -> x * 2
None -> 0
}
}
Chaîner des Option
Problème : Option Imbriqués
// Obtenir le premier élément du dernier élément (liste de listes)
// Version naïve → type bizarre
pub fn getPremierDuDernier(listes: Liste(Liste(a))) -> Option(Option(a)) {
case dernierElement(listes) {
None -> None
Some(derniereListe) -> Some(premier(derniereListe))
}
}
// Résultat : Option(Option(a)) - pas idéal
Solution : option.then
// option.then "aplatit" les Option imbriqués
pub fn getPremierDuDernier(listes: Liste(Liste(a))) -> Option(a) {
dernierElement(listes)
|> option.then(premier)
}
Exemples :
// [[1,2], [3,4], [5,6]] → premier de [5,6]
// -> Some(5)
// [[1,2], [3,4], []] → premier de [] = None
// -> None
// [] → pas de dernier
// -> None
Exemple Complet : minimum
Trouver le Minimum d'une Liste d'Entiers
pub fn minimum(lst: Liste(Int)) -> Option(Int) {
case lst {
Vide -> None
Cons(tete, queue) ->
case minimum(queue) {
None -> Some(tete) // tete est le seul élément
Some(min_queue) ->
case tete < min_queue {
True -> Some(tete) // tete est plus petit
False -> Some(min_queue) // min_queue est plus petit
}
}
}
}
Utilisation :
minimum(Cons(3, Cons(1, Cons(4, Cons(1, Cons(5, Vide))))))
// -> Some(1)
minimum(Cons(42, Vide))
// -> Some(42)
minimum(Vide)
// -> None
Trace d'exécution :
minimum [3, 1, 4]
= case minimum [1, 4] of
Some(1) -> case 3 < 1 of
False -> Some(1)
= Some(1)
minimum [5]
= case minimum [] of
None -> Some(5)
= Some(5)
Exercices
Exercice 1 : elementA
Écris une fonction qui retourne l'élément à la position n (commence à 0).
pub fn elementA(n: Int, lst: Liste(a)) -> Option(a)
// Exemples attendus
elementA(0, [1, 2, 3]) // -> Some(1)
elementA(2, [1, 2, 3]) // -> Some(3)
elementA(5, [1, 2, 3]) // -> None
elementA(-1, [1, 2, 3]) // -> None
Exercice 2 : maximum
Écris une fonction qui retourne le plus grand élément.
pub fn maximum(lst: Liste(Int)) -> Option(Int)
// Exemples attendus
maximum([3, 1, 4, 1, 5]) // -> Some(5)
maximum([]) // -> None
Indice : Similaire à minimum, mais avec > au lieu de <.
Exercice 3 : trouver
Écris une fonction qui retourne le premier élément satisfaisant un prédicat.
pub fn trouver(pred: fn(a) -> Bool, lst: Liste(a)) -> Option(a)
// Exemples attendus
trouver(fn(x) { x > 3 }, [1, 2, 3, 4, 5]) // -> Some(4)
trouver(fn(x) { x > 10 }, [1, 2, 3]) // -> None
Pourquoi Option est Important ?
1. Sécurité à la Compilation
// Le compilateur refuse ce code :
let x = premier(Vide)
let y = x + 1 // ERREUR : x est Option(Int), pas Int !
// ✅ Gleam force à gérer le cas None
let x = premier(Vide)
let y = case x {
Some(n) -> n + 1
None -> 0
}
Pas de crash à l'exécution !
2. Code Explicite
// La signature dit tout
pub fn dernierElement(lst: Liste(a)) -> Option(a)
// ↑
// "Peut ne rien retourner !"
// vs en Python
def last_element(lst): # Peut retourner None... ou crasher ?
...
3. Forcer à Penser aux Cas d'Erreur
// Tu DOIS gérer None - le compilateur le vérifie
case premier(maListe) {
None -> ... // Obligé de penser à ce cas
Some(x) -> ...
}
Option vs Autres Approches
| Approche | Problème |
|---|---|
| Valeur spéciale (-1, "") | Ambiguë - -1 peut être un vrai résultat |
| Exception | Invisible dans la signature, peut crasher |
null/None Python |
Pas vérifié à la compilation |
| Option | Explicite, vérifié, sans surprise ✅ |
Le Pattern Récurrent
pub fn maFonction(entree: ...) -> Option(Resultat) {
case entree {
CasVide -> None // Échec
CasNonVide(données) ->
Some(résultat) // Succès
}
}
Exemples de situations :
- Liste vide →
None - Division par zéro →
None - Clé inexistante dans dictionnaire →
None - Parsing échoué →
None
Résumé
Qu'est-ce que Option ?
None: Absence de valeur (échec, vide, erreur)Some(valeur): Présence d'une valeur
Quand Utiliser Option ?
Utilise Option(a) comme type de retour quand :
- ✅ La fonction peut ne rien retourner
- ✅ L'entrée peut être vide
- ✅ L'opération peut échouer
Fonctions Clés du Module gleam/option
// Valeur par défaut
option.unwrap(opt, or: valeur_par_defaut)
// Transformer la valeur si Some
option.map(opt, fn(x) { ... })
// Chaîner des opérations qui retournent Option
option.then(opt, ma_fonction)
Le Pattern
Conseil Final
Si ta fonction peut "échouer" ou "ne rien retourner", utilise Option !
// ✅ Bon
pub fn premier(lst: Liste(a)) -> Option(a)
// ❌ Mauvais : impossible de représenter l'échec
pub fn premier(lst: Liste(a)) -> a // Et si la liste est vide ???
Option = Sécurité + Clarté + Pas de surprise !
Pour Aller Plus Loin
Une fois que tu maîtrises Option, tu peux découvrir Result : pour les erreurs avec message (Ok(valeur) / Error(raison)).
Mais pour l'instant, Option est déjà très puissant !