Instructions de conception des composants F#
Ce document est un ensemble d’instructions de conception de composants pour la programmation F#, basée sur les instructions de conception de composants F#, v14, Microsoft Research et une version qui a été initialement organisée et gérée par la F# Software Foundation.
Ce document part du principe que vous connaissez la programmation F#. Merci beaucoup à la communauté F# pour leurs contributions et commentaires utiles sur différentes versions de ce guide.
Aperçu
Ce document examine certains des problèmes liés à la conception et au codage des composants F#. Un composant peut signifier l’un des éléments suivants :
- Une couche dans votre projet F# avec des consommateurs externes dans ce projet.
- Bibliothèque destinée à être consommée par le code F# au-delà des limites d’assembly.
- Bibliothèque destinée à être consommée par n’importe quel langage .NET au-delà des limites d’assembly.
- Bibliothèque destinée à la distribution via un référentiel de packages, par exemple NuGet.
Les techniques décrites dans cet article suivent les Cinq principes du bon code F#, et utilisent ainsi la programmation fonctionnelle et d’objet selon les besoins.
Quelle que soit la méthodologie, le concepteur de composants et de bibliothèques rencontre un certain nombre de problèmes pratiques et prosaïques lorsque vous essayez de créer une API qui est plus facilement utilisable par les développeurs. L’application consciencieux des instructions de conception de la bibliothèque .NET vous guidera vers la création d’un ensemble cohérent d’API qui sont agréables à consommer.
Instructions générales
Il existe quelques recommandations universelles qui s’appliquent aux bibliothèques F#, quelle que soit l’audience prévue pour la bibliothèque.
Découvrez les instructions de conception de la bibliothèque .NET
Quel que soit le type de codage F# que vous effectuez, il est utile d’avoir une connaissance pratique des instructions de conception bibliothèque .NET. La plupart des autres programmeurs F# et .NET seront familiarisés avec ces instructions et attendent que le code .NET soit conforme à ces instructions.
Les instructions de conception de la bibliothèque .NET fournissent des conseils généraux sur l’affectation de noms, la conception de classes et d’interfaces, la conception des membres (propriétés, méthodes, événements, etc.) et sont un point de référence utile pour une variété d’instructions de conception.
Ajouter des commentaires de documentation XML à votre code
La documentation XML sur les API publiques garantit que les utilisateurs peuvent bénéficier d’une excellente fonctionnalité IntelliSense et Quickinfo lors de l'utilisation de ces types et membres, et permet de créer des fichiers de documentation pour la bibliothèque. Consultez la documentation XML sur les différentes balises xml qui peuvent être utilisées pour des marques de balisage supplémentaires dans les commentaires xmldoc.
/// A class for representing (x,y) coordinates
type Point =
/// Computes the distance between this point and another
member DistanceTo: otherPoint:Point -> float
Vous pouvez utiliser les commentaires XML de forme courte (/// comment
) ou les commentaires XML standard (///<summary>comment</summary>
).
Envisagez d’utiliser des fichiers de signature explicites (.fsi) pour les API de bibliothèque et de composant stables
L’utilisation de fichiers de signatures explicites dans une bibliothèque F# fournit un résumé succinct de l’API publique, ce qui vous permet de vous assurer que vous connaissez la surface publique complète de votre bibliothèque et fournit une séparation claire entre la documentation publique et les détails de l’implémentation interne. Les fichiers de signature ajoutent des frictions à la modification de l’API publique, en exigeant que des modifications soient apportées dans les fichiers d’implémentation et de signature. Par conséquent, les fichiers de signature doivent généralement être introduits uniquement lorsqu’une API est devenue solidifiée et n’est plus censée changer de manière significative.
Suivez les bonnes pratiques pour l’utilisation de chaînes dans .NET
Suivez les recommandations Bonnes pratiques pour l’utilisation de chaînes dans .NET lorsque l’étendue du projet le justifie. En particulier, déclarez explicitement une intention culturelle dans la conversion et la comparaison des chaînes (le cas échéant).
Directives pour les bibliothèques conçues pour F#
Cette section présente des recommandations pour le développement de bibliothèques publiques F# ; autrement dit, les bibliothèques exposant les API publiques destinées à être consommées par les développeurs F#. Il existe diverses recommandations de conception de bibliothèque applicables spécifiquement à F#. En l’absence des recommandations spécifiques qui suivent, les instructions de conception de la bibliothèque .NET sont les instructions de secours.
Conventions d’affectation de noms
Utiliser des conventions d’affectation de noms et de mise en majuscules .NET
Le tableau suivant suit les conventions d’affectation de noms et de mise en majuscules .NET. Quelques ajouts ont été effectués pour inclure également les constructions F#. Ces recommandations sont particulièrement destinées aux API qui dépassent les limites entre F# et F#, conformes aux idiomes de .NET BCL et à la majorité des bibliothèques.
Construire | Incident | Partie | Exemples | Notes |
---|---|---|---|---|
Types concrets | PascalCase | Nom/adjectif | List, Double, Complex | Les types concrets sont des structures, des classes, des énumérations, des délégués, des enregistrements et des unions. Bien que les noms de types soient traditionnellement en minuscules dans OCaml, F# a adopté le schéma d’affectation de noms .NET pour les types. |
DLL | PascalCase | Fabrikam.Core.dll | ||
Balises d’union | PascalCase | Nom | Some, Add, Success | N’utilisez pas de préfixe dans les API publiques. Si vous le souhaitez, utilisez un préfixe lorsqu’il est interne, par exemple « type Teams = TAlpha | TBeta | TDelta". |
Événement | PascalCase | Verbe | ValueChanged / ValueChanging | |
Exceptions | PascalCase | WebException | Le nom doit se terminer par « Exception ». | |
Champ | PascalCase | Nom | NomActuel | |
Types d’interface | PascalCase | Nom/adjectif | IDisposable | Le nom doit commencer par « I ». |
Méthode | PascalCase | Verbe | ToString | |
Namespace | PascalCase | Microsoft.FSharp.Core | Utilisez généralement <Organization>.<Technology>[.<Subnamespace>] , mais supprimez l’organisation si la technologie est indépendante de l’organisation. |
|
Paramètres | camelCase | Nom | typeName, transform, range | |
Valeurs let (internes) | camelCase ou PascalCase | Nom/verbe | getValue, myTable | |
Valeurs let (externes) | camelCase ou PascalCase | Nom/verbe | List.map, Dates.Today | Les valeurs liées à let sont souvent publiques quand vous suivez des modèles de conception fonctionnelle traditionnels. Toutefois, utilisez généralement PascalCase lorsque l’identificateur peut être utilisé à partir d’autres langages .NET. |
Propriété | PascalCase | Nom/adjectif | IsEndOfFile, BackColor | Les propriétés booléennes utilisent généralement Is et Can et doivent être affirmatives, comme dans IsEndOfFile, et non IsNotEndOfFile. |
Éviter les abréviations
Les instructions .NET découragent l’utilisation d’abréviations (par exemple, « utiliser OnButtonClick
plutôt que OnBtnClick
»). Les abréviations courantes, telles que Async
pour « Asynchrone », sont tolérées. Cette directive est parfois ignorée pour la programmation fonctionnelle ; par exemple, List.iter
utilise une abréviation pour « itérer ». Pour cette raison, l’utilisation d’abréviations a tendance à être tolérée à un degré plus élevé dans la programmation F#-à-F#, mais doit toujours être évitée dans la conception des composants publics.
Éviter les collisions de noms en raison de la casse
Les instructions .NET indiquent que la casse ne doit pas être le seul critère de différenciation entre noms, car certains langages clients comme Visual Basic ne respectent pas la casse.
Utiliser des acronymes le cas échéant
Les acronymes tels que XML ne sont pas des abréviations et sont largement utilisés dans les bibliothèques .NET sous forme non capitalisée (Xml). Seuls les acronymes connus et largement reconnus doivent être utilisés.
Utiliser PascalCase pour les noms de paramètres génériques
Utilisez PascalCase pour les noms de paramètres génériques dans les API publiques, notamment pour les bibliothèques F#. En particulier, utilisez des noms tels que T
, U
, T1
, T2
pour les paramètres génériques arbitraires, et lorsque des noms spécifiques sont logiques, puis pour les bibliothèques F#, utilisez des noms tels que Key
, Value
, Arg
(mais pas par exemple, TKey
).
Utiliser PascalCase ou camelCase pour les fonctions publiques et les valeurs dans les modules F#
CamelCase est utilisé pour les fonctions publiques conçues pour être utilisées non qualifiées (par exemple, invalidArg
) et pour les « fonctions de collection standard » (par exemple, List.map). Dans ces deux cas, les noms de fonction agissent beaucoup comme des mots clés dans la langue.
Conception d’objet, de type et de module
Utiliser des espaces de noms ou des modules pour contenir vos types et modules
Chaque fichier F# d’un composant doit commencer par une déclaration d’espace de noms ou une déclaration de module.
namespace Fabrikam.BasicOperationsAndTypes
type ObjectType1() =
...
type ObjectType2() =
...
module CommonOperations =
...
or
module Fabrikam.BasicOperationsAndTypes
type ObjectType1() =
...
type ObjectType2() =
...
module CommonOperations =
...
Les différences entre l’utilisation de modules et d’espaces de noms pour organiser le code au niveau supérieur sont les suivantes :
- Les espaces de noms peuvent s’étendre sur plusieurs fichiers
- Les espaces de noms ne peuvent pas contenir de fonctions F#, sauf si elles se trouvent dans un module interne
- Le code d’un module donné doit être contenu dans un seul fichier
- Les modules de niveau supérieur peuvent contenir des fonctions F# sans avoir besoin d’un module interne
Le choix entre un espace de noms de niveau supérieur ou un module affecte la forme compilée du code et, par conséquent, influencera la manière dont d'autres langages .NET perçoivent votre API, si elle est consommée en dehors du code F#.
Utiliser des méthodes et des propriétés pour les opérations intrinsèques aux types d’objets
Lors de l’utilisation d’objets, il est préférable de s’assurer que les fonctionnalités consommables sont implémentées en tant que méthodes et propriétés sur ce type.
type HardwareDevice() =
member this.ID = ...
member this.SupportedProtocols = ...
type HashTable<'Key,'Value>(comparer: IEqualityComparer<'Key>) =
member this.Add(key, value) = ...
member this.ContainsKey(key) = ...
member this.ContainsValue(value) = ...
La majeure partie des fonctionnalités d’un membre donné n’a pas nécessairement besoin d’être implémentée dans ce membre, mais l’élément consommable de cette fonctionnalité doit être.
Utiliser des classes pour encapsuler l’état mutable
Dans F#, cette opération doit être effectuée uniquement lorsque cet état n’est pas déjà encapsulé par une autre construction de langage, telle qu’une fermeture, une expression de séquence ou un calcul asynchrone.
type Counter() =
// let-bound values are private in classes.
let mutable count = 0
member this.Next() =
count <- count + 1
count
Utiliser des interfaces pour regrouper les opérations associées
Utilisez des types d’interface pour représenter un ensemble d’opérations. Cette option l’emporte sur d’autres, notamment les tuples de fonctions ou les enregistrements de fonctions.
type Serializer =
abstract Serialize<'T> : preserveRefEq: bool -> value: 'T -> string
abstract Deserialize<'T> : preserveRefEq: bool -> pickle: string -> 'T
En préférence pour :
type Serializer<'T> = {
Serialize: bool -> 'T -> string
Deserialize: bool -> string -> 'T
}
Les interfaces sont des concepts de première classe dans .NET, que vous pouvez utiliser pour obtenir ce que les functors vous donnent normalement. En outre, ils peuvent être utilisés pour encoder des types existentiels dans votre programme, ce que les structures de fonctions ne peuvent pas faire.
Utiliser un module pour regrouper des fonctions qui agissent sur des collections
Lorsque vous définissez un type de collection, envisagez de fournir un ensemble standard d’opérations comme CollectionType.map
et CollectionType.iter
) pour les nouveaux types de collection.
module CollectionType =
let map f c =
...
let iter f c =
...
Si vous incluez un tel module, suivez les conventions d’affectation de noms standard pour les fonctions trouvées dans FSharp.Core.
Utiliser un module pour regrouper des fonctions pour les fonctions courantes, canoniques, en particulier dans les bibliothèques mathématiques et DSL
Par exemple, Microsoft.FSharp.Core.Operators
est une collection ouverte automatiquement de fonctions de niveau supérieur (comme abs
et sin
) fournies par FSharp.Core.dll.
De même, une bibliothèque de statistiques peut inclure un module avec des fonctions erf
et erfc
, où ce module est conçu pour être ouvert explicitement ou automatiquement.
Envisagez d’utiliser RequireQualifiedAccess et appliquez soigneusement les attributs AutoOpen
L’ajout de l’attribut [<RequireQualifiedAccess>]
à un module indique que le module peut ne pas être ouvert et que les références aux éléments du module nécessitent un accès qualifié explicite. Par exemple, le module Microsoft.FSharp.Collections.List
a cet attribut.
Cela est utile lorsque les fonctions et les valeurs du module ont des noms susceptibles de entrer en conflit avec des noms dans d’autres modules. L’exigence d’accès qualifié peut augmenter considérablement la maintenabilité à long terme et l’évocabilité d’une bibliothèque.
Il est fortement suggéré d’avoir l’attribut [<RequireQualifiedAccess>]
pour les modules personnalisés qui étendent ceux fournis par FSharp.Core
(tels que Seq
, List
, Array
), car ces modules sont couramment utilisés dans le code F# et ont [<RequireQualifiedAccess>]
définis sur eux ; plus généralement, il est déconseillé de définir des modules personnalisés qui n’ont pas l’attribut, lorsque ces ombres de module ou étendent d’autres modules qui ont l’attribut.
L’ajout de l’attribut [<AutoOpen>]
à un module signifie que le module est ouvert lorsque l’espace de noms conteneur est ouvert. L’attribut [<AutoOpen>]
peut également être appliqué à un assembly pour indiquer un module ouvert automatiquement lorsque l’assembly est référencé.
Par exemple, une bibliothèque de statistiques MathsHeaven.Statistics peut contenir un module MathsHeaven.Statistics.Operators
contenant des fonctions erf
et erfc
. Il est raisonnable de marquer ce module comme [<AutoOpen>]
. Cela signifie que open MathsHeaven.Statistics
ouvrira également ce module et placera les noms erf
et erfc
dans la portée. Une autre bonne utilisation de [<AutoOpen>]
concerne les modules contenant des méthodes d’extension.
L’utilisation excessive de [<AutoOpen>]
conduit à des espaces de noms pollués, et l’attribut doit être utilisé avec soin. Pour des bibliothèques spécifiques dans des domaines spécifiques, l’utilisation judicieuse de [<AutoOpen>]
peut entraîner une meilleure facilité d’utilisation.
Envisagez de définir des membres d’opérateur sur des classes où l’utilisation d’opérateurs connus est appropriée
Parfois, les classes sont utilisées pour modéliser des constructions mathématiques telles que Vectors. Lorsque le domaine modélisé a des opérateurs connus, leur définition en tant que membres intrinsèques à la classe est utile.
type Vector(x: float) =
member v.X = x
static member (*) (vector: Vector, scalar: float) = Vector(vector.X * scalar)
static member (+) (vector1: Vector, vector2: Vector) = Vector(vector1.X + vector2.X)
let v = Vector(5.0)
let u = v * 10.0
Ce guide correspond à des conseils .NET généraux pour ces types. Toutefois, il peut être également important dans le codage F#, car cela permet à ces types d’être utilisés conjointement avec les fonctions et méthodes F# avec des contraintes de membre, telles que List.sumBy.
Envisagez d’utiliser CompiledName pour fournir un nom convivial pour .NET à d’autres utilisateurs de langages .NET.
Parfois, vous souhaiterez peut-être nommer quelque chose dans un style pour les consommateurs F# (par exemple, un membre statique en minuscules afin qu’il apparaisse comme s’il s’agissait d’une fonction liée au module), mais avoir un style différent pour le nom lorsqu’il est compilé dans un assembly. Vous pouvez utiliser l’attribut [<CompiledName>]
pour fournir un style différent pour le code non F# consommant l’assembly.
type Vector(x:float, y:float) =
member v.X = x
member v.Y = y
[<CompiledName("Create")>]
static member create x y = Vector (x, y)
let v = Vector.create 5.0 3.0
En utilisant [<CompiledName>]
, vous pouvez appliquer les conventions de nommage .NET pour les utilisateurs non F# de l'assemblage.
Utiliser la surcharge de méthode pour les fonctions membres si cela simplifie l’API
La surcharge de méthode est un outil puissant pour simplifier une API qui peut avoir besoin d’effectuer des fonctionnalités similaires, mais avec différentes options ou arguments.
type Logger() =
member this.Log(message) =
...
member this.Log(message, retryPolicy) =
...
En F#, il est plus courant de surcharger le nombre d’arguments plutôt que les types d’arguments.
Masquer les représentations des types enregistrement et union si la conception de ces types est susceptible d’évoluer
Évitez de révéler des représentations concrètes d’objets. Par exemple, la représentation précise des valeurs de DateTime n’est pas dévoilée par l’API externe et publique de la bibliothèque .NET. Au moment de l’exécution, Common Language Runtime connaît l’implémentation validée qui sera utilisée tout au long de l’exécution. Toutefois, le code compilé ne récupère pas lui-même les dépendances sur la représentation concrète.
Éviter l’utilisation de l’héritage d’implémentation pour l’extensibilité
En F#, l’héritage d’implémentation est rarement utilisé. En outre, les hiérarchies d’héritage sont souvent complexes et difficiles à modifier lorsque de nouvelles exigences arrivent. L’implémentation de l’héritage existe toujours dans F# pour la compatibilité et les cas rares où il s’agit de la meilleure solution à un problème, mais d’autres techniques doivent être recherchées dans vos programmes F# lors de la conception pour le polymorphisme, comme l’implémentation d’interface.
Signatures de fonctions et de membres
Utiliser des tuples pour les valeurs de retour lors du retour d’un petit nombre de valeurs non liées
Voici un bon exemple d’utilisation d’un tuple dans un type de retour :
val divrem: BigInteger -> BigInteger -> BigInteger * BigInteger
Pour les types de retour contenant de nombreux composants ou lorsque les composants sont liés à une entité identifiable unique, envisagez d’utiliser un type nommé au lieu d’un tuple.
Utiliser Async<T>
pour la programmation asynchrone aux limites de l’API F#
S’il existe une opération synchrone correspondante nommée Operation
qui retourne un T
, l’opération asynchrone doit être nommée AsyncOperation
si elle retourne Async<T>
ou OperationAsync
si elle retourne Task<T>
. Pour les types .NET couramment utilisés qui exposent des méthodes Begin/End, envisagez d’utiliser Async.FromBeginEnd
pour écrire des méthodes d’extension en tant que façade pour fournir le modèle de programmation asynchrone F# à ces API .NET.
type SomeType =
member this.Compute(x:int): int =
...
member this.AsyncCompute(x:int): Async<int> =
...
type System.ServiceModel.Channels.IInputChannel with
member this.AsyncReceive() =
...
Exceptions
Consultez gestion des erreurs pour en savoir plus sur l’utilisation appropriée des exceptions, des résultats et des options.
Membres d’extension
Appliquer avec précaution les membres d’extension F# dans les composants F# à F#
En général, les membres d’extension F# ne doivent être utilisés que pour les opérations qui se trouvent dans la fermeture d’opérations intrinsèques associées à un type dans la majorité de ses modes d’utilisation. Une utilisation courante consiste à fournir des API plus idiomatiques à F# pour différents types .NET :
type System.ServiceModel.Channels.IInputChannel with
member this.AsyncReceive() =
Async.FromBeginEnd(this.BeginReceive, this.EndReceive)
type System.Collections.Generic.IDictionary<'Key,'Value> with
member this.TryGet key =
let ok, v = this.TryGetValue key
if ok then Some v else None
Types d’union
Utiliser des unions discriminatoires au lieu de hiérarchies de classes pour les données structurées par arborescence
Les structures de type arborescence sont définies de manière récursive. C’est maladroit avec l’héritage, mais élégant avec les unions discriminées.
type BST<'T> =
| Empty
| Node of 'T * BST<'T> * BST<'T>
La représentation de données de type arborescence avec des unions discriminatoires vous permet également de tirer parti de l’exhaustivité de la mise en correspondance des modèles.
Utilisez [<RequireQualifiedAccess>]
sur les types d'union dont les noms de cas ne sont pas suffisamment uniques
Vous pouvez vous trouver dans un domaine où le même nom convient le mieux à différentes choses, comme les cas de syndicat discriminant. Vous pouvez utiliser [<RequireQualifiedAccess>]
pour lever l’ambiguïté des noms de cas afin d’éviter de provoquer des erreurs de confusion dues au masquage dépendant de l’ordre des instructions open
.
Masquer les représentations des unions discriminatoires pour les API compatibles binaires si la conception de ces types est susceptible d’évoluer
Les types union s’appuient sur des formulaires de critères spéciaux F# pour un modèle de programmation succinct. Comme mentionné précédemment, vous devez éviter de révéler des représentations concrètes des données si la conception de ces types est susceptible d’évoluer.
Par exemple, la représentation d’une union discriminatoire peut être masquée à l’aide d’une déclaration privée ou interne, ou à l’aide d’un fichier de signature.
type Union =
private
| CaseA of int
| CaseB of string
Si vous révélez des unions discriminées sans discrimination, vous aurez peut-être du mal à versionner votre bibliothèque sans casser le code utilisateur. Au lieu de cela, envisagez de révéler un ou plusieurs modèles actifs pour autoriser les critères spéciaux sur les valeurs de votre type.
Les modèles actifs offrent une alternative pour permettre aux utilisateurs F# d'effectuer une mise en correspondance de motifs tout en évitant d’exposer directement les types d’union de F#.
Fonctions inline et contraintes de membre
Définir des algorithmes numériques génériques à l’aide de fonctions inline avec des contraintes de membre implicites et des types génériques résolus statiquement
Les contraintes de membre arithmétiques et les contraintes de comparaison F# sont des standards pour la programmation F#. Par exemple, considérez le code suivant :
let inline highestCommonFactor a b =
let rec loop a b =
if a = LanguagePrimitives.GenericZero<_> then b
elif a < b then loop a (b - a)
else loop (a - b) b
loop a b
Le type de cette fonction est le suivant :
val inline highestCommonFactor : ^T -> ^T -> ^T
when ^T : (static member Zero : ^T)
and ^T : (static member ( - ) : ^T * ^T -> ^T)
and ^T : equality
and ^T : comparison
Il s’agit d’une fonction appropriée pour une API publique dans une bibliothèque mathématique.
Éviter d’utiliser des contraintes de membre pour simuler des classes de type et le « duck typing »
Il est possible de simuler le « duck typing » à l’aide de contraintes de membre F#. Toutefois, les membres qui l’utilisent ne doivent généralement pas être utilisés dans des conceptions de bibliothèques F# à F#. Cela est dû au fait que les conceptions de bibliothèque basées sur des contraintes implicites inconnues ou non standard ont tendance à rendre le code utilisateur inflexible et lié à un modèle d’infrastructure particulier.
En outre, il existe une bonne chance que l’utilisation intensive des contraintes de membre de cette façon puisse entraîner des temps de compilation très longs.
Définitions d’opérateurs
Éviter de définir des opérateurs symboliques personnalisés
Les opérateurs personnalisés sont essentiels dans certaines situations et sont des appareils notationnels très utiles dans un grand corps de code d’implémentation. Pour les nouveaux utilisateurs d’une bibliothèque, les fonctions nommées sont souvent plus faciles à utiliser. En outre, les opérateurs symboliques personnalisés peuvent être difficiles à documenter, et les utilisateurs trouvent qu’il est plus difficile de rechercher de l’aide sur les opérateurs, en raison des limitations existantes dans les moteurs de recherche et l’IDE.
Il est donc préférable de publier vos fonctionnalités en tant que fonctions et membres nommés, et d’exposer les opérateurs pour cette fonctionnalité uniquement si les avantages de la notation l’emportent sur la documentation et le coût cognitif de leur utilisation.
Unités de mesure
Utiliser soigneusement des unités de mesure pour accroître la sécurité des types dans le code F#
D’autres informations de saisie pour les unités de mesure sont effacées lorsqu’elles sont consultées par d’autres langages .NET. N’oubliez pas que les composants, outils et mécanismes de réflexion de .NET percevront les types sans unités. Par exemple, les utilisateurs de C# verront float
plutôt que float<kg>
.
Abréviations de type
Utilisez soigneusement les abréviations de type pour simplifier le code F#
Les composants, outils et reflection .NET ne reconnaissent pas les noms abrégés pour les types. L’utilisation significative des abréviations de type peut également rendre un domaine plus complexe qu’il ne l’est réellement, ce qui pourrait confondre les consommateurs.
Évitez les abréviations de type pour les types publics dont les membres et les propriétés doivent être intrinsèquement différents de ceux disponibles sur le type abrégé
Dans ce cas, le type abrégé révèle trop de représentation du type réel défini. Au lieu de cela, envisagez d’encapsuler l’abréviation dans un type de classe ou une union discriminatoire à cas unique (ou, lorsque les performances sont essentielles, envisagez d’utiliser un type de struct pour encapsuler l’abréviation).
Par exemple, il est tentant de définir une multi-carte comme un cas spécial d'une mappe F#, par exemple :
type MultiMap<'Key,'Value> = Map<'Key,'Value list>
Toutefois, les opérations de notation par points logiques sur ce type ne sont pas les mêmes que les opérations sur une carte , par exemple, il est raisonnable que l’opérateur de recherche map[key]
retourner la liste vide si la clé n’est pas dans le dictionnaire, plutôt que de déclencher une exception.
Instructions pour les bibliothèques à utiliser à partir d’autres langages .NET
Lors de la conception de bibliothèques à utiliser à partir d’autres langages .NET, il est important de respecter les instructions de conception de bibliothèques .NET . Dans ce document, ces bibliothèques sont étiquetées en tant que bibliothèques Vanilla .NET, par opposition aux bibliothèques orientées F# qui utilisent des constructions F# sans aucune restriction. La conception de bibliothèques .NET vanille signifie fournir des API familières et idiomatiques cohérentes avec le reste du .NET Framework en minimisant l’utilisation de constructions spécifiques à F#dans l’API publique. Les règles sont expliquées dans les sections suivantes.
Conception d’espaces de noms et de types (pour les bibliothèques utilisées à partir d’autres langages .NET)
Appliquer les conventions d’affectation de noms .NET à l’API publique de vos composants
Attention particulière à l’utilisation de noms abrégés et des instructions de mise en majuscules .NET.
type pCoord = ...
member this.theta = ...
type PolarCoordinate = ...
member this.Theta = ...
Utiliser des espaces de noms, des types et des membres comme structure organisationnelle principale pour vos composants
Tous les fichiers contenant des fonctionnalités publiques doivent commencer par une déclaration namespace
, et les seules entités publiques dans les espaces de noms doivent être des types. N’utilisez pas de modules F#.
Utilisez des modules non publics pour contenir le code d’implémentation, les types d’utilitaires et les fonctions utilitaires.
Les types statiques doivent être préférés aux modules, car ils permettent à l’évolution future de l’API d’utiliser la surcharge et d’autres concepts de conception d’API .NET qui peuvent ne pas être utilisés dans les modules F#.
Par exemple, à la place de l’API publique suivante :
module Fabrikam
module Utilities =
let Name = "Bob"
let Add2 x y = x + y
let Add3 x y z = x + y + z
Envisagez à la place :
namespace Fabrikam
[<AbstractClass; Sealed>]
type Utilities =
static member Name = "Bob"
static member Add(x,y) = x + y
static member Add(x,y,z) = x + y + z
Utilisez les types d’enregistrements F# dans les API .NET standard si la conception des types n'évolue pas.
Les types d’enregistrements F# sont compilés en une classe .NET simple. Ceux-ci conviennent à certains types simples et stables dans les API. Envisagez d’utiliser les attributs [<NoEquality>]
et [<NoComparison>]
pour supprimer la génération automatique d’interfaces. Évitez également d’utiliser des champs d’enregistrement mutables dans les API .NET vanille, car ceux-ci exposent un champ public. Déterminez toujours si une classe fournirait une option plus flexible pour l’évolution future de l’API.
Par exemple, le code F# suivant expose l’API publique à un consommateur C# :
F# :
[<NoEquality; NoComparison>]
type MyRecord =
{ FirstThing: int
SecondThing: string }
C# :
public sealed class MyRecord
{
public MyRecord(int firstThing, string secondThing);
public int FirstThing { get; }
public string SecondThing { get; }
}
Masquer la représentation des types union F# dans les API Vanilla .NET
Les types d’union F# ne sont pas couramment utilisés entre les limites des composants, même pour le codage F#-à-F#. Il s’agit d’un excellent appareil d’implémentation lorsqu’il est utilisé en interne dans les composants et les bibliothèques.
Lors de la conception d’une API .NET vanille, envisagez de masquer la représentation d’un type union à l’aide d’une déclaration privée ou d’un fichier de signature.
type PropLogic =
private
| And of PropLogic * PropLogic
| Not of PropLogic
| True
Vous pouvez également augmenter les types qui utilisent une représentation union en interne avec des membres pour fournir l’API orientée .NET souhaitée.
type PropLogic =
private
| And of PropLogic * PropLogic
| Not of PropLogic
| True
/// A public member for use from C#
member x.Evaluate =
match x with
| And(a,b) -> a.Evaluate && b.Evaluate
| Not a -> not a.Evaluate
| True -> true
/// A public member for use from C#
static member CreateAnd(a,b) = And(a,b)
Conception de l’interface utilisateur graphique et d’autres composants à l’aide des modèles de conception de l’infrastructure
Il existe de nombreux frameworks différents disponibles dans .NET, tels que WinForms, WPF et ASP.NET. Les conventions de nommage et de conception pour chacune d’elles doivent être utilisées si vous concevez des composants à utiliser dans ces frameworks. Par exemple, pour la programmation WPF, adoptez des modèles de conception WPF pour les classes que vous concevez. Pour les modèles dans la programmation de l’interface utilisateur, utilisez des modèles de conception tels que des événements et des collections basées sur des notifications, tels que ceux trouvés dans System.Collections.ObjectModel.
Conception d’objet et de membre (pour les bibliothèques à utiliser à partir d’autres langages .NET)
Utiliser l’attribut CLIEvent pour exposer des événements .NET
Construisez un DelegateEvent
avec un type délégué .NET spécifique qui accepte un objet et EventArgs
(plutôt qu’un Event
, qui utilise simplement le type FSharpHandler
par défaut) afin que les événements soient publiés de manière familière à d’autres langages .NET.
type MyBadType() =
let myEv = new Event<int>()
[<CLIEvent>]
member this.MyEvent = myEv.Publish
type MyEventArgs(x: int) =
inherit System.EventArgs()
member this.X = x
/// A type in a component designed for use from other .NET languages
type MyGoodType() =
let myEv = new DelegateEvent<EventHandler<MyEventArgs>>()
[<CLIEvent>]
member this.MyEvent = myEv.Publish
Exposer des opérations asynchrones en tant que méthodes qui retournent des tâches .NET
Les tâches sont utilisées dans .NET pour représenter des calculs asynchrones actifs. Les tâches sont généralement moins compositionnelles que les objets F# Async<T>
, car elles représentent des tâches « déjà en cours d’exécution » et ne peuvent pas être composées de manière à effectuer une composition parallèle, ou qui masquent la propagation des signaux d’annulation et d’autres paramètres contextuels.
Toutefois, malgré cela, les méthodes qui retournent des tâches sont la représentation standard de la programmation asynchrone sur .NET.
/// A type in a component designed for use from other .NET languages
type MyType() =
let compute (x: int): Async<int> = async { ... }
member this.ComputeAsync(x) = compute x |> Async.StartAsTask
Vous souhaiterez fréquemment accepter un jeton d’annulation explicite :
/// A type in a component designed for use from other .NET languages
type MyType() =
let compute(x: int): Async<int> = async { ... }
member this.ComputeAsTask(x, cancellationToken) = Async.StartAsTask(compute x, cancellationToken)
Utiliser des types délégués .NET au lieu de types de fonctions F#
Ici, les types de fonctions F# signifient les types « flèche » comme int -> int
.
Au lieu de cela :
member this.Transform(f: int->int) =
...
Procédez comme suit :
member this.Transform(f: Func<int,int>) =
...
Le type de fonction F# apparaît en tant que class FSharpFunc<T,U>
à d’autres langages .NET et est moins adapté aux fonctionnalités de langage et aux outils qui comprennent les types délégués. Lors de la création d’une méthode d’ordre supérieur ciblant .NET Framework 3.5 ou version ultérieure, les délégués System.Func
et System.Action
sont les bonnes API à publier pour permettre aux développeurs .NET de consommer ces API de manière à faible friction. (Lorsque vous ciblez .NET Framework 2.0, les types délégués définis par le système sont plus limités ; envisagez d’utiliser des types délégués prédéfinis tels que System.Converter<T,U>
ou définir un type délégué spécifique.)
D’un autre côté, les délégués .NET ne sont pas naturels pour les bibliothèques orientées F# (voir la section suivante sur les bibliothèques orientées F#). Par conséquent, une stratégie d’implémentation courante lors du développement de méthodes d’ordre supérieur pour les bibliothèques .NET vanille consiste à réaliser toute l’implémentation à l’aide de types de fonctions F#, puis à concevoir l’API publique à l’aide de délégués comme une fine façade au-dessus de l’implémentation F# réelle.
Utilisez le modèle TryGetValue au lieu de retourner des valeurs d’option F#, et préférez la surcharge de méthode à la prise de valeurs d’option F# en tant qu’arguments
Les modèles courants d’utilisation pour le type d’option F# dans les API sont mieux implémentés dans les API .NET vanille à l’aide de techniques de conception .NET standard. Au lieu de retourner une valeur d’option F#, envisagez d’utiliser le type de retour bool plus un paramètre out comme dans le modèle « TryGetValue ». Au lieu de prendre des valeurs d’option F# en tant que paramètres, envisagez d’utiliser la surcharge de méthode ou des arguments facultatifs.
member this.ReturnOption() = Some 3
member this.ReturnBoolAndOut(outVal: byref<int>) =
outVal <- 3
true
member this.ParamOption(x: int, y: int option) =
match y with
| Some y2 -> x + y2
| None -> x
member this.ParamOverload(x: int) = x
member this.ParamOverload(x: int, y: int) = x + y
Utilisez les types d’interface de collection .NET IEnumerable<T> et IDictionary<Key,Value> pour les paramètres et les valeurs de retour
Évitez l’utilisation de types de collection concrets tels que des tableaux .NET T[]
, des types F# list<T>
, des Map<Key,Value>
et des Set<T>
, et des types de collection concrets .NET tels que Dictionary<Key,Value>
. Les recommandations en matière de conception de bibliothèque .NET ont de bons conseils concernant l’utilisation de différents types de collection comme IEnumerable<T>
. L’utilisation de tableaux (T[]
) est acceptable dans certains cas, pour des raisons de performances. Notez en particulier que seq<T>
est simplement l’alias F# pour IEnumerable<T>
, et donc seq est souvent un type approprié pour une API .NET vanille.
Au lieu d’utiliser des listes F# :
member this.PrintNames(names: string list) =
...
Utilisez des séquences F# :
member this.PrintNames(names: seq<string>) =
...
Utilisez le type d’unité comme seul type d’entrée d’une méthode pour définir une méthode zéro argument, ou comme seul type de retour pour définir une méthode void-return
Évitez les autres utilisations du type d’unité. Il s’agit de bons éléments :
✔ member this.NoArguments() = 3
✔ member this.ReturnVoid(x: int) = ()
C’est mauvais :
member this.WrongUnit( x: unit, z: int) = ((), ())
Vérifier les valeurs nulles aux frontières de l'API .NET standard
Le code d’implémentation F# a tendance à avoir moins de valeurs Null, en raison de modèles de conception immuables et de restrictions sur l’utilisation de littéraux Null pour les types F#. D’autres langages .NET utilisent souvent null comme valeur beaucoup plus fréquemment. En raison de cela, le code F# qui expose une API .NET vanille doit vérifier les paramètres null à la limite de l’API et empêcher ces valeurs de passer plus en profondeur dans le code d’implémentation F#. Vous pouvez utiliser la fonction isNull
ou les critères spéciaux sur le modèle null
.
let checkNonNull argName (arg: obj) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull' argName (arg: obj) =
if isNull arg then nullArg argName
else ()
À compter de F# 9, vous pouvez utiliser la nouvelle syntaxe | null
pour que le compilateur indique les valeurs null possibles et où elles doivent être gérées.
let checkNonNull argName (arg: obj | null) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull' argName (arg: obj | null) =
if isNull arg then nullArg argName
else ()
En F# 9, le compilateur émet un avertissement lorsqu’il détecte qu’une valeur null possible n’est pas gérée :
let printLineLength (s: string) =
printfn "%i" s.Length
let readLineFromStream (sr: System.IO.StreamReader) =
// `ReadLine` may return null here - when the stream is finished
let line = sr.ReadLine()
// nullness warning: The types 'string' and 'string | null'
// do not have equivalent nullability
printLineLength line
Ces avertissements doivent être résolus en utilisant le modèle Null F# dans la mise en correspondance :
let printLineLength (s: string) =
printfn "%i" s.Length
let readLineFromStream (sr: System.IO.StreamReader) =
let line = sr.ReadLine()
match line with
| null -> ()
| s -> printLineLength s
Éviter d’utiliser des tuples comme valeurs de retour
Au lieu de cela, préférez retourner un type nommé contenant les données d’agrégation ou utiliser des paramètres de sortie pour retourner plusieurs valeurs. Bien que les tuples et les tuples struct existent dans .NET (le langage C# prend notamment en charge les tuples struct), ils ne fournissent généralement pas l’API idéale à laquelle s’attendent les développeurs .NET.
Éviter d’utiliser la curryfication des paramètres
Utilisez plutôt des conventions d’appel .NET Method(arg1,arg2,…,argN)
.
member this.TupledArguments(str, num) = String.replicate num str
Conseil : Si vous concevez des bibliothèques à utiliser à partir d’un langage .NET, il n’existe aucun substitut pour effectuer une programmation C# expérimentale et Visual Basic pour vous assurer que vos bibliothèques « se sentent bien » de ces langages. Vous pouvez également utiliser des outils tels que .NET Reflector et Visual Studio Object Browser pour vous assurer que les bibliothèques et leur documentation apparaissent comme prévu pour les développeurs.
Appendice
Exemple de conception de code F# de bout en bout pour une utilisation par d’autres langages .NET
Considérez la classe suivante :
open System
type Point1(angle,radius) =
new() = Point1(angle=0.0, radius=0.0)
member x.Angle = angle
member x.Radius = radius
member x.Stretch(l) = Point1(angle=x.Angle, radius=x.Radius * l)
member x.Warp(f) = Point1(angle=f(x.Angle), radius=x.Radius)
static member Circle(n) =
[ for i in 1..n -> Point1(angle=2.0*Math.PI/float(n), radius=1.0) ]
Le type F# déduit de cette classe est le suivant :
type Point1 =
new : unit -> Point1
new : angle:double * radius:double -> Point1
static member Circle : n:int -> Point1 list
member Stretch : l:double -> Point1
member Warp : f:(double -> double) -> Point1
member Angle : double
member Radius : double
Examinons comment ce type F# apparaît pour un programmeur à l’aide d’un autre langage .NET. Par exemple, la « signature » C# approximative est la suivante :
// C# signature for the unadjusted Point1 class
public class Point1
{
public Point1();
public Point1(double angle, double radius);
public static Microsoft.FSharp.Collections.List<Point1> Circle(int count);
public Point1 Stretch(double factor);
public Point1 Warp(Microsoft.FSharp.Core.FastFunc<double,double> transform);
public double Angle { get; }
public double Radius { get; }
}
Il existe quelques points importants à noter sur la façon dont F# représente les constructions ici. Par exemple:
Les métadonnées telles que les noms d’arguments ont été conservées.
Les méthodes F# qui prennent deux arguments deviennent des méthodes C# qui acceptent deux arguments.
Les fonctions et les listes deviennent des références aux types correspondants dans la bibliothèque F#.
Le code suivant montre comment ajuster ce code pour tenir compte de ces éléments.
namespace SuperDuperFSharpLibrary.Types
type RadialPoint(angle:double, radius:double) =
/// Return a point at the origin
new() = RadialPoint(angle=0.0, radius=0.0)
/// The angle to the point, from the x-axis
member x.Angle = angle
/// The distance to the point, from the origin
member x.Radius = radius
/// Return a new point, with radius multiplied by the given factor
member x.Stretch(factor) =
RadialPoint(angle=angle, radius=radius * factor)
/// Return a new point, with angle transformed by the function
member x.Warp(transform:Func<_,_>) =
RadialPoint(angle=transform.Invoke angle, radius=radius)
/// Return a sequence of points describing an approximate circle using
/// the given count of points
static member Circle(count) =
seq { for i in 1..count ->
RadialPoint(angle=2.0*Math.PI/float(count), radius=1.0) }
Le type F# déduit du code est le suivant :
type RadialPoint =
new : unit -> RadialPoint
new : angle:double * radius:double -> RadialPoint
static member Circle : count:int -> seq<RadialPoint>
member Stretch : factor:double -> RadialPoint
member Warp : transform:System.Func<double,double> -> RadialPoint
member Angle : double
member Radius : double
La signature C# est désormais la suivante :
public class RadialPoint
{
public RadialPoint();
public RadialPoint(double angle, double radius);
public static System.Collections.Generic.IEnumerable<RadialPoint> Circle(int count);
public RadialPoint Stretch(double factor);
public RadialPoint Warp(System.Func<double,double> transform);
public double Angle { get; }
public double Radius { get; }
}
Les correctifs apportés pour préparer ce type à utiliser dans le cadre d’une bibliothèque .NET vanille sont les suivants :
Plusieurs noms ajustés :
Point1
,n
,l
etf
sont devenusRadialPoint
,count
,factor
ettransform
, respectivement.Utilisé un type de retour de
seq<RadialPoint>
au lieu deRadialPoint list
en modifiant une construction de liste à l’aide de[ ... ]
à une construction de séquence à l’aide deIEnumerable<RadialPoint>
.Utilisé le type délégué .NET
System.Func
au lieu d’un type de fonction F#.
La consommation dans le code C# est ainsi beaucoup plus agréable.