Membri dell'estensione per i vincoli SRTP

Una funzione inline con parametri di tipo risolti in modo statico può richiedere un membro con un nome e una firma specifici. F# 11.2 consente a un membro dell'estensione nell'ambito di soddisfare tale requisito, anche quando il tipo proviene da un altro assembly.

Usare questa funzionalità per separare un algoritmo generico dagli adattatori per determinati tipi. Non è necessario eseguire il wrapping di questi tipi o aggiungervi un'interfaccia nominale.

Note

Nella .NET 11.0.100 SDK abilitare <LangVersion>preview</LangVersion> la posizione in cui il compilatore risolve le estensioni. Per la banda di funzionalità pianificata , vedere Impostazioni della lingua F# 11.2 . Gli esempi di percorso e mapping usano una libreria F# 11.0 e un consumer di anteprima; vedere Compilare librerie e consumer.

Adattare una proprietà per un algoritmo condiviso

Il codice dell'applicazione seguente misura i percorsi rappresentati da due tipi di framework esistenti. Entrambi i percorsi hanno una lunghezza di 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

L'origine della libreria definisce Structural.Adapters e Structural.Algorithms. Gli adapter espongono la stessa proprietà di coordinate sui tipi con API native diverse:

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'algoritmo richiede una Coordinates proprietà che restituisce una coppia di valori a virgola mobile:

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))

A ogni chiamata concreta, il compilatore seleziona un Coordinates membro, denominato server di controllo del vincolo. L'algoritmo inline accetta una proprietà intrinseca o di estensione; i percorsi vuoti e a punto singolo hanno lunghezza zero.

Condividere un'operazione tra tipi di raccolta

I vincoli di estensione supportano anche membri statici. Il modulo della Structural.Adapters libreria definisce gli adattatori usando le funzioni e Option.map esistentiList.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

Nel modulo della Structural.Algorithms libreria richiedere un Map membro nel tipo di input completo:

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

L'applicazione può quindi condividere una trasformazione, modificare il tipo di elemento e comporre il mapping in una raccolta annidata:

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]

Il compilatore controlla i nomi e i tipi dei membri, non interfacce o leggi di mapping. Questo non è polimorfismo di tipo superiore: nessun parametro rappresenta un costruttore del contenitore, F<_>ad esempio .

Estensioni dell'operatore

Gli operatori possono usare lo stesso meccanismo. Ad esempio, definire un operatore di mapping per elenchi e opzioni:

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))

Viene aperto StructuralOperators un consumer e viene usato l'operatore :

open StructuralOperators

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

Le soluzioni degli operatori primitive predefinite mantengono la priorità per i tipi corrispondenti. Un'estensione non fornisce un'estensione numerica mista arbitraria o sostituisce tutti gli operatori predefiniti.

La modalità F# 11.0 può accettare una normale dichiarazione di operatore di estensione con FS1215, ma non può usarla come soluzione SRTP. Il progetto dell'operatore usa l'anteprima.

Operatori di tupla e contesto

Una raccolta di osservazioni e una coppia di risultati accumulati in modo indipendente possono partecipare allo stesso protocollo di combinazione. Il consumer seguente definisce l'algoritmo generico dopo l'apertura degli adattatori della libreria:

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"])

Queste estensioni della libreria danno all'operatore un significato per numeri interi, elenchi e coppie:

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 richiede un elenco nonempty perché usa List.reduce. L'operazione mantiene l'ordine di elenco; il compilatore non applica l'associatività o fornisce un'identità per un input vuoto.

Intestazioni type ('T1 * 'T2) with native ed type struct ('T1 * 'T2) with estendere tuple clr e valori. Entrambi sono funzionalità F# 11.2 e supportano da due a sette elementi; le intestazioni più grandi producono FS3915.

Le tuple possono anche trasportare il contesto insieme a un valore. L'operatore seguente aggiunge un trail durante l'applicazione di una funzione contestuale:

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)

Le chiamate dot-instance della tupla e le intestazioni native del tipo di funzione, type ('a -> 'b) with ad esempio, non sono supportate.

La libreria di composizione e il consumer usano entrambe l'anteprima. Mantenere combineAll nel consumer: l'esportazione di tale riduzione può produrre FS0193 per la tupla ++. Gli esempi di percorso e mapping separati esportano i relativi algoritmi.

Costruzione diretta di ritorno

Per un'operazione che introduce un valore normale in un contenitore, il tipo di output può identificare quale membro deve costruirlo:

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'annotazione dei risultati del consumer seleziona l'estensione del tipo di Return output. L'input da solo non può scegliere un contenitore.

Ogni contenitore fornisce il proprio membro. Per gli overload di sola restituzione in un singolo tipo, usare l'attributo separatoAllowOverloadOnReturnType.

Compilare librerie e consumer

Le impostazioni della lingua si applicano per ogni progetto, non transitivamente tramite riferimenti. La matrice seguente usa le impostazioni dell'SDK di .NET 11.0.100 per le funzionalità F# 11.2:

Implementazione della libreria Uso da parte del consumer Requisito di anteprima
Dichiara intestazioni di estensione tupla native. Risolve i relativi operatori di estensione. Entrambi necessitano dell'anteprima; la nuova sintassi dell'intestazione è in anteprima.
Definisce le estensioni normali ed esporta un vincolo membro inline non risolto. Fornisce tipi concreti le cui estensioni risolveranno tale vincolo. È necessaria l'anteprima per i consumer. La libreria può usare F# 11.0.
Risolve un vincolo di estensione internamente ed espone un'API inline normale o già risolta. Chiama l'API senza ulteriore risoluzione dell'estensione. La libreria necessita dell'anteprima; il consumer può usare F# 11.0.
Risolve anche le estensioni e i vincoli di esportazione risolti dal consumer tramite estensioni. Risolve i vincoli rimanenti. Entrambi richiedono l'anteprima.

Il percorso e la libreria di mapping utilizza 11.0; il relativo consumer usa preview. Dalla directory consumer eseguire dotnet run con un SDK F# 11 di supporto.

Ambito dell'estensione tra assembly

Usare le estensioni pubbliche e aprire il modulo prima di definire funzioni inline dipendenti. Per un vincolo non risolto importato, aprire anche gli adattatori nel consumer; le dichiarazioni del open produttore non vengono trasferite. Un'API già risolta non richiede al consumer di riaprire le schede.

Inferenza e firme

La risoluzione F# 11.2 sfida alcune scelte effettuate in precedenza dalla modalità F# 11.0. Si consideri un helper inline definito con un DateTime ricevitore:

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

In modalità F# 11.0 questo modulo può dedurre DateTime -> TimeSpan -> DateTime. Con la risoluzione dell'estensione F# 11.2 abilitata, può mantenere un vincolo per (DateTime or ^Offset) : (static member (+) : DateTime * ^Offset -> ^Result). Un consumer può quindi selezionare un'estensione pubblica nell'ambito per un tipo di offset diverso.

Un consumer non può recuperare la generalità persa in una libreria F# 11.0. Controllare le firme esportate; il producer potrebbe anche richiedere l'inferenza F# 11.2 abilitata.

Limitazioni e risoluzione dei problemi di anteprima

  • Per FS0001 o FS0193, controllare le impostazioni della lingua, la visibilità pubblica e l'ambito dell'adattatore.
  • Se un file producer successivo risolve l'esempio di percorso per Complex, la generazione del codice può avere esito negativo con FS0971, Undefined value 'get_Coordinates'. Mantenere la risoluzione con le relative definizioni o in un consumer separato.
  • Gli adattatori concorrenti possono rendere ambigua la selezione dei membri.

Vedere anche