Membros de extensão para restrições SRTP

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

Use essa funcionalidade para separar um algoritmo genérico de adaptadores para tipos específicos. Você não precisa encapsular esses tipos ou adicionar uma interface nominal a eles.

Note

No SDK do .NET 11.0.100, habilite <LangVersion>preview</LangVersion> onde o compilador resolve extensões. Consulte as configurações de linguagem F# 11.2 para a faixa de recursos planejada. Os exemplos de caminho e mapeamento usam uma biblioteca F# 11.0 e um consumidor de visualização; consulte Compilar bibliotecas e consumidores.

Adaptar uma propriedade para um algoritmo compartilhado

O código do aplicativo a seguir mede caminhos representados por dois tipos de estrutura 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 origem da biblioteca define Structural.Adapters e Structural.Algorithms. Os adaptadores expõem a mesma propriedade de coordenadas em tipos com APIs nativas diferentes:

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 solicita uma Coordinates propriedade que retorna 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 embutido aceita uma propriedade intrínseca ou de extensão; caminhos vazios e de ponto único têm comprimento zero.

Compartilhar uma operação entre tipos de coleção

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

No módulo da Structural.Algorithms biblioteca, solicite um Map membro no tipo de entrada completo:

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

Em seguida, o aplicativo pode compartilhar uma transformação, alterar o tipo de elemento e redigir o mapeamento em 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 nomes e tipos de membros, não interfaces ou leis de mapeamento. Isso não é polimorfismo de tipo superior: nenhum parâmetro representa um construtor de contêiner, como F<_>.

Extensões de operador

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 usa 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 operador primitivo internas retêm prioridade para tipos correspondentes. Uma extensão não fornece ampliação arbitrária de numérico misto ou substitui todos os operadores internos.

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

Operadores e contexto de tupla

Uma coleção de observações e um par de resultados acumulados independentemente podem participar do mesmo protocolo de combinação. O consumidor a seguir define seu algoritmo genérico depois de 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"])

Essas 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 íntegra porque ela usa List.reduce. A operação preserva a ordem de lista; o compilador não impõe associatividade nem fornece uma identidade para uma entrada vazia.

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

Tuplas também podem levar contexto junto com um valor. O operador a seguir acrescenta uma trilha ao aplicar 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)

Não há suporte para chamadas de tupla de instância de ponto e cabeçalhos nativos do tipo função, como type ('a -> 'b) with não há suporte.

A biblioteca de composição e o consumidor usam a versão prévia. Mantenha-se combineAll no consumidor: exportar essa redução pode produzir FS0193 para tupla ++. O caminho separado e os exemplos de mapeamento exportam seus algoritmos.

Construção direcionada ao retorno

Para uma operação que introduz um valor simples em um contêiner, o tipo de saída pode identificar qual membro 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 de resultado do consumidor seleciona a extensão do tipo de Return saída. A entrada por si só não pode escolher um contêiner.

Cada contêiner fornece seu próprio membro. Para sobrecargas somente de retorno em um único tipo, use o atributo separadoAllowOverloadOnReturnType.

Compilar bibliotecas e consumidores

As configurações de idioma se aplicam por projeto, não transitivamente por meio de referências. A matriz a seguir usa as configurações do SDK do .NET 11.0.100 para recursos do F# 11.2:

Implementação da biblioteca Uso do consumidor Requisito de visualização
Declara cabeçalhos de extensão de tupla nativos. Resolve seus operadores de extensão. Ambos precisam de visualização; a sintaxe do novo cabeçalho em si é a visualização.
Define extensões comuns e exporta uma restrição de membro embutida não resolvida. Fornece tipos concretos cujas extensões resolvem essa restrição. O consumidor precisa de visualização. A biblioteca pode usar o F# 11.0.
Resolve uma restrição de extensão internamente e expõe uma API embutida comum ou já resolvida. Chama essa API sem mais resolução de extensão. A biblioteca precisa de visualização; o consumidor pode usar f# 11.0.
Resolve extensões e exporta restrições que o consumidor também resolve por meio de extensões. Resolve as restrições restantes. Ambos precisam de visualização.

O caminho e a biblioteca de mapeamento usam 11.0; seu consumidor usa preview. No diretório do consumidor, execute dotnet run com um SDK F# 11 de suporte.

Escopo da extensão entre assemblies

Use extensões públicas e abra seu módulo antes de definir funções embutidas dependentes. Para uma restrição não resolvida importada, abra também 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 seus adaptadores.

Inferência e assinaturas

A resolução F# 11.2 adia algumas opções que o modo F# 11.0 faz anteriormente. Considere um auxiliar embutido definido com um DateTime receptor:

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

No modo F# 11.0, esse formulário pode inferir DateTime -> TimeSpan -> DateTime. Com a resolução de extensão F# 11.2 habilitada, ela pode manter uma restrição para (DateTime or ^Offset) : (static member (+) : DateTime * ^Offset -> ^Result). Um consumidor pode selecionar uma extensão pública no escopo para um tipo de deslocamento diferente.

Um consumidor não pode recuperar a generalidade perdida em uma biblioteca F# 11.0. Verificar assinaturas exportadas; o produtor também pode precisar da inferência F# 11.2 habilitada.

Limitações de visualização e solução de problemas

  • Para FS0001 ou FS0193, verifique as configurações de idioma, a visibilidade pública e o escopo do adaptador.
  • Se um arquivo de produtor posterior resolver o exemplo de caminho para Complex, a geração de código poderá falhar com FS0971 Undefined value 'get_Coordinates'. Mantenha essa resolução com suas definições ou em um consumidor separado.
  • Adaptadores concorrentes podem tornar a seleção de membro ambígua.

Consulte também