Membros de extensão para restrições SRTP

Uma função inline com parâmetros de tipo estaticamente resolvidos pode exigir um membro com um nome e assinatura específicos. O F# 11.2 permite que um membro de extensão dentro do âmbito satisfaça esse requisito, mesmo quando o tipo vem de outro assembly.

Use esta capacidade para separar um algoritmo genérico de adaptadores para tipos específicos. Não precisas de encapsular esses tipos ou adicionar uma interface nominal.

Note

No SDK .NET 11.0.100, ative <LangVersion>preview</LangVersion> onde o compilador resolve extensões. Consulte as definições de idioma F# 11.2 para a banda de funcionalidades planeada. Os exemplos de caminho e mapeamento usam uma biblioteca F# 11.0 e um consumidor de pré-visualização; ver Compilar bibliotecas e consumidores.

Adaptar uma propriedade para um algoritmo partilhado

O seguinte código de aplicação mede caminhos representados por dois tipos de frameworks existentes. Ambos os caminhos têm um comprimento 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

A fonte da biblioteca define Structural.Adapters e Structural.Algorithms. Os adaptadores expõem a mesma propriedade de coordenadas em tipos com diferentes APIs nativas:

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

O algoritmo pede uma Coordinates propriedade que devolve um par de valores de ponto flutuante:

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

Em cada chamada concreta, o compilador seleciona um Coordinates membro, chamado testemunha da restrição. O algoritmo em linha aceita uma propriedade intrínseca ou de extensão; caminhos vazios e de ponto único têm comprimento zero.

Partilhar uma operação entre tipos de coleções

As restrições de extensão também suportam membros estáticos. O módulo da Structural.Adapters biblioteca define adaptadores usando as funções e existentes Option.mapList.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

No módulo da Structural.Algorithms biblioteca, peça a um Map membro sobre o tipo de entrada completo:

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

A aplicação pode então partilhar uma transformação, alterar o tipo de elemento e compor mapeamento sobre uma coleção aninhada:

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]

O compilador verifica os nomes e tipos dos membros, não interfaces ou leis de mapeamento. Isto não é polimorfismo de tipo superior: nenhum parâmetro representa um construtor de contentores como F<_>.

Extensões de operadores

Os operadores podem usar o mesmo mecanismo. Por exemplo, defina um operador de mapeamento para listas e opções:

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

Um consumidor abre StructuralOperators e utiliza o operador:

open StructuralOperators

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

As soluções de operadores primitivos incorporadas mantêm prioridade para tipos de correspondência. Uma extensão não fornece alargamento numérico misto arbitrário nem substitui todos os operadores incorporados.

O modo F# 11.0 pode aceitar uma declaração comum de operador de extensão com FS1215, mas não pode usá-la como solução SRTP. O projeto do operador utiliza pré-visualização.

Operadores de tuplas e contexto

Uma coleção de observações e um par de resultados acumulados independentemente podem participar no mesmo protocolo combinado. O seguinte consumidor define o seu algoritmo genérico após abrir os adaptadores da biblioteca:

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

Estas extensões de biblioteca dão ao operador um significado para inteiros, listas e pares:

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 requer uma lista não vazia porque utiliza List.reduce. A operação preserva a ordem da lista; o compilador não impõe associatividade nem fornece uma identidade para uma entrada vazia.

Cabeçalhos type ('T1 * 'T2) with nativos e type struct ('T1 * 'T2) with extensão das tuplas de referência e valor CLR. Ambos são funcionalidades F# 11.2 e suportam elementos de dois a sete; cabeçalhos maiores produzem FS3915.

Tuplas também podem conter contexto juntamente com um valor. O operador seguinte acrescenta um trilho enquanto aplica uma função contextual:

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)

Chamadas de instância Tuple dot e cabeçalhos nativos de função como type ('a -> 'b) with não são suportados.

Tanto a biblioteca de composições como o utilizador usam pré-visualização. Mantenha combineAll o consumidor: exportar essa redução pode produzir FS0193 para a tupla ++. Os exemplos separados de caminho e mapeamento exportam os seus algoritmos.

Construção dirigida pelo retorno

Para uma operação que introduz um valor simples num contentor, o tipo de saída pode identificar qual o elemento que deve construí-lo:

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"

A anotação do resultado do consumidor seleciona a extensão do Return tipo de saída. Só a entrada não consegue escolher um contentor.

Cada contentor fornece o seu próprio membro. Para sobrecargas apenas de retorno num único tipo, use o atributo separadoAllowOverloadOnReturnType.

Compilar bibliotecas e consumidores

As definições linguísticas aplicam-se por projeto, não transitivamente através de referências. A matriz seguinte utiliza as definições do SDK .NET 11.0.100 para funcionalidades do F# 11.2:

Implementação da biblioteca Utilização pelo consumidor Requisito de pré-visualização
Declara cabeçalhos nativos de extensão de tupla. Resolve os seus operadores de extensão. Ambos precisam de pré-visualização; a nova sintaxe do cabeçalho em si é pré-visualização.
Define extensões ordinárias e exporta uma restrição de membro inline não resolvida. Fornece tipos de betão cujas extensões resolvem essa restrição. Pré-visualização das necessidades do consumidor. A biblioteca pode usar F# 11.0.
Resolve internamente uma restrição de extensão e expõe uma API inline ordinária ou já resolvida. Chama essa API sem resolução adicional de extensão. A biblioteca precisa de pré-visualização; o consumidor pode usar F# 11.0.
Resolve restrições de extensões e exportações que o consumidor também resolve através de extensões. Resolve as restrições restantes. Ambos precisam de pré-visualização.

A biblioteca de caminho e mapeamento usa 11.0; o seu consumidor usa preview. A partir do diretório de consumidor, executa dotnet run com um SDK F#11 de suporte.

Âmbito de extensão entre assemblagens

Use extensões públicas e abra o seu módulo antes de definir funções dependentes em linha. Para uma restrição importada não resolvida, também abrir os adaptadores no consumidor; as declarações do open produtor não são transferidas. Uma API já resolvida não exige que o consumidor reabra os seus adaptadores.

Inferência e assinaturas

A resolução de F# 11.2 adia algumas escolhas que o modo F# 11.0 faz anteriormente. Considere um assistente inline definido com um DateTime recetor:

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

No modo F# 11.0, esta forma pode inferir DateTime -> TimeSpan -> DateTime. Com a resolução de extensão F# 11.2 ativada, pode manter uma restrição para (DateTime or ^Offset) : (static member (+) : DateTime * ^Offset -> ^Result). Um consumidor pode então selecionar uma extensão pública dentro do âmbito para um tipo de offset diferente.

Um consumidor não pode recuperar a generalidade perdida numa biblioteca F# 11.0. Verifique as assinaturas exportadas; o produtor pode também precisar de ter a inferência F# 11.2 ativada.

Limitações de pré-visualização e resolução de problemas

  • Para FS0001 ou FS0193, verifique as definições de idioma, visibilidade pública e o âmbito do adaptador.
  • Se um ficheiro produtor posterior resolver o exemplo de caminho para Complex, a geração de código pode falhar com FS0971, Undefined value 'get_Coordinates'. Mantém essa resolução com as suas definições ou num consumidor separado.
  • Adaptadores concorrentes podem tornar a escolha dos membros ambígua.

Ver também