Membres d’extension pour les contraintes SRTP

Une fonction inline avec des paramètres de type résolus statiquement peut nécessiter un membre portant un nom et une signature particuliers. F# 11.2 permet à un membre d’extension dans l’étendue de satisfaire cette exigence, même lorsque le type provient d’un autre assembly.

Utilisez cette fonctionnalité pour séparer un algorithme générique des adaptateurs pour des types particuliers. Vous n’avez pas besoin d’encapsuler ces types ou d’y ajouter une interface nominale.

Note

Dans le sdk .NET 11.0.100, activez l’emplacement <LangVersion>preview</LangVersion> où le compilateur résout les extensions. Consultez les paramètres de langue F# 11.2 pour la bande de fonctionnalités planifiée. Les exemples de chemin d’accès et de mappage utilisent une bibliothèque F# 11.0 et un consommateur en préversion ; consultez Compiler des bibliothèques et des consommateurs.

Adapter une propriété pour un algorithme partagé

Le code d’application suivant mesure les chemins d’accès représentés par deux types d’infrastructure existants. Les deux chemins ont une longueur de 8:

open System.Numerics
open Structural.Adapters
open Structural.Algorithms

let complexRoute =
    [ Complex(0.0, 0.0); Complex(3.0, 4.0); Complex(6.0, 4.0) ]
let vectorRoute =
    [ Vector2(0.0f, 0.0f); Vector2(3.0f, 4.0f); Vector2(6.0f, 4.0f) ]

printn (pathLength complexRoute) // 8
printn (pathLength vectorRoute)  // 8

La source de bibliothèque définit Structural.Adapters et Structural.Algorithms. Les adaptateurs exposent la même propriété de coordonnées sur les types avec différentes API natives :

type System.Numerics.Complex with
    member point.Coordinates = point.Real, point.Imaginary

type System.Numerics.Vector2 with
    member point.Coordinates = float point.X, float point.Y

L’algorithme demande une Coordinates propriété retournant une paire de valeurs à virgule flottante :

let inline coordinates (point: ^Point) : float * float =
    (^Point : (member Coordinates: float * float) point)

let inline pathLength (points: ^Point list) =
    points
    |> List.pairwise
    |> List.sumBy (fun (a, b) ->
        let ax, ay = coordinates a
        let bx, by = coordinates b
        let dx, dy = bx - ax, by - ay
        sqrt (dx * dx + dy * dy))

À chaque appel concret, le compilateur sélectionne un Coordinates membre, appelé témoin de la contrainte. L’algorithme inline accepte une propriété intrinsèque ou d’extension ; les chemins vides et à point unique ont la longueur zéro.

Partager une opération entre les types de collection

Les contraintes d’extension prennent également en charge les membres statiques. Le module de la Structural.Adapters bibliothèque définit des adaptateurs à l’aide des fonctions existantes List.map :Option.map

type List<'T> with
    static member Map(source: 'T list, mapping: 'T -> 'U) : 'U list =
        List.map mapping source

type Option<'T> with
    static member Map(source: 'T option, mapping: 'T -> 'U) : 'U option =
        Option.map mapping source

Dans le module de Structural.Algorithms la bibliothèque, demandez un Map membre sur le type d’entrée complet :

let inline map (mapping: 'T -> 'U) (source: ^Input) : ^Output =
    (^Input :
        (static member Map: ^Input * ('T -> 'U) -> ^Output)
        (source, mapping))

L’application peut ensuite partager une transformation, modifier le type d’élément et composer le mappage sur une collection imbriquée :

let inline cleanNames values =
    map (fun (name: string) -> name.Trim().ToUpperInvariant()) values

let batch: string list = cleanNames [ " Ada "; " grace " ]
let optional: string option = cleanNames (Some " linus ")
let missing: string option = cleanNames (None: string option)
let lengths: int list = map String.length batch
let nested: int option list = map (map String.length) [ Some "abc"; None ]

printfn "%A; %A; %A" batch optional missing
printfn "%A; %A" lengths nested
// ["ADA"; "GRACE"]; Some "LINUS"; None
// [3; 5]; [Some 3; None]

Le compilateur vérifie les noms et les types de membres, et non les interfaces ou les lois de mappage. Ce n’est pas un polymorphisme de type supérieur : aucun paramètre ne représente un constructeur de conteneur tel que F<_>.

Extensions d’opérateur

Les opérateurs peuvent utiliser le même mécanisme. Par exemple, définissez un opérateur de mappage pour les listes et les options :

type List<'T> with
    static member (|>>) (source: 'T list, mapping: 'T -> 'U) =
        List.map mapping source

type Option<'T> with
    static member (|>>) (source: 'T option, mapping: 'T -> 'U) =
        Option.map mapping source

let inline (|>>) (source: ^Input) (mapping: 'T -> 'U) : ^Output =
    (^Input :
        (static member (|>>): ^Input * ('T -> 'U) -> ^Output)
        (source, mapping))

Un consommateur s’ouvre StructuralOperators et utilise l’opérateur :

open StructuralOperators

let incremented: int list = [1; 2; 3] |>> ((+) 10)
let length: int option = Some "hello" |>> String.length
// [11; 12; 13] and Some 5

Les solutions d’opérateur primitive intégrées conservent la priorité pour les types correspondants. Une extension ne fournit pas d’élargissement arbitraire numérique mixte ou ne remplace pas tous les opérateurs intégrés.

Le mode F# 11.0 peut accepter une déclaration d’opérateur d’extension ordinaire avec FS1215, mais ne peut pas l’utiliser comme solution SRTP. Le projet d’opérateur utilise la préversion.

Opérateurs et contexte tuples

Une collection d’observations et une paire de résultats accumulés indépendamment peuvent participer au même protocole de combinaison. Le consommateur suivant définit son algorithme générique après avoir ouvert les adaptateurs de la bibliothèque :

open Composition.Adapters
open Composition.Algorithms

let inline combineAll (batches: ^T list) : ^T =
    List.reduce (++) batches

let observations = combineAll [["temperature"]; ["pressure"; "humidity"]]
let summary = combineAll [(2, ["temperature"]); (3, ["pressure"; "humidity"])]

printfn "%A" observations
printfn "%A" summary
// ["temperature"; "pressure"; "humidity"]
// (5, ["temperature"; "pressure"; "humidity"])

Ces extensions de bibliothèque donnent à l’opérateur une signification pour les entiers, les listes et les paires :

type System.Int32 with
    static member (++) (left: int, right: int) = left + right

type List<'T> with
    static member (++) (left: 'T list, right: 'T list) = left @ right

type ('T1 * 'T2) with
    static member inline (++) ((a, b), (c, d)) = (a ++ c, b ++ d)

combineAll nécessite une liste sansmpty, car elle utilise List.reduce. L’opération conserve l’ordre de liste ; le compilateur n’applique pas l’associativité ou ne fournit pas d’identité pour une entrée vide.

En-têtes natifs et type struct ('T1 * 'T2) with étendre les tuples type ('T1 * 'T2) with de référence et de valeur CLR. Les deux sont des fonctionnalités F# 11.2 et prennent en charge deux à sept éléments ; les en-têtes plus volumineux produisent FS3915.

Les tuples peuvent également porter le contexte en même temps qu’une valeur. L’opérateur suivant ajoute une piste lors de l’application d’une fonction contextuelle :

let priced =
    (["catalogue price"], fun units -> decimal units * 12m)
        <*> (["validated quantity"], 3)

printfn "%A" priced
// (["catalogue price"; "validated quantity"], 36M)
type ('Context * 'Value) with
    static member inline (<*>) ((before, mapping), (after, value)) =
        (before ++ after, mapping value)

type struct ('Context * 'Value) with
    static member inline (<*>) (struct (before, mapping), struct (after, value)) =
        struct (before ++ after, mapping value)

Les appels point-instance tuple et les en-têtes de type de fonction natifs tels que type ('a -> 'b) with ne sont pas pris en charge.

La bibliothèque de composition et le consommateur utilisent la préversion. Conserver combineAll dans le consommateur : l’exportation de cette réduction peut produire FS0193 pour tuple ++. Les exemples de chemin d’accès et de mappage distincts exportent leurs algorithmes.

Construction dirigée par retour

Pour une opération qui introduit une valeur brute dans un conteneur, le type de sortie peut identifier le membre qui doit le construire :

type List<'T> with
    static member Return(value: 'T) : 'T list = [value]

type Option<'T> with
    static member Return(value: 'T) : 'T option = Some value
let inline singleton (value: 'T) : ^Output =
    (^Output : (static member Return: 'T -> ^Output) value)
let oneBatch: string list = singleton "ready"
let oneOptional: string option = singleton "ready"
// ["ready"] and Some "ready"

L’annotation de résultat du consommateur sélectionne l’extension du type de Return sortie. L’entrée seule ne peut pas choisir un conteneur.

Chaque conteneur fournit son propre membre. Pour les surcharges de retour uniquement sur un type unique, utilisez l’attribut distinctAllowOverloadOnReturnType.

Compiler des bibliothèques et des consommateurs

Les paramètres linguistiques s’appliquent par projet, et non transitif par le biais de références. La matrice suivante utilise les paramètres du SDK .NET 11.0.100 pour les fonctionnalités F# 11.2 :

Implémentation de bibliothèque Utilisation du consommateur Conditions requises en préversion
Déclare les en-têtes tuple-extension natifs. Résout leurs opérateurs d’extension. Les deux ont besoin d’une préversion ; la nouvelle syntaxe d’en-tête elle-même est en préversion.
Définit les extensions ordinaires et exporte une contrainte membre inline non résolue. Fournit des types concrets dont les extensions résolvent cette contrainte. Le consommateur a besoin d’une préversion. La bibliothèque peut utiliser F# 11.0.
Résout une contrainte d’extension en interne et expose une API inline ordinaire ou déjà résolue. Appelle cette API sans résolution d’extension supplémentaire. La bibliothèque a besoin d’une préversion ; le consommateur peut utiliser F# 11.0.
Résout les extensions et les contraintes d’exportation que le consommateur résout également par le biais d’extensions. Résout les contraintes restantes. Les deux ont besoin d’une préversion.

Le chemin d’accès et la bibliothèque de mappages utilise 11.0; son consommateur utilise preview. À partir du répertoire consommateur, exécutez-le dotnet run avec un SDK F# 11 prenant en charge.

Étendue d’extension entre assemblys

Utilisez des extensions publiques et ouvrez leur module avant de définir des fonctions inline dépendantes. Pour une contrainte non résolue importée, ouvrez également les adaptateurs dans le consommateur ; les déclarations du open producteur ne sont pas transférées. Une API déjà résolue ne demande pas au consommateur de rouvrir ses adaptateurs.

Inférence et signatures

La résolution F# 11.2 reporte certains choix que le mode F# 11.0 effectue précédemment. Considérez un assistance inline défini avec un DateTime récepteur :

let inline shift (date: System.DateTime) offset = date + offset

En mode F# 11.0, ce formulaire peut déduire DateTime -> TimeSpan -> DateTime. Avec la résolution d’extension F# 11.2 activée, elle peut conserver une contrainte pour (DateTime or ^Offset) : (static member (+) : DateTime * ^Offset -> ^Result). Un consommateur peut ensuite sélectionner une extension publique dans l’étendue pour un autre type de décalage.

Un consommateur ne peut pas récupérer la généralité perdue dans une bibliothèque F# 11.0. Vérifier les signatures exportées ; le producteur peut également avoir besoin d’une inférence F# 11.2 activée.

Limitations et résolution des problèmes en préversion

  • Pour FS0001 ou FS0193, vérifiez les paramètres linguistiques, la visibilité publique et l’étendue de l’adaptateur.
  • Si un fichier producteur ultérieur résout l’exemple de chemin d’accès pour Complex, la génération de code peut échouer avec FS0971, Undefined value 'get_Coordinates'. Conservez cette résolution avec ses définitions ou dans un consommateur distinct.
  • Les adaptateurs concurrents peuvent rendre la sélection de membre ambiguë.

Voir aussi