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, 'T peut 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 : 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>

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 :

Contrainte
Syntaxe F♯
Syntaxe C♯

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

Contrainte
Syntaxe F♯
Syntaxe C♯

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.defaultof pour 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 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)

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 :

  1. 'T : comparison > 'T : IComparable

  2. 'T : comparison'T : IComparable<'T>

  3. Pratique pour méthodes génériques compare ou sort 💡

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

Last updated

Was this helpful?