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

Let bindings et inférence de type

Le réflexe à désapprendre

En C#, déclarer une valeur suppose presque toujours d'écrire ou de laisser deviner un type explicite : int total = 42;, ou var total = 42; si vous laissez le compilateur l'inférer localement — mais var reste une exception ponctuelle, réservée aux initialisations locales, et une méthode garde toujours une signature entièrement typée : int Add(int a, int b) { return a + b; }.

En F#, il n'y a qu'un seul mot-clé pour lier un nom à quelque chose, que ce soit une valeur ou une fonction : let. Et l'inférence de type n'est pas une exception locale, c'est le mode de fonctionnement par défaut, y compris pour les paramètres et le type de retour d'une fonction entière.

let pour une valeur

let total = 42

Comparé à int total = 42; en C#, la différence saute aux yeux : aucun type écrit. Le compilateur F# regarde le littéral 42, en déduit qu'il s'agit d'un int, et fige le type de total en conséquence — de façon aussi stricte que si vous l'aviez écrit vous-même. F# est un langage à typage statique : ce n'est pas parce que le type n'est pas visible dans le code qu'il n'existe pas ou qu'il pourrait changer plus tard.

Si vous lancez dotnet fsi et tapez :

let total = 42;;

fsi confirme le type déduit :

val total: int = 42

let pour une fonction

La même construction sert à définir une fonction — il n'existe pas de mot-clé séparé comme void ou un type de retour à écrire avant le nom :

let add a b = a + b

En fsi :

let add a b = a + b;;

Sortie attendue :

val add: a: int -> b: int -> int

Ce signe -> répété n'est pas une coquille : add est en réalité une fonction à un seul paramètre (a) qui renvoie une autre fonction à un seul paramètre (b), qui renvoie enfin un int. C'est la conséquence du currying, une mécanique que ce parcours n'a pas besoin de creuser maintenant — retenez simplement que add a b s'utilise exactement comme une méthode C# à deux paramètres : add 2 3 renvoie 5, aussi naturellement que Add(2, 3).

Comparez avec l'équivalent C#, où le type de chaque paramètre et le type de retour doivent être écrits explicitement dans la signature :

int Add(int a, int b) => a + b;

D'où vient le type déduit : le corps de la fonction

Le compilateur ne devine pas au hasard : pour add a b = a + b, l'opérateur + appliqué à a et b n'a de sens ici qu'entre deux nombres, et en l'absence d'autre indice, F# choisit int par défaut. Le type de retour, lui, est toujours déterminé par la dernière expression évaluée dans le corps de la fonction — ici, a + b, donc int.

Cela veut dire que le type de retour d'une fonction F# n'a jamais besoin d'être annoncé à l'avance comme en C# : il découle mécaniquement de ce que la fonction calcule pour finir.

Annoter un type explicitement quand c'est utile

L'inférence n'interdit pas d'écrire un type — elle le rend simplement optionnel. Les deux syntaxes suivantes sont valides et strictement équivalentes pour le compilateur :

let increment x = x + 1
let increment (x: int) : int = x + 1

Un cas où l'annotation devient nécessaire : quand le corps de la fonction utilise un membre d'un type précis, comme une méthode .NET, et qu'il n'y a rien d'autre dans le code pour indiquer de quel type il s'agit :

let toLower (s: string) = s.ToLower()

Sans le : string, le compilateur n'aurait aucun moyen de savoir que s possède une méthode ToLower — l'annotation lève l'ambiguïté, exactement comme un type de paramètre explicite le ferait en C#.

Une conséquence utile : la généralisation automatique

Quand rien dans le corps d'une fonction n'impose un type précis, F# rend la fonction générique tout seul, sans que vous ayez à écrire <T> nulle part :

let makeTuple a b = (a, b)

En fsi :

val makeTuple: a: 'a -> b: 'b -> 'a * 'b

'a et 'b sont des paramètres de type génériques (l'équivalent F# du T d'une méthode générique C#). makeTuple fonctionne donc avec des entiers, des chaînes, ou n'importe quel mélange des deux, sans version supplémentaire à écrire — un comportement que vous obtiendriez en C# seulement en écrivant explicitement (T1, T2) MakeTuple<T1, T2>(T1 a, T2 b).

Exercice — écrire et exécuter des let bindings

Ouvrez dotnet fsi et exécutez, une ligne à la fois, chacune de ces trois définitions :

let price = 19.99;;
let greeting name = "Bonjour, " + name;;
let double x = x * 2;;

Sorties attendues, dans l'ordre :

val price: float = 19.99
val greeting: name: string -> string
val double: x: int -> int

Piège classique pour un développeur C# : le float F# n'est pas le float C#. price est ici un nombre à virgule flottante 64 bits, exactement ce que C# appelle double — c'est le type que F# choisit par défaut pour un littéral décimal comme 19.99. Le vrai équivalent du float C# (32 bits) s'appelle float32 en F#, et il faut l'écrire explicitement (19.99f) pour l'obtenir.

Vérifiez ensuite ces déductions en appelant chaque fonction :

greeting "Alice";;
double 21;;

Sorties attendues :

val it: string = "Bonjour, Alice"
val it: int = 42

Si vos types déduits diffèrent de ceux ci-dessus, relisez le corps de la fonction concernée : le type affiché découle toujours directement de ce que fait cette dernière expression, jamais d'une convention arbitraire.

Ce que vous venez de faire

Vous avez utilisé let pour lier des valeurs et des fonctions, observé comment F# déduit systématiquement leurs types à partir du corps de l'expression plutôt que d'une déclaration explicite, et constaté qu'une annotation de type reste possible — et parfois nécessaire — quand le compilateur n'a pas assez d'information pour trancher seul.

Au module suivant, vous allez tester une limite de ce que let vous permet de faire : essayer de réaffecter une valeur déjà liée, et comprendre pourquoi F# refuse ce que C# autorise par défaut.

Vérifiez votre compréhension

Sans l'exécuter, quel type fsi va-t-il afficher pour `let greeting name = "Bonjour, " + name` ?

Pourquoi `let toLower (s: string) = s.ToLower()` a besoin d'une annotation explicite, alors que `let double x = x * 2` s'en passe très bien ?

`let makeTuple a b = (a, b)` est déclarée sans aucune annotation. En quoi son type inféré, 'a -> 'b -> 'a * 'b, diffère-t-il de ce que ferait C# pour obtenir un comportement é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.