Novidades no F# 11

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

Obrigado à comunidade F# pelas propostas, implementações, revisões e testes por trás dessas alterações. Os links para os colaboradores acompanham as funcionalidades abaixo.

Note

Os exemplos exigem um compilador F# 11 e o FSharp.Core 11 com os recursos mostrados. No SDK do .NET 11.0.100, os recursos do F# 11.2 exigem<LangVersion>preview</LangVersion>; consulte as configurações de idioma e o suporte planejado ao SDK. O exemplo de runtime-async tem requisitos adicionais de runtime; uma versão anterior do SDK do .NET 11 pode não incluir todas as alterações.

Espalhamento de registros: modelos distintos sem mapeamentos repetitivos

Para manter distintos os modelos de armazenamento e da aplicação, os record spreads reutilizam seus campos compartilhados sem repetir declarações. Aqui, os modelos compartilham dados do cliente, mas usam diferentes tipos de identificador:

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 fornece declarações de campos. Na fronteira, ...row copia valores enquanto a conversão faz o parsing do 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 detalhes compartilhados, substitui Id e exclui InternalNote. Guid.TryParse, não o spread, valida o identificador; a entrada malformada produz Error.

Isso resolve o problema de mapeamento da camada de aplicação. Os spreads de valores também combinam registros nomeados e anônimos; os spreads de tipos criam apenas registros nomeados. Os campos posteriores prevalecem, e as origens são executadas uma vez em ordem. Consulte operadores spread de registro para obter exemplos de composição e riscos de evolução compartilhada.

Graças a @brianrourkeboll pelas distribuições de registros.

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

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

Note

No SDK do .NET 11.0.100, habilite <LangVersion>preview</LangVersion> onde as extensões são resolvidas e abra seus módulos. Consulte os requisitos da biblioteca.

Compartilhar uma transformação entre listas e opções

Com a map da biblioteca disponível, o aplicativo pode limpar qualquer uma das representações, alterar tipos de elemento 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 usam operações familiares, em Structural.Adapters vez de impor novos contêineres:

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 shared em Structural.Algorithms solicita ao tipo de entrada um membro 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 resolvidos estaticamente. 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. O aplicativo completo abre os dois módulos.

Operadores, contexto e tipos de saída

O mesmo mecanismo dá suporte a operadores de tupla e propagação de contexto e construção direcionada à saída. Os cabeçalhos de tupla nativa precisam do recurso F# 11.2 habilitado em suas declarações, bem como nos locais de chamada de resolução de extensões.

Uma adição AllowOverloadOnReturnType, , permite que uma fábrica declare sobrecargas distinguidas por 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"

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

Graças a @gusty por estender o suporte de programação genérica.

integração com .NET e a experiência de desenvolvimento

Os delegados diretos preservam o contrato real do handler

Para expor nomes de manipuladores e atributos de associação a estruturas, o F# 11 pode direcionar uma função nomeada compatível diretamente em vez de um wrapper. ASP.NET Core APIs mínimas podem ler a rota, o serviço e o contrato de cabeçalho:

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)

Consulte o projeto executável/passível de execução e as regras de construção/compatibilidade. Capturas de variáveis e adaptação de argumento ainda podem exigir wrappers.

Graças a @kerams para delegados diretos.

Reutilizar documentação XML

Para manter os comentários compartilhados da API em um só lugar, coloque-os em shared.xml:

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

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

/// <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 de um fragmento. Separadamente, o resolvedor de ferramentas expõe descrições base para <inheritdoc/>; o compilador deixa essa marca para os consumidores de documentação. Consulte a documentação XML para obter exemplos e regras de resolução.

Agradecimentos a @T-Gro por compartilhar a documentação XML no FSharp.Core.

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

Para inspecionar argumentos auxiliares inline, defina um ponto let total de interrupção em um build não otimizado e use Entrar em addFee. O F# 11 agora prefere métodos que podem ser chamados nestes builds:

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 preservam a inserção embutida. Alguns construtos ainda devem ser expandidos. Consulte Depuração de funções embutidas para ver esses limites e o controle --always-inline.

Coordenação assíncrona de tarefas

Para conectar fluxos de trabalho F# com APIs baseadas em tarefas, o FSharp.Core 11 adiciona pontes com reconhecimento de cancelamento, lotes limitados e transformações de resultados.

Limitado por E/S pendente, não apenas a coleção de resultados

Um grande lote de leituras pode iniciar mais trabalho do que um aplicativo deve ter pendente. Para decidir quando o trabalho baseado em tarefas começa, um coordenador precisa de fábricas de tarefas, não de tarefas que já tenham sido iniciadas:

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 simultaneamente; os resultados retêm a ordem de entrada. Uma falha solicita o cancelamento de solicitações relacionadas por meio dos tokens fornecidos. Isso limita a simultaneidade, não as solicitações por segundo. Consulte funções assíncronas e task para variantes sequenciais e de retorno unitário.

Manter o cancelamento e a exceção subjacente através da barreira

Quando um fluxo de trabalho inicia o trabalho externo, 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 como o FileNotFoundException subjacente. Para uma tarefa ou tarefa de valor já criada, use Async.Await; o mais antigo Async.AwaitTask mantém seu comportamento de encapsulamento de exceção.

Para processos chamadores baseados em tarefas, Task.startAsyncImmediate token workflow fornece a ponte inversa. A execução começa na thread de chamada até a primeira suspensão assíncrona.

Usar auxiliares correspondentes para transformações curtas

Para transformar um resultado sem uma expressão de computação completa, use os helpers correspondentes em Microsoft.FSharp.Control. Por exemplo, leia um arquivo, obtenha seu tamanho e retorne uma falha comum 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 arquivo que contém Report 1 gera Ok 8; um arquivo inexistente gera um Error contendo FileNotFoundException. O cancelamento ainda é propagado. Async e ValueTask oferecem map, bind e auxiliares de erro correspondentes; consulte funções assíncronas e de tarefa. Use uma expressão de computação para branches ou vários valores dependentes.

Os auxiliares do ValueTask exigem o build ou net10.0 do Core e netstandard2.1.

Agradecimentos a @bartelink pelas contribuições para a API async/task.

Propagar novamente após relatório de falha assíncrona

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

Note

O relançamento de expressão de computação é um recurso do F# 11.2. No SDK do .NET 11.0.100, habilite <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 o registro em log bem-sucedido, a mesma exceção e informações originais do site de lançamento são propagadas. Uma falha do logger pode substituir o original. Isso aborda a solicitação de observabilidade assíncrona sem assincronia em tempo de execução. Consulte Relançamento em uma expressão de computação.

Agradecemos a @xperiandri por este suporte.

O FSharp.Core pode se basear em .NET modernas

Para usar APIs modernas sem abandonar plataformas mais antigas, FSharp.Core adiciona net10.0 ao lado de netstandard2.0 e netstandard2.1. O NuGet seleciona a compilação compatível. O empacotamento atualizado da ferramenta também permite o descarte assíncrono baseado em tarefas no F# Interactive, assim como em projetos compilados. Consulte os frameworks de destino do FSharp.Core.

Agradecimentos a por @T-Gro e empacotamento das ferramentas do SDK.

Assíncrono de runtime para autores do construtor

Os autores de builders podem permitir que o tempo de execução suspenda e retome os métodos marcados, em vez de usar máquinas de estado geradas pelo compilador. Um builder experimental definido pela biblioteca expõe o mecanismo como uma expressão computacional:

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

Note

Este construtor experimental usa um recurso do F# 11.2 no qual a implementação inline é definida ou expandida; no SDK .NET 11.0.100, ative a versão prévia. Também requer a compilação e as referências de net10.0 do Core 11 e um runtime com suporte a runtime-async. A combinação verificada de runtime e referência aqui é o .NET 11 RC1. Ter como destino apenas o .NET 10 não é suficiente. Consulte experimentos assíncronos de runtime.

O FSharp.Core não fornece este exemplo como um builder para uso geral. O código comum task e async não muda automaticamente para isso, e os chamadores de uma API comum já compilada que retorna tarefa não precisam necessariamente de versão prévia.

Graças a @majocha por runtime assíncrono.

Texto compatível com Native AOT e arrays bidimensionais

Para oferecer suporte ao Native AOT, o F# 11 remove a formatação baseada em reflexão e a necessidade de alocação dinâmica das operações mais comuns de texto e de matrizes.

Quando você já tem uma string, não precisa de um formato "%s" só para escrevê-la. As novas funções Core print e printn aceitam valores por meio da conversão de string do 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 strings usa conversão direta e String.Concat em um número maior de casos. Especificadores de formato do .NET, como {total:F2}, usam formatação invariante e empacotam o valor; %A e formatos printf com especificador de precisão ainda podem exigir formatação printf. Selecione uma cultura específica para saída localizada.

Com --reflectionfree, registros e uniões também obtêm a saída gerada ToString . Consulte Interpolação e AOT Nativo para obter exemplos de cultura e caminhos com suporte.

Agradecimentos a @bbatsov pelas funções de impressão e a @charlesroddie pela implementação de interpolação.

Manter operações normais de grade de base zero

Uma grade baseada em zero não deve exigir um módulo Array2D de substituição apenas para evitar um caminho de alocação de código dinâmico. As implementações atualizadas do Core suportam a inicialização e a transformação já conhecidas:

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 nas compilações Core. map, mapi e copy com preservação de limites podem usar uma API de alocação .NET mais recente no build net10.0; os builds do .NET Standard mantêm seu fallback. Arrays que não começam em zero não são obsoletas, mas suas restrições nativas de alocação permanecem.

Consulte matrizes bidimensionais e AOT nativo para obter caminhos de alocação e restrições restantes. Verifique os diagnósticos de publicação da aplicação completa.

Agradecimentos a @T-Gro pelas melhorias no Array2D.

Menos memória retida e menos trabalho repetido

O F# 11 reduz a análise repetida do compilador e algumas alocações de memória no aplicativo. Elas afetam cargas de trabalho diferentes.

Compartilhar bibliotecas referenciadas entre projetos

Para evitar cópias repetidas de dependências binárias compartilhadas, o serviço de idioma pode compartilhar grafos de bibliotecas importadas elegíveis entre projetos.

No experimento de memória retida da PR de implementação, a solução ReSharper.FSharp de dez projetos caiu de 1.547,2 MB para 1.002,2 MB com compartilhamento habilitado. Essa é uma medida off/on para essa carga de trabalho, não uma redução geral de 10 para 11 em F#. Soluções dominadas por referências de projeto em vez de referências binárias compartilhadas podem ver ganhos muito menores.

A leitura de metadados e a especialização embutida também reduzem as alocações temporárias, outra métrica. Consulte desempenho das ferramentas para ver o escopo e os controles.

Graças a @auduchinok para importações de biblioteca compartilhada.

Compilar trabalho independente em paralelo por padrão

A compilação paralela agora está habilitada por padrão, com regras determinísticas de saída e ordem de origem preservadas. O armazenamento em cache de sobrecargas e as melhorias nos padrões com guarda evitam repetir outras operações. Visual Studio/FCS também reduzem a análise de documentos inalterados e as verificações simultâneas associadas; consulte o desempenho de Ferramentas.

Evitar callback e sobrecarga de alocação de resultado vazio

As funções List e Array selecionadas agora são callbacks inline. Para seus próprios helpers inline, OptimizeClosureIfNotInlined adapta uma vez um callback currificado opaco:

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 currificados para ver os requisitos.

Resultados vazios são outro caso comum de trabalho bem-sucedido. A carga de trabalho motivadora do FSharpLint combinou repetidamente regras que não geravam avisos. O Core agora reutiliza arrays vazios compartilhados 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 do pipeline. Não conte com resultados vazios separados tendo identidades de referência distintas.

Graças a @T-Gro por callbacks de coleção embutidos e arrays vazios compartilhados.

Chamadas de API mais precisas e conveniências cotidianas

Exigir nomes dos parâmetros quando a ordem dos parâmetros gerados pode ser alterada

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

Note

Enforcement é um recurso do F# 11.2; no SDK do .NET 11.0.100, habilite a versão prévia. Uma definição disponível System.Diagnostics.CodeAnalysis.RequireNamedArgumentsAttribute também é necessária. O exemplo completo fornece uma definição de compatibilidade para referências que não têm ela.

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, 0.75M; uma chamada posicional é rejeitada. Para o caso de uso da API gerada, o provedor deve emitir a anotação.

Reconhecer um argumento genérico já encapsulado

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

Note

O desempate mais concreto e o comportamento de prioridade de sobrecarga são recursos do F# 11.2; no SDK do .NET 11.0.100, habilite a versão prévia.

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 obter os limites da regra.

Preferir 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 em que adiciona uma sobrecarga que inclui a localização no código-fonte do chamador. OverloadResolutionPriorityAttribute torna essa preferência explícita para 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 sobrecarga sensível à linha. Consulte Overload selection para obter regras de escopo e compatibilidade.

Usar as promessas de nulidade condicional do .NET

Se o caminho do arquivo não for nulo, o resultado anulável de uma API Path não deverá forçar uma verificação que seu próprio contrato exclui. O F# 11 consome a promessa existente do .NET NotNullIfNotNull: um parâmetro de entrada 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

Habilitar <Nullable>enable</Nullable>. As entradas nullable ainda precisam de tratamento de nullable; ver Contratos de retorno condicional. Para APIs C# T? sem restrições, use o Unchecked.withNull utilitário separado.

Agradecemos a @kerams pelo suporte à anulabilidade condicional.

Pequenas melhorias de linguagem

Para chamar um tipo de registro com a sintaxe de construtor do .NET, use os construtores de registro do F# 11.2:

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

Argumentos nomeados evitam dependência da ordem de campo. Obrigado a @charlesroddie por este recurso.

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

#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 prevalece. 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 estas alterações conseqüentes em mente ao atualizar:

Código afetado Action
Bibliotecas que contêm .fsi arquivos Copia atributos de contrato obrigatórios para assinaturas; as omissões agora são erros FS3888.
Bibliotecas inline com suporte por extensão Analise as assinaturas inferidas e os requisitos do produtor/consumidor. A inferência F# 11.2 pode manter restrições mais gerais.
Delegados de métodos de instância Valide os receptores antes da construção direta, quando agora ocorrem falhas de receptor nulo.
Saída interpolada localizada Os especificadores de formato do .NET usam a cultura invariável. Escolha uma cultura explícita quando se pretende uma saída localizada.
Construtores personalizados com o modificador final do! Fornecimento de vinculação e conclusão de unidade; final do! não seleciona mais membros de encaminhamento final.
Inacabado let! Adicione o seguinte corpo de computação; uma vinculação incompleta agora tem um diagnóstico mais direto.
Configurações de idioma anteriores Os modos numerados com suporte começam em F# 8; o modo de recuo não estrito foi removido. Consulte Versão do idioma e recuo.
operações bit a bit de enum Use enumerações com tipo subjacente integral; tipos subjacentes inválidos são rejeitados.