Novidades no F# 6

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

Você pode baixar o SDK mais recente do .NET 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 com F#.

tarefa {…}

O F# 6 inclui suporte nativo para criar tarefas do .NET no código F#. Por exemplo, considere o seguinte código F# 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 à tarefa 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 de async {…}. Usar task {…} tem várias vantagens em relação a async {…}:

  • A sobrecarga de task {...} é menor, possivelmente aprimorando o desempenho em caminhos de código quente em que o trabalho assíncrono é executado rapidamente.
  • A depuração de rastreamentos de etapas e pilhas para 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 para 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 tailcalls assíncronos. Isso significa que usar return! .. recursivamente pode resultar em excedente de pilha se não houver esperas assíncronas intervindo.

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

Esse recurso implementa o F# RFC FS-1097.

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

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

Até e incluindo F# 5, F# tem usado expr.[idx] como sintaxe de indexação. Permitir o uso de 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 ponto 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 outras mensagens informativas. Por exemplo, você pode ativar um aviso informativo opcional (/warnon:3566) para iniciar a relatar o uso da notação expr.[idx]. Para obter mais informações, consulte Notação do indexador.

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

Esse recurso implementa o F# RFC FS-1110.

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

O F# 6 aumenta o recurso "padrões ativos" com representações de struct opcionais 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 é a redução das alocações.

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 de expressões de computação content:

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 operação personalizada body 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 o F# RFC FS-1056.

padrões “as”

No F# 6, o lado direito de um padrão as agora pode ser 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 tem permissão para ser um padrão adicional, que pode corresponder ao objeto no tipo mais forte.

Esse recurso implementa o F# RFC FS-1105.

Revisões de sintaxe de recuo

O F# 6 remove uma série de inconsistências e limitações no uso da sintaxe com reconhecimento de recuo. Consulte RFC FS-1108. Isso resolve 10 problemas significativos destacados pelos usuários do F# desde o F# 4.0.

Por exemplo, em 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 F# mais simples e fácil de aprender. O colaborador da comunidade F# Hadrian Tang liderou o caminho sobre 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 adicionais "implícitas" e "direcionadas por tipo", 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 explícitas de inteiro são necessárias
  3. Adicionado suporte de primeira classe para conversões implícitas no estilo .NET

Esse recurso implementa o F# RFC FS-1093.

Conversões adicionais de upcast implícitas

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 ramificações diferentes, 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 computação condicional são um TextReader e StreamReader, respectivamente, e o upcast foi adicionado para fazer com que ambas as ramificações 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 ativar o aviso /warnon:3388 para mostrar um aviso a cada ponto em que um upcast implícito adicional for usado, conforme descrito em Avisos opcionais para conversões implícitas.

Conversões de inteiro implícitas

Em F# 6, 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])

ou

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

Em In F# 6, a ampliação ocorre automaticamente para int32 para int64, para int32 para nativeint e para int32 para double quando o tipo de origem e de destino são conhecidos durante a inferência de tipos. Portanto, em casos como os exemplos anteriores, literais int32 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, o alargamento implícito não se aplica a outros tipos numéricos, como int8 ou int16, ou de float32 a float64, ou quando o tipo de origem ou destino é desconhecido. Você também pode ativar opcionalmente 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 .NET para XML:

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

No F# 6, as conversões op_Implicit no estilo .NET 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")

Como alternativa, você pode ativar o aviso /warnon:3395 para mostrar um aviso a cada ponto op_Implicit a ampliação das conversões é usada nos argumentos do 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 foi /warnon:3390. Devido a um conflito, o número de aviso foi atualizado posteriormente para /warnon:3395.

Avisos opcionais para conversões implícitas

As conversões direcionadas por tipo ou implícitas podem interagir mal com a inferência de tipos 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 métodos, ativado por padrão)
  • /warnon:3395 (op_Implicit em argumentos de métodos)

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

Formatação para números binários

F# 6 adiciona o padrão %B 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

Este código imprime a seguinte saída:

173
1111011

Esse recurso implementa o F# RFC FS-1100.

Descarta no uso de associações

O F# 6 permite _ ser usado em uma use associação, por exemplo:

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

Esse recurso implementa o F# RFC FS-1102.

InlineIfLambda

O compilador F# inclui um otimizador que embute o código. No F# 6 adicionamos um novo recurso declarativo que permite ao código indicar, opcionalmente, que se um argumento for determinado como uma função lambda, esse argumento deverá ser sempre embutido em sites de chamada.

Por exemplo, considere a seguinte função iterateTwice para cruzar 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 embutir e de 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 de F#, essa otimização é aplicada independentemente do tamanho da expressão lambda envolvida. Esse recurso também pode ser usado para implementar o cancelamento de loop e transformações semelhantes de maneira mais confiável.

Um aviso de aceitação (/warnon:3517, desativado por padrão) pode ser ativado para indicar locais em seu código em que argumentos InlineIfLambda 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 embutido e nivelado.

Esse recurso implementa o F# RFC FS-1098.

Código retomável

O suporte task {…} do F# 6 baseia-se em 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 de rendimento.

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 uso dessas funções, consulte a documentação correspondente, por exemplo, List.insertAt.

Como exemplo, considere o modelo, a mensagem e a lógica de atualização para um aplicativo simples "Lista de Todos" escrito no estilo Elmish. Aqui, o usuário interage com o aplicativo, gerando mensagens e a função update 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 o F# RFC FS-1113.

O mapa tem chaves e valores

No FSharp.Core 6.0.0, o tipo Map 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 o uso delas 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 tipo agora dão suporte a anotações de unidade de medida. As novas adições são mostradas em negrito:

Alias do 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 diretrizes suaves que des normalizam 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 propriedade Value.

Na programação F#, as células de referência podem ser usadas para registros mutáveis alocados em heap. Embora ocasionalmente sejam úteis, raramente são necessários na codificação moderna do F#, porque pode-se usar let mutable no lugar. 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 para a 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 a operação not em C# e em outras linguagens, uma fonte potencialmente sutil de bugs ao traduzir código.

A lógica para essa alteração é reduzir o número de operadores que o programador F# precisa conhecer e, portanto, simplificar o 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 r.Value <- r.Value + 1 e vincule-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 o 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. Esta funcionalidade estava em prévia nas versões posteriores do Visual Studio 2019 e agora está habilitada por padrão.

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

Ferramentas F#: fixar a versão do SDK de seus 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 "bloquear" o SDK usado para compilar, analisar e executar seus scripts.

Se você abrir e editar seu script no Visual Studio e em outras 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 no computador de desenvolvimento.

No Linux e em outros sistemas Unix, você pode combinar isso com um shebang para também especificar 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 pelas ferramentas de edição, que analisarão o script assumindo a versão mais recente do idioma.

Removendo recursos herdados

Desde o F# 2.0, alguns recursos herdados preteridos há muito tempo receberam avisos. Usar esses recursos no F# 6 oferece erros, a menos que você use /langversion:5.0 explicitamente. Os recursos que dão erros são:

  • Vários parâmetros genéricos usando um nome de tipo de postfixo, por exemplo (int, int) Dictionary. Isso se torna um erro em F# 6. Em vez disso, a sintaxe Dictionary<int,int> 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 de infix. Essas são palavras-chave infixas em F# porque eram palavras-chave infixas em OCaml e não são definidas em FSharp.Core. Agora, o uso dessas palavras-chave emitirá um aviso.

Isso implementa F# RFC FS-1114.