Pattern Matching en Rust : gérer la complexité
Définition
Un pattern matching est un pattern de programmation hyper puissant qui va nous permettre d’économiser beaucoup de lignes de code et surtout rendre le code compréhensible !
Pour te montrer comment ça fonctionne on va prendre un exemple avec le jeu Mario.
Dans un jeu Mario, Mario va changer d’état en fonction de :
- son état actuel
- et de ce qu’il va prendre comme objet (champignon, fleur, plumes…)
On peut voir sur ce schéma les différents états de Mario :
- Si Mario est petit et prend un champignon => Il devient SuperMario
- Si Mario est petit et prend une plume => Il devient CapeMario
- etc
On va donc d’abord créer un enum avec les différents états de mario :
Puis même chose avec les différents objets :
Écrire tout ça avec des if
, ce serait vraiment long et assez obscure, ça donnerait quelque chose comme ça :
Il existe une façon BIEN plus belle d’écrire tout ça, les pattern matching :
Voilà comment se lit un pattern matching :
- si
(current_state, item)
“match”, c’est-à-dire vaut :(MarioState::Mario, Item::Mushroom)
, le match renvoie la valeurMarioState::SuperMario
- si
(current_state, item)
“match”(MarioState::Mario, Item::Flower)
, le match renvoie la valeurMarioState::FireMario
- etc
Comme match
est une expression qui retourne une valeur cela devient la valeur de retour de la fonction.
Petite précision très importante, il faut absolument couvrir tous les cas de figures possibles de notre pattern matching, car si Rust rencontre une combinaison qu’on n’a pas couverte, il sera incapable de déterminer la valeur du match…
Maintenant on va voir comment écrire tous les états possibles de Mario avec un pattern matching ;)
Pour ça on va partir de chaque état de Mario (Mario, SuperMario, FireMario, CapMario) et on va écrire ce qu’il devient pour chaque item (Fleur, Champignon, plume)
Le code est bien plus clair que si on avait dû le faire avec des if, mais on peut encore l’améliorer !
Pour cela on va juste regrouper ensemble les différentes conditions qui donnent le même résultat, grâce au OU logique qu’on va écrire à l’aide du caractère |
(et non pas ||
comme dans les if), ça donne le résultat suivant :
Et là on se rend compte une nouvelle fois qu’on peut simplifier ! On voit que peu importe l’état de mario, s’il prend une fleur il devient FireMario, même chose pour la plume qui le transforme en CapeMario, on peut écrire ça sous cette forme :
Le underscore signifie “peu importe la valeur”, donc (_, Item::Flower)
signifie, “peu importe la valeur de current_state
si item
vaut Item::Flower
”
Le code est très élégant avec aucune répétition !
On va pouvoir ajouter une dernière petite touche pour éviter de répéter Item::
pour chaque item et Mario::
pour chaque état de mario, pour ça on va ajouter use MarioState::*;
et use Item::*;
.
Et voilà je crois qu’on ne peut pas faire mieux ! Tu vois à quel point les pattern matching nous permettent d’écrire du code complexe très facilement ?
C’est une des fonctionnalités qui font que Rust est très apprécié des devs.
Règles du pattern matching
Lorsqu’on utilise un pattern matching il y a quelques règles à respecter,
match est une expression
Le mot clé match
est une expression, autrement dit il renvoie une valeur, et donc si on ne fait pas suivre de ;
après un match
, la valeur retournée par le match sera celui de la fonction actuelle.
Les branches peuvent tenir sur plusieurs lignes
Jusqu’à maintenant on a seulement vu les cas ou les branches du pattern matching tiennent sur une seule ligne, mais il est possible d’utiliser plusieurs lignes, pour ça on va utiliser les accolades :
Ne pas en abuser du pattern matching
Comme toutes les bonnes choses, il ne faut en abuser, il y a certains cas de figure ou on préférera un simple if
.
C’est surtout le cas lorsqu’on veut tester une condition booléenne, même si on peut très bien le faire avec un match.
On utilisera surtout le match lorsqu’on veut tester des conditions plus complexes qu’une simple condition booléenne.
Pour te montrer la différence voici ce qui se passe avec un match
Avec un if on aurait fait la même chose de cette façon :
Le if
est plus pertinent ici puisqu’en lisant le code c’est beaucoup plus clair de se dire : “si age est supérieur ou égal à 18 alors retourne vrai”, plutôt que “si age >= 18 match true”.
D’ailleurs on peut même faire sans if
en écrivant juste :
Comme ça on renvoie simplement si l’égalité est vraie ou non et même pas besoin de if
!
Vérifie bien à l’avance si tu ne peux pas écrire tes conditions aussi simplement avant d’utiliser un if
ou bien même un match
.
Chaque branche doit retourner une valeur de même type
Imaginons qu’on utilise un pattern matching pour vérifier la valeur d’un montant, et si le montant est zéro le match retourne une String, sinon il retourne le montant, c’est-à-dire un u8
Eh bien ça, c’est interdit ! Puisque Rust a besoin de connaître le type qui sera retourné par le match et donc un pattern matching doit retourner le même type dans chacune de ses branches.
Sinon, ça ne compile pas.
pattern matching & Option
Parfois on doit utiliser un pattern matching sur une valeur de type Option
, voilà comment on va faire ça :
Imaginons qu’on veuille faire des choses plus complexes avec le i
voilà comment on s’y prendrait :