Génériques
Dernière mise à jour
Cet article vous a-t-il été utile ?
Dernière mise à jour
Cet article vous a-t-il été utile ?
Fonctions et types peuvent être génériques, avec + de flexibilité qu'en C♯.
Par défaut, généricité implicite
Inférée
Voire généralisée, grâce à « généralisation automatique »
Sinon, généricité peut être explicite ou résolue statiquement.
Notations différentes (avant F# 7) :
'T
: paramètre de type générique
^T
: paramètre de type résolu statiquement (SRTP)
Depuis F# 7, 'T
peut désigner l'un ou l'autre
Le compilateur est capable d'inférer qu'une fonction est générique. → Simplifie le code
👉 Explications :
singleton
: x
est juste mis dans une liste générique → son type est donc quelconque
couple
: ses 2 arguments x
et y
doivent être du même type pour pouvoir être dans une liste
☝️ Les noms des types génériques inférés rendent parfois une signature de fonction difficile à comprendre. Ne pas hésiter à ajouter alors des annotations de types plus explicites.
Exemple: Result<'a, 'b>
→ Result<'ok, 'err>
→ Inférence de la généricité de x
et y
👍
❓ Comment indiquer que x
et y
doivent avoir le même type ?
→ Besoin de l'indiquer explicitement :
💡 Astuce : la convention en 'x
permet ici d'être + concis :
La définition des types génériques est explicite :
Le wildcard _
permet de remplacer un paramètre de type ignoré :
Encore + utile avec type flexible📍Types flexibles :
F♯ propose deux catégories de types de paramètre :
'X
: type de paramètre générique comme en C# : le type concret est défini au runtime.
^X
: type de paramètre résolu statiquement : le type concret est défini lors de la compilation.
☝ SRTP : abréviation fréquente de Statically Resolved Type Parameter
Sans SRTP :
→ Inférence du type int
pour x
et y
, sans généralisation (aux float
par ex.) !
Avec SRTP, de pair avec fonction inline
:
On peut utiliser les SRTP pour appeler un membre en même temps que l'on contraint son existence dans le type associé.
💡 Dans vscode avec Ionide, l'IntelliSense est plus friendly :
Déclaration :
💡 IntelliSense dans VsCode + Ionide :
Usages (exemples) :
💡 Plus d'informations et de conseils sur le duck typing dans l'article ci-dessous duquel sont extraits certains exemples :
Jusqu'en F♯ 6.0, il fallait mettre un espace entre le chevron ouvrant et le SRTP.
Depuis F♯ 7.0 (novembre 2022), cela n'est pas nécessaire. Cela permet d'être uniforme avec les types génériques :
Cependant, la syntaxe reste encore un peu difficile à lire, à dessein : pour encourager les solutions alternatives.
De plus, on ne peut pas savoir à l'avance tous les types compatibles avec une fonction avec SRTP.
Enfin, cela peut ralentir beaucoup la compilation. C'est un des problèmes remontés par des utilisateurs de la librairie FSharpPlus.
👉 Les SRTP sont à utiliser avec parcimonie car leur syntaxe est difficile à lire.
Les contraintes sur paramètres de type en F♯ reposent sur le même principe qu'en C♯, avec quelques différences :
Mots clés
when xxx and yyy
where xxx, yyy
Emplacement
Juste après type :
Fin de ligne :
fn (arg: 'T when 'T ...)
Method<T>(arg: T) where T ...
Dans chevrons :
fn<'T when 'T ...> (arg: 'T)
Type de base
'T :> my-base
T : my-base
Type valeur
'T : struct
T : struct
Type référence
'T : not struct
T : class
Type référence nullable
'T : null
T : class?
Constructeur sans param
'T : (new: unit -> 'T)
T : new()
Énumération
'T : enum<my-enum>
T : System.Enum
Comparaison
'T : comparison
≃ T : System.IComparable
Égalité
'T : equality
(pas nécessaire)
Membre explicite
^T : member-signature
(pas d'équivalent)
Pour forcer le type de base : classe mère ou interface
→ Équivalent en C♯ :
💡 Syntaxe alternative : let check condition (error: #System.Exception)
→ Cf. Types flexibles 📍
Exemple de fonction avec une telle contrainte : la fonction Option.ofObj
(Type Option📍) prend en entrée une valeur nullable venant "de l'extérieur" et la convertit en type Option
plus sûr à utiliser.
👉 Le paramètre générique 'a
comporte une contrainte de nullabilité : when 'a: null
.
☝️ Note : cette contrainte ne s'applique pas aux types F♯ (Tuple, Record, Union) qui sont bien des types référence mais qui ne peuvent pas être instanciés null
dans les cas d'usage standard. Cependant, lors d'une interop avec une librairie .NET telle que le micro-ORM Dapper, on peut obtenir une valeur null
à la barbe du compilateur F♯. Pour retomber sur nos pieds, on peut :
Soit décorer le type avec un attribut AllowNullLiteral
, mais on perd alors la sécurité des types non nullables.
Soit utiliser la fonction Unchecked.defaultof
pour tester cette nullité sans perdre en sécurité dans le reste de la codebase :
(1) La contrainte when 'T : enum<int>
permet :
D'éviter la ArgumentException
au runtime (Type provided must be an Enum)
Au profit d'une erreur dès la compilation (The type 'ColorUnion' is not an enum)
Syntaxe : 'T : comparison
Indique que le type 'T
doit :
soit implémenter IComparable
(1)
soit être un collection d'éléments comparables (2)
☝ Notes :
'T : comparison
> 'T : IComparable
❗
'T : comparison
≠ 'T : IComparable<'T>
❗
Pratique pour méthodes génériques compare
ou sort
💡
Exemple :
Pb : Comment indiquer qu'un objet doit disposer d'un certain membre ?
• Manière classique en .NET : typage nominal → Contrainte spécifiant type de base (interface ou classe parent)
• Alternative en F♯ : typage structurel (a.k.a duck-typing des langages dynamiques) → Contrainte de membre explicite → Utilisée avec les SRTP (statically resolved type parameter)
⚖️ Pour et contre :
👍 Permet de rendre code générique pour types hétérogènes
👎 Difficile à lire, à maintenir. Ralentit la compilation
👉 À utiliser dans une librairie, pas pour modéliser un domaine
Un autre exemple de SRTP est détaillé dans cette à propos du type Functor
dans la librairie .
Plusieurs améliorations ont été introduites en pour améliorer la syntaxe des SRTP.
Plusieurs améliorations ont été introduites en pour améliorer la syntaxe des SRTP. Elles rendent le code plus lisible.
Attention : ne pas confondre avec le type System.Nullable<T>
qui est un type valeur alors que la contrainte de nullabilité ne s'applique à qu'un type référence.