Novidades em F#11

Bem-vindo ao F# 11! Esta versão ajuda a manter os modelos de domínio distintos, a reutilizar algoritmos genéricos, a coordenar o trabalho assíncrono e a reduzir o uso de memória do compilador.

Obrigado à comunidade F# pelas propostas, implementações, revisões e testes por detrás destas mudanças. As hiperligações dos colaboradores acompanham as funcionalidades apresentadas abaixo.

Note

Os exemplos requerem um compilador F#11 e FSharp.Core 11 com as funcionalidades mostradas. No SDK .NET 11.0.100, as funcionalidades do F# 11.2 requerem <LangVersion>preview</LangVersion>; veja definições de linguagem e suporte planeado do SDK. O exemplo runtime-async tem requisitos adicionais de runtime; uma versão anterior do SDK do .NET 11, por si só, poderá não incluir todas as alterações.

Expansões de registos: modelos distintos sem mapeamentos repetitivos

Para manter os modelos de armazenamento e aplicação distintos, as expansões de registos reutilizam os seus campos partilhados sem declarações repetidas. Aqui, os modelos partilham dados do cliente, mas usam diferentes tipos de identificadores:

open System

type CustomerId = CustomerId of Guid

type CustomerDetails = { Name: string; PreferredLanguage: string }

type CustomerRow =
    { ...CustomerDetails
      Id: string
      InternalNote: string }

type Customer =
    { ...CustomerDetails
      Id: CustomerId }

...CustomerDetails contribui com declarações de campo. Na fronteira, ...row copia valores enquanto a conversão analisa o identificador:

let toCustomer (row: CustomerRow) =
    match Guid.TryParse row.Id with
    | true, id -> Ok ({ ...row; Id = CustomerId id }: Customer)
    | false, _ -> Error "Invalid customer identifier."

O destino mantém os dados partilhados, substitui Id, e exclui InternalNote. Guid.TryParse, e não o spread, valida o identificador; entrada mal formada produz Error.

Isto aborda o problema do mapeamento da camada de aplicação. Os spreads de valor também combinam registos nomeados e anónimos; os spreads de tipo criam apenas registos nomeados. Os campos posteriores prevalecem, e as fontes são executadas uma vez sequencialmente. Consulte Operadores de spread de registos para exemplos de composição e riscos de evolução partilhada.

Obrigado a @brianrourkeboll pelos spreads recorde.

Membros de extensão para algoritmos genéricos, operadores e tuplas

Para partilhar um algoritmo entre tipos existentes, F# 11.2 permite que os membros de extensão satisfaçam restrições de parâmetros de tipo resolvidos estaticamente (SRTP). Por exemplo, uma transformação pode lidar com listas e opções sem tipos de wrapper.

Note

No SDK .NET 11.0.100, ativa <LangVersion>preview</LangVersion> onde as extensões são resolvidas e abre o respetivo módulo. Consulte os requisitos da biblioteca.

Partilhe uma transformação em listas e opções

Com o map da biblioteca em escopo, a aplicação pode limpar qualquer representação, alterar tipos de elementos e compor transformações:

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]

Os adaptadores em Structural.Adapters utilizam operações familiares, em vez de impor novos contentores:

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

A função partilhada em Structural.Algorithms solicita ao tipo de entrada um elemento estático Map:

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

^Input e ^Output são tipos estaticamente resolvidos. Em cada chamada concreta, o compilador seleciona o membro com a assinatura necessária. As extensões não adicionam uma implementação de interface. A aplicação completa abre ambos os módulos.

Operadores, contexto e tipos de saída

O mesmo mecanismo suporta operadores de tuplas e propagação de contexto e construção dirigida à saída. Cabeçalhos nativos de tuplas necessitam da funcionalidade F# 11.2 ativada nas suas declarações, bem como nos locais de chamada onde ocorre a resolução de extensões.

Uma adição à parte, AllowOverloadOnReturnType, permite a uma fábrica declarar sobrecargas distinguidas pelos seus retornos:

type Inject =
    [<AllowOverloadOnReturnType>]
    static member Pure(value: 'T) : 'T list = [value]

    [<AllowOverloadOnReturnType>]
    static member Pure(value: 'T) : 'T option = Some value

let batch: string list = Inject.Pure "ready"
let optional: string option = Inject.Pure "ready"
// ["ready"] and Some "ready"

Este atributo Core 11 não requer pré-visualização. A anotação do resultado seleciona uma sobrecarga; sem ela, a chamada permanece ambígua. Ver Selecionar por tipo de retorno.

Obrigado à @gusty por estender o suporte à programação genérica.

Integração .NET e experiência de desenvolvimento

Os delegados diretos preservam o contrato real do handler

Para expor aos frameworks os nomes dos processadores e os atributos de ligação, o F# 11 pode ter diretamente como destino uma função nomeada compatível, em vez de um invólucro. As APIs Minimal do ASP.NET Core podem então ler o contrato da rota, do serviço e do cabeçalho HTTP:

open System
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.DependencyInjection

type ShipmentStore() =
    member _.Status trackingId =
        if trackingId = "SHP-1042" then "Dispatched" else "Unknown"

let status
    (trackingId: string)
    ([<FromServices>] store: ShipmentStore)
    ([<FromHeader(Name = "X-Customer")>] customer: string) =
    $"{customer}: {trackingId} is {store.Status trackingId}"

let handler = Func<string, ShipmentStore, string, string>(status)

Veja o projeto executável e regras de construção/compatibilidade. Capturas e adaptação de argumentos ainda podem exigir wrappers.

Obrigado à @kerams pelos delegados diretos.

Reutilizar documentação XML

Para manter as observações partilhadas sobre a API num único local, coloque-as em shared.xml:

<docs>
  <tracking>
    <remarks>An unrecognized tracking identifier has status Unknown.</remarks>
  </tracking>
</docs>

Com GenerateDocumentationFile ativado, o F# 11 expande <include> nas entradas de membro geradas:

/// <summary>Find the latest status of a shipment.</summary>
/// <include file="shared.xml" path="/docs/tracking/*"/>
let status trackingId =
    if trackingId = "SHP-1042" then "Dispatched" else "Unknown"

/// <summary>Find the latest statuses of several shipments.</summary>
/// <include file="shared.xml" path="/docs/tracking/*"/>
let statuses trackingIds =
    trackingIds |> List.map (fun id -> id, status id)

A implementação de inclusão atualiza ambas as descrições a partir de um único fragmento. Separadamente, o resolvedor de tooling expõe descrições base para <inheritdoc/>; o compilador deixa essa tag para os consumidores de documentação. Consulte a documentação XML para exemplos e regras de resolução.

Obrigado a @T-Gro pela documentação XML partilhada no FSharp.Core.

Depurar funções inline em compilações não otimizadas

Para inspecionar os argumentos de funções auxiliares inline, defina um ponto de interrupção em let total numa compilação não otimizada e avance para dentro de addFee. F# 11 agora prefere métodos invocáveis nestas compilações:

let inline addFee (fee: decimal) (amount: decimal) =
    let total = amount + fee
    total

let invoiceTotal = addFee 2m 40m
printfn "%M" invoiceTotal
// 42

As compilações otimizadas mantêm o inlining. Algumas construções ainda têm de ser expandidas. Consulte Depurar funções em linha para conhecer esses limites e o controlo --always-inline.

Assíncrona e coordenação de tarefas

Para ligar fluxos de trabalho F# a APIs baseadas em tarefas, o FSharp.Core 11 adiciona pontes conscientes do cancelamento, lotes limitados e transformações de resultados.

I/O pendente vinculada, não apenas a coleção de resultados

Um grande lote de leituras pode gerar mais trabalho do que uma aplicação deveria ter em espera. Para decidir quando começa o trabalho baseado em tarefas, um coordenador precisa de factories, não de tarefas que já tenham começado:

open System.IO
open System.Threading

let readFiles (paths: seq<string>) (ct: CancellationToken) =
    paths
    |> Seq.map (fun path ->
        fun token -> File.ReadAllTextAsync(path, token))
    |> Task.parallelLimit 4 ct

No máximo, quatro leituras são executadas em simultâneo; os resultados mantêm a ordem de entrada. Uma falha solicita o cancelamento de elementos irmãos através dos tokens fornecidos. Isto limita a concorrência, não os pedidos por segundo. Veja funções assíncronas e de tarefa para variantes sequenciais e de resultado unitário.

Mantém o cancelamento e a exceção subjacente ao atravessar o limite

Quando um fluxo de trabalho inicia trabalho externo, o seu token deve viajar com esse trabalho. Async.StartTaskImmediate Passa o token do fluxo de trabalho em execução para a fábrica de tarefas e aguarda a conclusão:

open System.IO

let tryReadFile path =
    async {
        try
            let! contents =
                Async.StartTaskImmediate(fun ct -> File.ReadAllTextAsync(path, ct))
            return Some contents
        with :? FileNotFoundException ->
            return None
    }

Uma única falha de tarefa chega a este manipulador na forma do elemento subjacente FileNotFoundException. Para uma tarefa ou tarefa de valor já criada, use Async.Await; a mais antiga Async.AwaitTask mantém o seu comportamento de envolvimento de exceções.

Para chamadores baseados em tarefas, Task.startAsyncImmediate token workflow fornece a ponte inversa. Começa no thread de chamada até à primeira suspensão assíncrona.

Utilize auxiliares de correspondência para transformações breves

Para transformar um resultado sem uma expressão de cálculo completa, use as funções auxiliares de correspondência em Microsoft.FSharp.Control. Por exemplo, lê um ficheiro, obtém o seu tamanho e devolve uma falha ordinária como Error:

open System.IO
open System.Threading

let fileLength (path: string) (ct: CancellationToken) =
    File.ReadAllTextAsync(path, ct)
    |> Task.map String.length
    |> Task.catch

Um ficheiro que contém Report 1 fornece Ok 8; um ficheiro em falta dá um Error que contém FileNotFoundException. O cancelamento continua a propagar-se. Async e ValueTask oferecem helpers correspondentes map, bind e de erro; veja Funções assíncronas e de tarefa. Use uma expressão de cálculo para ramos ou vários valores dependentes.

Os ajudantes do ValueTask requerem Core's netstandard2.1 ou net10.0 build e consumo único.

Obrigado a @bartelink pelas contribuições para APIs assíncronas/tarefas.

Lançamento novamente após reporte assíncrono de falhas

O registo operacional deve adicionar informação sem alterar o que o chamador captura ou esconder onde a falha começou. Os manipuladores de expressões de computação podem agora usar reraise () mesmo depois de aguardarem o logger:

Note

Computation-expression rethrow é uma funcionalidade de F# 11.2. No SDK .NET 11.0.100, ative a opção <LangVersion>preview</LangVersion>.

open System
open System.IO
open System.Threading.Tasks

let readReport (logFailure: exn -> Task) path =
    task {
        try
            return! File.ReadAllTextAsync(path)
        with :? IOException as error ->
            do! logFailure error
            return reraise ()
    }

Após registo bem-sucedido, a mesma exceção e a informação original do site de lançamento propagam-se. Uma falha no logger pode substituir o original. Isto responde à solicitação de observabilidade assíncrona sem assincronismo em tempo de execução. Ver Relançar a partir de uma expressão de cálculo.

Agradecemos a @xperiandri por este apoio.

O FSharp.Core pode tirar partido do .NET moderno

Para usar APIs modernas sem deixar de suportar destinos mais antigos, o FSharp.Core acrescenta net10.0 a par de netstandard2.0 e netstandard2.1. O NuGet seleciona a compilação compatível. O empacotamento atualizado da ferramenta também permite a eliminação assíncrona baseada em tarefas no F# Interactive, tal como em projetos compilados. Consulte as plataformas de destino do FSharp.Core.

Obrigado a @T-Gro por suporte moderno ao Core e empacotamento de ferramentas do SDK.

Assíncrono em tempo de execução para autores de builders

Os autores de builders podem permitir que o ambiente de execução suspenda e retome os métodos assinalados, em vez de utilizar máquinas de estados geradas pelo compilador. Um construtor experimental, definido numa biblioteca, expõe o mecanismo como uma expressão de computação:

let readManifest (path: string) =
    runtimeTask {
        let! text = System.IO.File.ReadAllTextAsync path
        return text.Length
    }

Note

Este construtor experimental utiliza uma funcionalidade do F# 11.2 em que a sua implementação inline é definida ou expandida; no SDK .NET 11.0.100, ativa a pré-visualização. Também requer a build e referências do net10.0 Core 11 e um runtime com suporte a tempo de execução assíncrono. A combinação verificada de runtime/referência aqui é .NET 11 RC1. Ter como destino apenas o .NET 10 não é suficiente. Consulte experiências assíncronas em tempo de execução.

O FSharp.Core não disponibiliza este exemplo como um construtor de uso geral. Ordinary task and async code não muda automaticamente para ele, e o código que chama uma API normal já compilada que retorna tarefas não precisa inerentemente de pré-visualização.

Obrigado a @majocha pelo assincronismo em tempo de execução.

Texto nativo compatível com AOT e arrays bidimensionais

Para suportar o AOT nativo, o F# 11 remove os requisitos de formatação baseada em reflexão e alocação dinâmica de operações de texto e matrizes mais comuns.

Quando já tens uma string, não precisas de um formato "%s" só para a escrever. Novas funções print Core e printn aceitam valores através da conversão de string F#; printn acrescenta uma nova linha:

let report (processed: int) (total: decimal) (elapsed: TimeSpan) : string =
    $"{processed} items, total {total:F2}, elapsed {elapsed.TotalMilliseconds:F0} ms"

printn (report 12 145.50m (TimeSpan.FromMilliseconds 250.0))
// 12 items, total 145.50, elapsed 250 ms

A interpolação de cadeias utiliza conversão direta e String.Concat em mais casos. Marcadores de formato .NET, como {total:F2}, usam formatação invariante e encapsulam o valor; %A e formulários printf com precisão ainda podem necessitar de formatação printf. Selecione uma cultura explícita para saída localizada.

Com --reflectionfree, os registos e as uniões também ganham ToStringsaída gerada. Consulte Interpolação e Native AOT para exemplos culturais e caminhos suportados.

Obrigado à @bbatsov pelas funções de impressão e à @charlesroddie pela implementação da interpolação.

Mantém as operações normais de grelha baseadas em zero

Uma grelha baseada em zero não deveria exigir um módulo Array2D substituto só para evitar um caminho de alocação de código dinâmico. Implementações Core atualizadas suportam inicializações e transformações familiares:

let grid = Array2D.init 3 4 (fun row column -> row * 4 + column)
let doubled = grid |> Array2D.map (fun value -> value * 2)
let snapshot = Array2D.copy doubled

printn $"{snapshot.GetLength(0)} x {snapshot.GetLength(1)}; last = {snapshot[2, 3]}"
// 3 x 4; last = 22

Array2D.create, init, e rebase usam o caminho de alocação baseado em zero entre builds Core. mapque preservam os limites, mapi e copy podem usar uma API de alocação .NET mais recente na compilação net10.0; as versões do .NET Standard mantêm o seu mecanismo alternativo. Os arrays que não são baseados em zero não estão obsoletos, mas as suas restrições nativas de alocação mantêm-se.

Consulte Arrays bidimensionais e AOT Nativo para caminhos de alocação e restrições remanescentes. Verifique os diagnósticos de publicação completos da aplicação.

Obrigado à @T-Gro pelas melhorias no Array2D.

Menos memória retida e menos trabalho repetido

F# 11 reduz a análise repetida do compilador e algumas alocações de aplicação. Estes fatores afetam diferentes cargas de trabalho.

Partilhe bibliotecas referenciadas entre projetos

Para evitar cópias repetidas de dependências binárias partilhadas, o serviço da linguagem pode partilhar grafos de biblioteca importados elegíveis entre projetos.

Em a experiência de memória retida do PR de implementação, a solução ReSharper.FSharp de dez projetos caiu de 1.547,2 MB para 1.002,2 MB com a partilha ativada. Isso é uma medição desligado/ligado para essa carga de trabalho, não uma redução geral de F# de 10 para 11. Soluções dominadas por referências de projeto em vez de referências binárias partilhadas podem apresentar ganhos muito menores.

A leitura de metadados e a especialização inline também reduzem as alocações temporárias, que são uma métrica separada. Consulte Desempenho do ferramental para informações sobre o âmbito e os controlos.

Obrigado à @auduchinok pelas importações partilhadas da biblioteca.

Compilar trabalho independente em paralelo por defeito

A compilação paralela está agora ativada por defeito, mantendo a saída determinística e as regras de ordem de origem. A sobrecarga de cache e as melhorias de padrões guardados evitam outros trabalhos repetidos. O Visual Studio/FCS também reduz a análise de documentos inalterados e limita verificações concorrentes; ver Desempenho das ferramentas.

Evite a sobrecarga de callback e alocação de resultados vazios

Selecionado List e Array funciona agora callbacks inline. Para os seus próprios ajudantes inline, OptimizeClosureIfNotInlined adapte uma referência opaca com caril uma vez:

let inline foldItems
    ([<InlineIfLambda; OptimizeClosureIfNotInlined>] folder)
    state
    (items: 'T[]) =
    let mutable total = state
    for item in items do
        total <- folder total item
    total

Outras alocações podem permanecer. Consulte Otimizar callbacks com curry para os requisitos.

Resultados vazios são outro caso comum de trabalho concluído com êxito. A carga de trabalho motivadora do FSharpLintcombinava repetidamente regras que não encontravam avisos. O Core agora reutiliza matrizes vazias partilhadas em mais operações:

let checkTitle (title: string) =
    if String.IsNullOrWhiteSpace title then [|"A title is required."|]
    else Array.empty

let warnings = ["Introduction"; "Installation"] |> List.toArray |> Array.collect checkTitle
let sharedEmpty = obj.ReferenceEquals(warnings, Array.empty<string>)
// true: no new empty result array

A alteração remove objetos de array vazio redundantes, não todas as alocações no pipeline. Não confie que resultados vazios separados tenham identidades de referência distintas.

Obrigado @T-Gro pelas recolhas inline callbacks e pelos arrays vazios partilhados.

Chamadas API mais precisas e conveniências do dia a dia

Exigir nomes quando a ordem dos parâmetros gerados pode mudar

Para proteger as APIs geradas de alterações silenciosas na ordem dos parâmetros, um autor pode exigir argumentos nomeados:

Note

A imposição é uma funcionalidade do F# 11.2; no SDK .NET 11.0.100, ativar a pré-visualização. Também é necessária uma System.Diagnostics.CodeAnalysis.RequireNamedArgumentsAttribute definição disponível. O exemplo completo fornece uma definição de compatibilidade para referências que não a possuem.

open System.Diagnostics.CodeAnalysis

type Query =
    [<RequireNamedArguments>]
    static member Execute(numerator: decimal, denominator: decimal) =
        numerator / denominator

let ratio = Query.Execute(numerator = 3m, denominator = 4m)
// 0.75M; Query.Execute(3m, 4m) is rejected.

Reordenar a declaração preserva o resultado da chamada nomeada, 0.75M; uma chamada posicional é rejeitada. Para o caso de utilização da API gerada, o fornecedor deve emitir a anotação.

Reconhecer um argumento genérico já enrolado

Para evitar envolver uma tarefa existente como um valor de resultado simples, uma nova regra de sobrecarga de último recurso prefere Task<'T> em vez do simples 'T:

Note

O desempate mais concreto e o comportamento de prioridade de sobrecarga são funcionalidades do F# 11.2; no SDK .NET 11.0.100, ative as funcionalidades de pré-visualização.

open System.Threading.Tasks

let pending = Task.FromResult 42
let wrapped = ValueTask<_>(pending)
let answer: int = wrapped.Result
// ValueTask<int>, not ValueTask<Task<int>>

O resultado é ValueTask<int>, não ValueTask<Task<int>>. Consulte Overload selection para os limites da regra.

Prefira uma nova sobrecarga sem remover o ponto de entrada antigo

Uma biblioteca pode manter um método de diagnóstico antigo para binários existentes, ao mesmo tempo que adiciona uma sobrecarga de método que inclui a localização no código-fonte do chamador. OverloadResolutionPriorityAttribute Torna essa preferência explícita para os chamadores recém-compilados:

open System.Runtime.CompilerServices

type Check =
    static member That(condition: bool) =
        if condition then "OK" else "Check failed."

    [<OverloadResolutionPriority(1)>]
    static member That(condition: bool, [<CallerLineNumber>] ?line: int) =
        if condition then "OK" else $"Check failed at line {defaultArg line 0}."

let stock = 0
let diagnostic = Check.That(stock > 0)
// The diagnostic includes the caller's source line.

A chamada inalterada seleciona a versão sobrecarregada sensível à linha. Consulte seleção de sobrecarga para regras de âmbito e compatibilidade.

Use as promessas de nulidade condicional do .NET

Se o caminho do ficheiro não for nulo, um resultado que pode ser nulo de uma API Path não deve obrigar a uma verificação que o próprio contrato exclui. O F# 11 baseia-se na garantia existente do .NET NotNullIfNotNull: um parâmetro nomeado não nulo implica um resultado não nulo.

open System.IO

let fileName: string = Path.GetFileName("reports/annual.csv")
let extension: string = Path.GetExtension(fileName)
printn (extension.ToUpperInvariant())
// .CSV

let fileNameOf (path: string | null) : string | null =
    Path.GetFileName path

Ativar <Nullable>enable</Nullable>. Parâmetros anuláveis continuam a precisar de tratamento de valores anuláveis; ver Contratos de retorno condicional. Para APIs C# T? sem restrições, use o Unchecked.withNullassistente separado.

Obrigado à @kerams pelo suporte de anulabilidade condicional.

Melhorias linguísticas menores

Para chamar um tipo de registo com sintaxe de construtor .NET, use construtores de registo F# 11.2:

type Person = { Name: string; Age: int }
let person = Person(Name = "Ada", Age = 36)

Argumentos nomeados evitam a dependência da ordem dos campos. Obrigado à @charlesroddie por esta funcionalidade.

Para fonte condicional, #elif evita grupos aninhados #if :

#if EU_WAREHOUSE
let dispatchTimeZone = "Europe/Paris"
#elif US_WAREHOUSE
let dispatchTimeZone = "America/New_York"
#else
let dispatchTimeZone = "UTC"
#endif

System.Console.WriteLine(dispatchTimeZone)

A primeira ramificação correspondente vence. O projeto de exemplo define EU_WAREHOUSE e imprime Europe/Paris; sem nenhum símbolo definido, a saída é UTC.

Para ferramentas relacionadas e casos de interoperabilidade:

Antes de atualizar

Tenha em mente estas mudanças importantes ao atualizar:

Código afetado Action
Bibliotecas com .fsi ficheiros Copiar atributos de contrato aplicados às assinaturas; as omissões são agora erros FS3888.
Bibliotecas inline suportadas por extensões Reveja as assinaturas inferidas e os requisitos de produtor/consumidor. A inferência F# 11.2 pode manter restrições mais gerais.
Delegados de método de instância Validar recetores antes da construção direta, quando ocorrem falhas de recetores nulos.
Saída interpolada localizada Os buracos no formato .NET usam cultura invariante. Escolha uma cultura explícita quando se pretende uma saída localizada.
Construtores personalizados com do! final Associação de fornecimento e conclusão de unidades; o final do! já não seleciona os membros de reencaminhamento final.
Inacabado let! Adicione o seguinte corpo de cálculo; uma ligação incompleta tem agora um diagnóstico mais direto.
Configurações linguísticas mais antigas Os modos numerados suportados começam em F# 8; o modo de indentação não estrita é removido. Ver Versão da linguagem e indentação.
Operações bit a bit de enum Utilize enums com suporte integral; tipos de suporte inválidos são rejeitados.