Génériques
Génériques
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,
'Tpeut désigner l'un ou l'autre
Généricité implicite
Le compilateur est capable d'inférer qu'une fonction est générique. → Simplifie le code
module ListHelper =
let singleton x = [x]
// val singleton : x:'a -> 'a list
let couple x y = [x; y]
// val couple : x:'a -> y:'a -> 'a list👉 Explications :
singleton:xest juste mis dans une liste générique → son type est donc quelconquecouple: ses 2 argumentsxetydoivent ê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>
Généricité explicite
→ 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 :
Généricité explicite - Forme inline
💡 Astuce : la convention en 'x permet ici d'être + concis :
Généricité explicite - Type
La définition des types génériques est explicite :
Généricité ignorée
Le wildcard _ permet de remplacer un paramètre de type ignoré :
Encore + utile avec type flexible📍Types flexibles :
SRTP
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
SRTP : pourquoi ?
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 :
SRTP : duck typing
On peut utiliser les SRTP pour appeler un membre en même temps que l'on contraint son existence dans le type associé.
Duck typing d'une propriété
💡 Dans vscode avec Ionide, l'IntelliSense est plus friendly :
Duck typing d'une méthode
Déclaration :
💡 IntelliSense dans VsCode + Ionide :
Usages (exemples) :
Duck typing : compléments
💡 Plus d'informations et de conseils sur le duck typing dans l'article ci-dessous duquel sont extraits certains exemples :
Un autre exemple de SRTP est détaillé dans cette réponse sous StackOverflow à propos du type Functor dans la librairie FSharpPlus.
SRTP en F♯ 7.0
Plusieurs améliorations ont été introduites en F# 7.0 pour améliorer la syntaxe des SRTP.
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 :
SRTP : recommandation
Plusieurs améliorations ont été introduites en F# 7.0 pour améliorer la syntaxe des SRTP. Elles rendent le code plus lisible.
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.
Contraintes
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)
Vue d'ensemble
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)
Contraintes de type
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 📍
Contrainte de nullabilité
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.
⚠️ 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.
☝️ 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.defaultofpour tester cette nullité sans perdre en sécurité dans le reste de la codebase :
Contrainte d'enum
(1) La contrainte when 'T : enum<int> permet :
D'éviter la
ArgumentExceptionau runtime (Type provided must be an Enum)Au profit d'une erreur dès la compilation (The type 'ColorUnion' is not an enum)
Contrainte de comparaison
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
compareousort💡
Exemple :
Contrainte de membre explicite
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
Mis à jour
Ce contenu vous a-t-il été utile ?