Aller au contenu principal
Nicolas Cousin Tech SolutionsNicolas Cousin Tech Solutions
Module 5 sur 7

Pattern matching

Le scénario

Une grille tarifaire simple, en C#, avec un switch sur une plage de quantités :

string TarifPour(int quantite)
{
    switch (quantite)
    {
        case 0:
            return "Aucune commande";
        case 1:
        case 2:
        case 3:
            return "Tarif unitaire";
        default:
            return quantite > 10 ? "Tarif de gros" : "Tarif standard";
    }
}

Deux mécanismes se superposent ici : le switch pour les cas exacts, et un ?: en secours pour la logique de plage restante dans le default. F# unifie les deux dans une seule construction : l'expression match.

(Le C# moderne, depuis C# 8, propose aussi une expression switch plus proche de match — mais la forme statement ci-dessus reste très répandue dans du code existant, c'est pourquoi elle sert de point de départ ici. Vous recroiserez l'expression switch plus loin dans ce module.)

L'expression match

let tarifPour quantite =
    match quantite with
    | 0 -> "Aucune commande"
    | 1 | 2 | 3 -> "Tarif unitaire"
    | q when q > 10 -> "Tarif de gros"
    | _ -> "Tarif standard"

Trois choses à noter par rapport au switch C# :

  • 1 | 2 | 3 -> regroupe plusieurs valeurs sur un seul cas, comme les case empilés en C#, mais sur une seule ligne.
  • q when q > 10 -> est une garde : q capture la valeur testée dans une variable, et la clause when n'est évaluée que si aucun cas précédent n'a matché. C'est ce qui remplace le ?: planqué dans le default — sans qu'il faille sortir de la construction match pour l'exprimer.
  • _ est le joker : il matche tout ce qui n'a été capté par aucun cas précédent, comme le default d'un switch. Sans lui (et sans que tous les cas possibles soient couverts autrement), le compilateur émet un avertissement de correspondance incomplète.

match est une expression, pas une instruction : elle produit une valeur, tout comme if...then...else en produit une en F#. Il n'y a jamais besoin d'un return séparé dans chaque branche : la valeur du cas qui a matché devient directement la valeur de toute l'expression match.

Faire correspondre un tuple

Un des cas les plus utiles de match porte sur les tuples :

let detectZero point =
    match point with
    | (0, 0) -> "Les deux valeurs sont à zéro."
    | (0, y) -> $"La première valeur est zéro, la seconde vaut {y}."
    | (x, 0) -> $"La seconde valeur est zéro, la première vaut {x}."
    | _ -> "Aucune des deux valeurs n'est zéro."

Chaque motif (0, 0), (0, y), (x, 0) compare la forme du tuple reçu à un patron : 0 est un littéral qui doit correspondre exactement, tandis que y ou x sont des variables qui capturent la valeur présente à cette position, quelle qu'elle soit. C'est du filtrage et de la décomposition en une seule étape.

Le C# moderne (depuis C# 8) sait faire la même chose avec l'expression switch annoncée plus haut, en utilisant un positional pattern directement sur le tuple :

static string DetectZero((int, int) point) => point switch
{
    (0, 0) => "Les deux valeurs sont à zéro.",
    (0, var y) => $"La première valeur est zéro, la seconde vaut {y}.",
    (var x, 0) => $"La seconde valeur est zéro, la première vaut {x}.",
    _ => "Aucune des deux valeurs n'est zéro.",
};

Les deux versions se lisent presque ligne à ligne : ce n'est donc pas une capacité totalement absente du C#, contrairement à ce que laisserait penser le seul switch statement de la section précédente. Trois différences subsistent, plus fines qu'une impossibilité :

  • en C#, chaque variable capturée doit être préfixée par var (var y, var x) ; en F#, le nom seul suffit ;
  • match est toujours une expression en F# — il n'existe qu'une seule forme à connaître, là où C# distingue switch statement et switch expression ;
  • en F#, le compilateur avertit par défaut dès qu'un match ne couvre pas tous les cas possibles ; en C#, cet avertissement n'existe que pour l'expression switch, pas pour le switch statement plus ancien vu en début de module.

Exercice — écrire votre propre match sur tuple

À vous d'écrire, dans dotnet fsi, une fonction comparerPaire qui prend un tuple (a, b) de deux entiers et renvoie :

  • "Égales" si a et b sont égaux ;
  • "La première est plus grande" si a > b ;
  • "La seconde est plus grande" sinon.

Utilisez match avec des gardes (when), sur le même principe que detectZero ci-dessus — mais cette fois, décomposez et comparez vous-même les deux éléments du tuple, sans regarder de correction avant d'avoir essayé.

Testez avec :

comparerPaire (3, 3);;
comparerPaire (5, 2);;
comparerPaire (1, 9);;

Sorties attendues, dans l'ordre :

val it: string = "Égales"
val it: string = "La première est plus grande"
val it: string = "La seconde est plus grande"

Si vous bloquez : le premier cas à tester doit être l'égalité ((a, b) when a = b), sinon a > b matchera en premier même quand a et b sont égaux — le même piège d'ordre existe pour n'importe quelle liste de gardes, comme vous le reverrez dans l'exercice suivant.

Contraste avec if/then/else

if...then...else reste parfaitement valide en F# pour une condition binaire simple :

let estPositif x =
    if x >= 0 then "positif ou nul"
    else "négatif"

match devient préférable dès que vous avez plus de deux branches, ou que vous décomposez une valeur structurée comme un tuple : la logique équivalente en if/elif/else imbriqués perd rapidement en lisibilité, et ne décompose pas (x, y) en variables nommées aussi directement qu'un motif de tuple le fait.

Exercice — convertir un if/else C# en match F#

Voici une méthode C# qui classe une note sur 20 :

string Mention(int note)
{
    if (note < 10)
        return "Insuffisant";
    else if (note < 12)
        return "Passable";
    else if (note < 16)
        return "Bien";
    else
        return "Très bien";
}

Écrivez, dans dotnet fsi, une fonction mention équivalente en F#, en utilisant match avec des gardes (when) plutôt que des bornes littérales exactes — la valeur n'étant pas une constante fixe mais une plage, vous ne pouvez pas la filtrer avec un simple littéral comme | 10 ->.

Testez-la avec ces quatre appels :

mention 8;;
mention 11;;
mention 14;;
mention 18;;

Sorties attendues, dans l'ordre :

val it: string = "Insuffisant"
val it: string = "Passable"
val it: string = "Bien"
val it: string = "Très bien"

Si mention 11 renvoie "Insuffisant" au lieu de "Passable", c'est que vos gardes sont mal ordonnées ou mal bornées : match évalue les cas dans l'ordre où ils sont écrits et s'arrête au premier qui correspond, exactement comme les case d'un switch ou les branches d'un if/elif s'évaluent de haut en bas.

Ce que vous venez de faire

Vous avez remplacé un if/else C# par une expression match F#, découvert comment les gardes (when) couvrent les cas qu'un littéral seul ne peut pas exprimer, et vu comment match décompose un tuple en une seule étape — une capacité que le C# moderne offre aussi via l'expression switch, mais avec un peu plus de cérémonie (var devant chaque variable capturée, et une distinction statement/expression que F# n'a pas).

Au module suivant, vous découvrez un nouveau type de donnée, le record F#, et la question qu'il pose immédiatement : que veut dire « deux records sont égaux », et en quoi cela diffère fondamentalement de l'égalité par défaut d'une classe C#.

Vérifiez votre compréhension

Avec detectZero point = match point with | (0, 0) -> ... | (0, y) -> ... | (x, 0) -> ... | _ -> ..., quel texte est renvoyé par detectZero (0, 7) ?

Si vous écrivez match n with | _ -> "Autre" | n when n < 10 -> "Petit", que se passe-t-il ?

Le C# moderne (switch expression, depuis C# 8) peut aussi décomposer un tuple avec (0, var y) => .... Quelle différence reste-t-il malgré tout avec le match F# équivalent ?

Envie d'être prévenu des prochains modules ?

L'Académie reste gratuite et en accès libre, sans inscription. Si vous voulez juste être averti par email à la sortie d'un nouveau module, c'est ici — aucune obligation, désinscription en un clic.