Compartilhar via


Novidades no F# 6

F# 6 adiciona várias melhorias à linguagem F# e F# Interativo. Ele é lançado com o .NET 6.

Você pode baixar o SDK do .NET mais recente na página de downloads do .NET.

Introdução

O F# 6 está disponível em todas as distribuições do .NET Core e nas ferramentas do Visual Studio. Para obter mais informações, consulte Introdução ao F#.

tarefa {...}

O F# 6 inclui suporte nativo para criar tarefas do .NET no código F# usando expressões de tarefa. Expressões de tarefa são semelhantes a expressões assíncronas, mas permitem criar tarefas do .NET diretamente.

Por exemplo, considere o código F# a seguir para criar uma tarefa compatível com .NET:

let readFilesTask (path1, path2) =
   async {
        let! bytes1 = File.ReadAllBytesAsync(path1) |> Async.AwaitTask
        let! bytes2 = File.ReadAllBytesAsync(path2) |> Async.AwaitTask
        return Array.append bytes1 bytes2
   } |> Async.StartAsTask

Usando F# 6, esse código pode ser reescrito da seguinte maneira.

let readFilesTask (path1, path2) =
   task {
        let! bytes1 = File.ReadAllBytesAsync(path1)
        let! bytes2 = File.ReadAllBytesAsync(path2)
        return Array.append bytes1 bytes2
   }

O suporte para tarefas estava disponível para F# 5 por meio das excelentes bibliotecas TaskBuilder.fs e Ply. Deve ser simples migrar o código para o suporte interno. No entanto, há algumas diferenças: namespaces e inferência de tipo diferem ligeiramente entre o suporte interno e essas bibliotecas, e algumas anotações de tipo adicionais podem ser necessárias. Se necessário, você ainda poderá usar essas bibliotecas de comunidade com F# 6 se referenciá-las explicitamente e abrir os namespaces corretos em cada arquivo.

Usar task {…} é muito semelhante ao uso async {…}. O uso task {…} tem várias vantagens em relação async {…}a:

  • A sobrecarga de task {...} é menor, o que pode melhorar o desempenho em trechos de código frequentemente executados, onde o trabalho assíncrono é realizado de maneira rápida.
  • A depuração de rastreamentos de etapas e pilhas task {…} é melhor.
  • A interoperação com pacotes .NET que esperam ou produzem tarefas é mais fácil.

Se você estiver familiarizado com async {…}, há algumas diferenças a serem observadas.

  • task {…} executa imediatamente a tarefa até o primeiro ponto de espera.
  • task {…} não propaga implicitamente um token de cancelamento.
  • task {…} não executa verificações de cancelamento implícitas.
  • task {…} não dá suporte a chamadas de cauda assíncronas. Isso significa que usar return! .. recursivamente pode resultar em estouros de pilha se não houver esperas assíncronas intervenindo.

Em geral, você deve considerar o uso de task {…} em vez de async {…} em novo código, se estiver interoperando com bibliotecas do .NET que usam tarefas e se você não depender de chamadas de retorno de código assíncrono ou propagação implícita de token de cancelamento. No código existente, você só deve alternar para task {…} depois de revisar seu código para garantir que não está confiando nas características mencionadas anteriormente de async {…}.

Esse recurso implementa F# RFC FS-1097.

Sintaxe de indexação mais simples com expr[idx]

F# 6 permite a sintaxe expr[idx] para indexação e fatiamento de coleções.

Até e incluindo a versão F# 5, F# usa expr.[idx] como sintaxe de indexação. Permitir o uso expr[idx] baseia-se em comentários repetidos daqueles que aprendem F# ou vêem F# pela primeira vez que o uso da indexação de notação de pontos aparece como uma divergência desnecessária da prática padrão do setor.

Essa não é uma alteração significativa porque, por padrão, nenhum aviso é emitido no uso de expr.[idx]. No entanto, algumas mensagens informativas que sugerem esclarecimentos de código são emitidas. Opcionalmente, você também pode ativar mais mensagens informativas. Por exemplo, você pode ativar um aviso de informação opcional (/warnon:3366) para começar a relatar o uso da notação expr.[idx]. Para obter mais informações, consulte Indexer Notation.

No novo código, recomendamos o uso sistemático de expr[idx] como sintaxe de indexação.

Esse recurso implementa F# RFC FS-1110.

Representações de struct para padrões parciais ativos

O F# 6 aprimora o recurso de "padrões ativos" com representações opcionais de struct para padrões ativos parciais. Isso permite que você use um atributo para restringir um padrão ativo parcial para retornar uma opção de valor:

[<return: Struct>]
let (|Int|_|) str =
   match System.Int32.TryParse(str) with
   | true, int -> ValueSome(int)
   | _ -> ValueNone

O uso do atributo é necessário. Em sites de uso, o código não é alterado. O resultado líquido é que as alocações são reduzidas.

Esse recurso implementa o F# RFC FS-1039.

Operações personalizadas sobrecarregadas em expressões de computação

O F# 6 permite que você use CustomOperationAttribute nos métodos sobrecarregados.

Considere o seguinte uso de um construtor contentde expressões de computação:

let mem = new System.IO.MemoryStream("Stream"B)
let content = ContentBuilder()
let ceResult =
    content {
        body "Name"
        body (ArraySegment<_>("Email"B, 0, 5))
        body "Password"B 2 4
        body "BYTES"B
        body mem
        body "Description" "of" "content"
    }

Aqui, a body operação personalizada usa um número variado de argumentos de diferentes tipos. Isso é compatível com a implementação do seguinte construtor, que usa sobrecarga:

type Content = ArraySegment<byte> list

type ContentBuilder() =
    member _.Run(c: Content) =
        let crlf = "\r\n"B
        [|for part in List.rev c do
            yield! part.Array[part.Offset..(part.Count+part.Offset-1)]
            yield! crlf |]

    member _.Yield(_) = []

    [<CustomOperation("body")>]
    member _.Body(c: Content, segment: ArraySegment<byte>) =
        segment::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, bytes: byte[]) =
        ArraySegment<byte>(bytes, 0, bytes.Length)::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, bytes: byte[], offset, count) =
        ArraySegment<byte>(bytes, offset, count)::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, content: System.IO.Stream) =
        let mem = new System.IO.MemoryStream()
        content.CopyTo(mem)
        let bytes = mem.ToArray()
        ArraySegment<byte>(bytes, 0, bytes.Length)::c

    [<CustomOperation("body")>]
    member _.Body(c: Content, [<ParamArray>] contents: string[]) =
        List.rev [for c in contents -> let b = Text.Encoding.ASCII.GetBytes c in ArraySegment<_>(b,0,b.Length)] @ c

Esse recurso implementa F# RFC FS-1056.

Padrões "as"

No F# 6, o lado direito de um as padrão agora pode ser ele próprio um padrão. Isso é importante quando um teste de tipo deu um tipo mais forte a uma entrada. Por exemplo, considere o seguinte código:

type Pair = Pair of int * int

let analyzeObject (input: obj) =
    match input with
    | :? (int * int) as (x, y) -> printfn $"A tuple: {x}, {y}"
    | :? Pair as Pair (x, y) -> printfn $"A DU: {x}, {y}"
    | _ -> printfn "Nope"

let input = box (1, 2)

Em cada caso de padrão, o objeto de entrada é testado por tipo. O lado direito do padrão as agora pode ser um padrão adicional, que por sua vez pode corresponder ao objeto em um tipo mais forte.

Esse recurso implementa F# RFC FS-1105.

Revisões de sintaxe de indentação

O F# 6 elimina várias inconsistências e limitações no uso de sintaxe sensível a indentação. Consulte RFC FS-1108. Isso resolve 10 problemas significativos realçados pelos usuários do F# desde f# 4.0.

Por exemplo, no F# 5, o seguinte código foi permitido:

let c = (
    printfn "aaaa"
    printfn "bbbb"
)

No entanto, o código a seguir não foi permitido (ele produziu um aviso):

let c = [
    1
    2
]

No F# 6, ambos são permitidos. Isso torna o F# mais simples e fácil de aprender. O colaborador da comunidade F# Hadrian Tang liderou isso, incluindo testes sistemáticos notáveis e altamente valiosos do recurso.

Esse recurso implementa o F# RFC FS-1108.

Conversões implícitas adicionais

No F# 6, ativamos o suporte para conversões "implícitas" e "direcionadas por tipo" adicionais, conforme descrito no RFC FS-1093.

Essa alteração traz três vantagens:

  1. Menos upcasts explícitos são necessários
  2. Menos conversões de inteiros explícitas são necessárias
  3. Suporte de primeira classe para conversões implícitas no estilo .NET é adicionado

Esse recurso implementa o F# RFC FS-1093.

Conversões de upcast implícitas adicionais

O F# 6 implementa conversões de upcast implícitas adicionais. Por exemplo, em F# 5 e versões anteriores, os upcasts eram necessários para a expressão de retorno ao implementar uma função em que as expressões tinham subtipos diferentes em diferentes ramos, mesmo quando uma anotação de tipo estava presente. Considere o seguinte código F# 5:

open System
open System.IO

let findInputSource () : TextReader =
    if DateTime.Now.DayOfWeek = DayOfWeek.Monday then
        // On Monday a TextReader
        Console.In
    else
        // On other days a StreamReader
        File.OpenText("path.txt") :> TextReader

Aqui, os branches da condicional computam TextReader e StreamReader respectivamente, e o upcast foi adicionado para que ambos os branches tenham o tipo StreamReader. No F# 6, esses upcasts agora são adicionados automaticamente. Isso significa que o código é mais simples:

let findInputSource () : TextReader =
    if DateTime.Now.DayOfWeek = DayOfWeek.Monday then
        // On Monday a TextReader
        Console.In
    else
        // On other days a StreamReader
        File.OpenText("path.txt")

Opcionalmente, você pode habilitar o aviso /warnon:3388 para mostrar um aviso em cada ponto em que um upcast implícito adicional é usado, conforme descrito em avisos opcionais para conversões implícitas.

Conversões de inteiros implícitas

No F# 6, os inteiros de 32 bits são ampliados para inteiros de 64 bits quando ambos os tipos são conhecidos. Por exemplo, considere uma forma de API típica:

type Tensor(…) =
    static member Create(sizes: seq<int64>) = Tensor(…)

Em F# 5, literais inteiros para int64 devem ser usados:

Tensor.Create([100L; 10L; 10L])

or

Tensor.Create([int64 100; int64 10; int64 10])

No F# 6, a ampliação ocorre automaticamente para int32int64, int32 para nativeint, e int32 para double, quando o tipo de origem e de destino são conhecidos durante a inferência de tipo. Portanto, em casos como os exemplos anteriores, int32 literais podem ser usados:

Tensor.Create([100; 10; 10])

Apesar dessa alteração, o F# continua a usar a ampliação explícita de tipos numéricos na maioria dos casos. Por exemplo, a ampliação implícita não se aplica a outros tipos numéricos, como int8 ou int16, ou de float32 para float64, ou quando o tipo de origem ou destino é desconhecido. Opcionalmente, você também pode habilitar o aviso /warnon:3389 para mostrar um aviso em cada ponto em que a ampliação numérica implícita é usada, conforme descrito em avisos opcionais para conversões implícitas.

Suporte de primeira classe para conversões implícitas no estilo .NET

No F# 6, as conversões "op_Implicit" do .NET são aplicadas automaticamente no código F# ao chamar métodos. Por exemplo, no F# 5, era necessário usar XName.op_Implicit ao trabalhar com APIs do .NET para XML:

open System.Xml.Linq
let purchaseOrder = XElement.Load("PurchaseOrder.xml")
let partNos = purchaseOrder.Descendants(XName.op_Implicit "Item")

No F# 6, op_Implicit as conversões são aplicadas automaticamente para expressões de argumento quando os tipos estão disponíveis para expressão de origem e tipo de destino:

open System.Xml.Linq
let purchaseOrder = XElement.Load("PurchaseOrder.xml")
let partNos = purchaseOrder.Descendants("Item")

Opcionalmente, você pode habilitar o aviso /warnon:3395 para exibir um aviso em cada ponto onde ocorre o uso de ampliação de conversões nos argumentos de método, conforme descrito em Avisos Opcionais para Conversões Implícitas.

Observação

Na primeira versão do F# 6, esse número de aviso era /warnon:3390. Devido a um conflito, o número de aviso foi atualizado posteriormente para /warnon:3395.

Avisos opcionais para conversões implícitas

Conversões implícitas e direcionadas por tipo podem interagir mal com inferência de tipo e levar a um código mais difícil de entender. Por esse motivo, existem algumas mitigações para ajudar a garantir que esse recurso não seja abusado no código F#. Primeiro, o tipo de origem e de destino deve ser fortemente conhecido, sem ambiguidade ou inferência de tipo adicional decorrente. Em segundo lugar, os avisos de aceitação podem ser ativados para relatar qualquer uso de conversões implícitas, com um aviso ativado por padrão:

  • /warnon:3388 (upcast implícito adicional)
  • /warnon:3389 (ampliação numérica implícita)
  • /warnon:3391 (op_Implicit em argumentos que não são de método, ativados por padrão)
  • /warnon:3395 (op_Implicit em argumentos de método)

Se sua equipe quiser proibir todos os usos de conversões implícitas, você também poderá especificar /warnaserror:3388, /warnaserror:3389e /warnaserror:3391/warnaserror:3395.

Formatação para números binários

F# 6 adiciona o %B padrão aos especificadores de formato disponíveis para formatos de número binário. Considere o seguinte código F#:

printf "%o" 123
printf "%B" 123

Esse código imprime a seguinte saída:

173
1111011

Esse recurso implementa F# RFC FS-1100.

Descarte ao utilizar vínculos

O F# 6 permite que _ seja usado em uma use definição, por exemplo:

let doSomething () =
    use _ = System.IO.File.OpenText("input.txt")
    printfn "reading the file"

Esse recurso implementa F# RFC FS-1102.

InlineIfLambda

O compilador F# inclui um otimizador que executa o inlining de código. No F# 6, adicionamos um novo recurso declarativo que permite que o código indique opcionalmente que, se um argumento for determinado como uma função lambda, esse argumento deve estar sempre embutido em sites de chamada.

Por exemplo, considere a seguinte iterateTwice função para percorrer uma matriz:

let inline iterateTwice ([<InlineIfLambda>] action) (array: 'T[]) =
    for j = 0 to array.Length-1 do
        action array[j]
    for j = 0 to array.Length-1 do
        action array[j]

Se o site de chamada for:

let arr = [| 1.. 100 |]
let mutable sum = 0
arr  |> iterateTwice (fun x ->
    sum <- sum + x)

Depois de sublinhar e outras otimizações, o código se torna:

let arr = [| 1.. 100 |]
let mutable sum = 0
for j = 0 to arr.Length-1 do
    sum <- sum + arr[j]
for j = 0 to arr.Length-1 do
    sum <- sum + arr[j]

Ao contrário das versões anteriores do F#, essa otimização é aplicada independentemente do tamanho da expressão lambda envolvida. Esse recurso também pode ser usado para implementar o desenrolamento de loop e transformações semelhantes de forma mais confiável.

Um aviso de aceitação (/warnon:3517desabilitado por padrão) pode ser ativado para indicar locais em seu código InlineIfLambda em que os argumentos não estão associados a expressões lambda em sites de chamada. Em situações normais, esse aviso não deve ser habilitado. No entanto, em determinados tipos de programação de alto desempenho, pode ser útil garantir que todo o código seja inlinado e simplificado.

Esse recurso implementa F# RFC FS-1098.

Código retomável

O task {…} suporte do F# 6 é construído sobre uma base chamada código retomávelRFC FS-1087. O código retomável é um recurso técnico que pode ser usado para criar muitos tipos de computadores de estado assíncronos de alto desempenho e que produzem.

Funções de coleção adicionais

O FSharp.Core 6.0.0 adiciona cinco novas operações às funções de coleção principal. Essas funções são:

  • List/Array/Seq.insertAt
  • List/Array/Seq.removeAt
  • List/Array/Seq.updateAt
  • List/Array/Seq.insertManyAt
  • List/Array/Seq.removeManyAt

Todas essas funções executam operações de cópia e atualização no tipo de coleção ou sequência correspondente. Esse tipo de operação é uma forma de "atualização funcional". Para obter exemplos de como usar essas funções, consulte a documentação correspondente, por exemplo, List.insertAt.

Por exemplo, considere o modelo, a mensagem e a lógica de atualização para um aplicativo simples de Lista de Tarefas escrito no estilo Elmish. Aqui, o usuário interage com o aplicativo, gerando mensagens e a update função processa essas mensagens, produzindo um novo modelo:

type Model =
    { ToDo: string list }

type Message =
    | InsertToDo of index: int * what: string
    | RemoveToDo of index: int
    | LoadedToDos of index: int * what: string list

let update (model: Model) (message: Message) =
    match message with
    | InsertToDo (index, what) ->
        { model with ToDo = model.ToDo |> List.insertAt index what }
    | RemoveToDo index ->
        { model with ToDo = model.ToDo |> List.removeAt index }
    | LoadedToDos (index, what) ->
        { model with ToDo = model.ToDo |> List.insertManyAt index what }

Com essas novas funções, a lógica é clara e simples e depende apenas de dados imutáveis.

Esse recurso implementa F# RFC FS-1113.

O mapa tem chaves e valores

No FSharp.Core 6.0.0, o Map tipo agora dá suporte às propriedades Chaves e Valores . Essas propriedades não copiam a coleção subjacente.

Esse recurso está documentado no F# RFC FS-1113.

Intrínsecos adicionais para NativePtr

O FSharp.Core 6.0.0 adiciona novos intrínsecos ao módulo NativePtr :

  • NativePtr.nullPtr
  • NativePtr.isNullPtr
  • NativePtr.initBlock
  • NativePtr.clear
  • NativePtr.copy
  • NativePtr.copyBlock
  • NativePtr.ofILSigPtr
  • NativePtr.toILSigPtr

Assim como acontece com outras funções em NativePtr, essas funções são embutidas, e seu uso emite avisos a menos que /nowarn:9 seja usado. O uso dessas funções é restrito a tipos não gerenciados.

Esse recurso está documentado no F# RFC FS-1109.

Tipos numéricos adicionais com anotações de unidade

No F# 6, os seguintes tipos ou aliases de abreviação de tipos agora suportam anotações de unidade de medida. As novas adições são mostradas em negrito:

Alias F# Tipo CLR
float32/single System.Single
float/double System.Double
decimal System.Decimal
sbyte/int8 System.SByte
int16 System.Int16
int/int32 System.Int32
int64 System.Int64
byte/uint8 System.Byte
uint16 System.UInt16
uint/uint32 System.UInt32
uint64 System.UIn64
nativeint System.IntPtr
unativeint System.UIntPtr

Por exemplo, você pode anotar um inteiro sem sinal da seguinte maneira:

[<Measure>]
type days

let better_age = 3u<days>

Esse recurso está documentado no F# RFC FS-1091.

Avisos informativos para operadores simbólicos raramente usados

F# 6 adiciona orientações suaves que desnormaliza o uso de :=, !, incr, e decr em F# 6 e além. O uso desses operadores e funções produz mensagens informativas que solicitam que você substitua seu código pelo uso explícito da Value propriedade.

Na programação F#, as células de referência podem ser usadas para registros mutáveis alocados por heap. Embora sejam ocasionalmente úteis, raramente são necessários na codificação moderna do F#, porque let mutable podem ser usados em vez disso. A biblioteca principal do F# inclui dois operadores := e ! duas funções incr e decr especificamente relacionados a chamadas de referência. A presença desses operadores torna as células de referência mais centrais à programação F# do que precisam ser, exigindo que todos os programadores F# conheçam esses operadores. Além disso, o ! operador pode ser facilmente confundido com o not operador em C# e outras linguagens, uma fonte potencialmente sutil de bugs ao traduzir código.

A lógica dessa alteração é reduzir o número de operadores que o programador F# precisa saber e, portanto, simplificar f# para iniciantes.

Por exemplo, considere o seguinte código F# 5:

let r = ref 0

let doSomething() =
    printfn "doing something"
    r := !r + 1

Primeiro, as células de referência raramente são necessárias na codificação moderna do F#, como let mutable normalmente pode ser usado em vez disso:

let mutable r = 0

let doSomething() =
    printfn "doing something"
    r <- r + 1

Se você usar células de referência, o F# 6 emitirá um aviso informativo solicitando que você altere a última linha para r.Value <- r.Value + 1 e vinculando-o a orientações adicionais sobre o uso apropriado de células de referência.

let r = ref 0

let doSomething() =
    printfn "doing something"
    r.Value <- r.Value + 1

Essas mensagens não são avisos; são "mensagens informativas" mostradas na saída do IDE e do compilador. F# permanece compatível com versões anteriores.

Esse recurso implementa F# RFC FS-1111.

Ferramentas F#: .NET 6 o padrão para scripts no Visual Studio

Se você abrir ou executar um Script F# (.fsx) no Visual Studio, por padrão, o script será analisado e executado usando o .NET 6 com execução de 64 bits. Essa funcionalidade estava em versão prévia nas versões posteriores do Visual Studio 2019 e agora está habilitada por padrão.

Para habilitar o script do .NET Framework, selecione Ferramentas>Opções>F# Ferramentas>F# Interactive. Defina Usar Script do .NET Core como false e reinicie a janela F# Interativa. Essa configuração afeta a edição de script e a execução do script. Para habilitar a execução de 32 bits para scripts do .NET Framework, defina também f# interativo de 64 bits como false. Não há nenhuma opção de 32 bits para scripts do .NET Core.

Ferramentas F#: fixar a versão do SDK dos scripts F#

Se você executar um script usando dotnet fsi em um diretório que contém um arquivo global.json com uma configuração do SDK do .NET, a versão listada do SDK do .NET será usada para executar e editar o script. Esse recurso está disponível nas versões posteriores do F# 5.

Por exemplo, suponha que haja um script em um diretório com o seguinte arquivo global.json especificando uma política de versão do SDK do .NET:

{
  "sdk": {
    "version": "5.0.200",
    "rollForward": "minor"
  }
}

Se você executar o script usando dotnet fsi, neste diretório, a versão do SDK será respeitada. Esse é um recurso poderoso que permite que você "bloqueie" o SDK usado para compilar, analisar e executar seus scripts.

Se você abrir e editar seu script no Visual Studio e em outros IDEs, as ferramentas respeitarão essa configuração ao analisar e verificar seu script. Se o SDK não for encontrado, você precisará instalá-lo em seu computador de desenvolvimento.

No Linux e em outros sistemas Unix, você pode combinar isso com um shebang para especificar também uma versão de idioma para execução direta do script. Um shebang simples para script.fsx é:

#!/usr/bin/env -S dotnet fsi

printfn "Hello, world"

Agora o script pode ser executado diretamente com script.fsx. Você pode combinar isso com uma versão de idioma específica e não padrão como esta:

#!/usr/bin/env -S dotnet fsi --langversion:5.0

Observação

Essa configuração é ignorada por ferramentas de edição, que analisarão o script assumindo a versão mais recente do idioma.

Removendo funcionalidades legadas

Desde a versão F# 2.0, alguns recursos legados obsoletos há muito tempo geram avisos. Usar esses recursos no F# 6 oferece erros, a menos que você use /langversion:5.0explicitamente. As funcionalidades que apresentam erros são:

  • Vários parâmetros genéricos usando um nome de tipo de sufixo, por exemplo (int, int) Dictionary. Isso se torna um erro em F# 6. Em vez disso, a sintaxe Dictionary<int,int> padrão deve ser usada.
  • #indent "off". Isso se torna um erro.
  • x.(expr). Isso se torna um erro.
  • module M = struct … end . Isso se torna um erro.
  • Uso de entradas *.ml e *.mli. Isso se torna um erro.
  • Uso de (*IF-CAML*) ou (*IF-OCAML*). Isso se torna um erro.
  • Uso de land, lor, lxor, lsl, lsr ou asr como operadores infixos. Elas são palavras-chave de infixo em F# porque eram palavras-chave de infixo no OCaml e não são definidas no FSharp.Core. Agora, o uso dessas palavras-chave emitirá um aviso.

Isso implementa F# RFC FS-1114.