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

Immutabilité : let vs mutable

Le scénario

En C#, ceci ne pose aucune question :

int counter = 0;
counter = counter + 1;
counter = counter + 1;

Une variable locale se réaffecte librement, par défaut, sans mot-clé particulier. C'est tellement naturel en C# qu'on ne le remarque même plus.

Essayez maintenant, dans dotnet fsi, l'équivalent qui semble évident :

let counter = 0
counter <- counter + 1

Cela échoue. Pas parce que la syntaxe est fausse, mais parce que let en F# ne crée pas une variable au sens C# du terme : il crée un binding immuable, une association figée entre un nom et une valeur, qui ne peut plus changer une fois établie.

Pourquoi F# a choisi ça par défaut

Dans un langage comme C#, une variable mutable peut être modifiée à tout moment, y compris par du code que vous n'avez pas sous les yeux au moment où vous la lisez — un autre thread, une méthode appelée entre-temps, un effet de bord caché. F# inverse la présomption par défaut : une valeur, une fois liée par let, reste ce qu'elle était à sa création, pour toute la durée où elle est visible. Cela élimine par construction toute une classe de bugs liés à des valeurs partagées qui changent sous les pieds d'un autre bout de code — un problème particulièrement sensible dès que plusieurs threads sont en jeu.

F# n'est pas un langage purement fonctionnel — il autorise la mutation quand elle est nécessaire — mais l'immutabilité y est le choix par défaut, et la mutation doit être demandée explicitement.

Exercice — prédire puis vérifier

Avant de lancer quoi que ce soit, répondez mentalement à cette question : que va afficher fsi si vous tapez ces deux lignes l'une après l'autre ?

let counter = 0;;
counter <- counter + 1;;

Maintenant, vérifiez en fsi. La première ligne réussit normalement :

val counter: int = 0

La seconde échoue à la compilation, avant même de s'exécuter. Le message exact peut légèrement varier selon la version du compilateur, mais son sens est constant : counter n'est pas mutable, donc l'opérateur <- ne peut pas s'appliquer à lui. C'est le compilateur qui bloque ce code, pas une exception qui se produirait à l'exécution — l'erreur est détectée avant même que le programme ne tourne.

Rendre une valeur explicitement mutable

Pour obtenir un comportement de variable réaffectable comparable à celui du C#, il faut le demander avec le mot-clé mutable :

let mutable counter = 0
counter <- counter + 1

Deux différences avec l'exemple C# du début, à bien distinguer :

  • l'opérateur de réaffectation est <-, pas = (en F#, = dans une expression teste l'égalité, il ne réaffecte jamais rien) ;
  • mutable doit être écrit explicitement au moment de la définition — vous ne pouvez pas rendre mutable après coup un let qui ne l'était pas.

Prédisez maintenant le résultat de cette séquence, puis vérifiez-la en fsi :

let mutable counter = 0;;
counter <- counter + 1;;
counter <- counter + 1;;
counter;;

Sortie attendue pour les trois premières lignes (l'affectation elle-même ne produit pas de valeur intéressante, seulement unit, le type F# qui signifie « rien de significatif à retourner ») :

val mutable counter: int = 0
val it: unit = ()
val it: unit = ()

Attention à la première ligne : le mot mutable doit apparaître dans la réponse de fsi, confirmant que counter est bien déclaré mutable — mais sa mise en forme exacte (espacement, ordre des éléments) peut varier légèrement selon la version de dotnet fsi installée sur votre machine. Ne vous inquiétez pas si le texte affiché chez vous diffère un peu caractère pour caractère ; seule la présence du mot mutable compte ici, contrairement aux autres sorties de ce module qui, elles, doivent correspondre exactement.

Et pour la dernière ligne, qui relit simplement la valeur actuelle de counter :

val it: int = 2

Ce que cela change dans la façon d'écrire du code

En C#, modifier une valeur est l'outil par défaut pour faire progresser un calcul — une boucle for qui incrémente un compteur, un accumulateur qu'on met à jour à chaque itération. En F#, l'habitude qui domine est différente : plutôt que de modifier une valeur existante, on produit une nouvelle valeur à partir de l'ancienne. mutable reste disponible et légitime quand la mutation reflète vraiment le problème à résoudre — mais ce n'est plus le réflexe de départ, ce sera un choix conscient et localisé.

Ce que vous venez de faire

Vous avez constaté qu'un let ordinaire refuse toute réaffectation via <-, compris que ce refus est une décision de conception délibérée de F# et non une limitation accidentelle, puis observé comment mutable réintroduit ce comportement de manière explicite quand vous en avez réellement besoin.

Au module suivant, vous quittez la question de la mutation pour vous intéresser à un autre outil quotidien : comment enchaîner et construire des fonctions avec les opérateurs |> et >>, pour remplacer les appels imbriqués auxquels le C# vous a habitué.

Vérifiez votre compréhension

Dans dotnet fsi, vous tapez `let counter = 0;;` puis `counter <- counter + 1;;`. Que se passe-t-il pour la seconde ligne ?

Quelle est la vraie raison pour laquelle F# rend une valeur immuable par défaut, plutôt qu'un simple choix de syntaxe arbitraire ?

Vous avez écrit let mutable counter = 0. Un collègue veut ensuite rendre une autre valeur, let total = 100 (déjà définie plus haut sans mutable), réaffectable avec <-. Est-ce possible sans redéfinir total ?

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.