Partilhar via


O que há de novo no F# 5

O F# 5 adiciona várias melhorias à linguagem F# e ao F# interativo. É lançado com o .NET 5.

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

Começar agora

O F# 5 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# para saber mais.

Referências de pacote em scripts F#

F# 5 traz suporte para referências de pacote em scripts F# com #r "nuget:..." sintaxe. Por exemplo, considere a seguinte referência de pacote:

#r "nuget: Newtonsoft.Json"

open Newtonsoft.Json

let o = {| X = 2; Y = "Hello" |}

printfn $"{JsonConvert.SerializeObject o}"

Você também pode fornecer uma versão explícita após o nome do pacote assim:

#r "nuget: Newtonsoft.Json,11.0.1"

As referências de pacotes suportam pacotes com dependências nativas, como ML.NET.

As referências de pacotes também suportam pacotes com requisitos especiais sobre referência a dependentes .dll. Por exemplo, o pacote FParsec usado para exigir que os usuários garantam manualmente que seu dependente FParsecCS.dll foi referenciado primeiro antes de FParsec.dll ser referenciado em F# Interativo. Isso não é mais necessário e você pode fazer referência ao pacote da seguinte maneira:

#r "nuget: FParsec"

open FParsec

let test p str =
    match run p str with
    | Success(result, _, _)   -> printfn $"Success: {result}"
    | Failure(errorMsg, _, _) -> printfn $"Failure: {errorMsg}"

test pfloat "1.234"

Este recurso implementa F # Tooling RFC FST-1027. Para obter mais informações sobre referências de pacote, consulte o tutorial interativo do F#.

Interpolação de cadeias

As cadeias interpoladas F# são bastante semelhantes às cadeias interpoladas C# ou JavaScript, na medida em que permitem escrever código em "buracos" dentro de um literal de cadeia de caracteres. Eis um exemplo básico:

let name = "Phillip"
let age = 29
printfn $"Name: {name}, Age: {age}"

printfn $"I think {3.0 + 0.14} is close to {System.Math.PI}!"

No entanto, as cadeias de caracteres interpoladas F# também permitem interpolações digitadas, assim como a sprintf função, para impor que uma expressão dentro de um contexto interpolado esteja em conformidade com um tipo específico. Ele usa os mesmos especificadores de formato.

let name = "Phillip"
let age = 29

printfn $"Name: %s{name}, Age: %d{age}"

// Error: type mismatch
printfn $"Name: %s{age}, Age: %d{name}"

No exemplo de interpolação digitado anterior, o %s requer que a interpolação seja do tipo string, enquanto o requer que a %d interpolação seja um integer.

Além disso, qualquer expressão (ou expressões) F# arbitrária pode ser colocada ao lado de um contexto de interpolação. É até possível escrever uma expressão mais complicada, assim:

let str =
    $"""The result of squaring each odd item in {[1..10]} is:
{
    let square x = x * x
    let isOdd x = x % 2 <> 0
    let oddSquares xs =
        xs
        |> List.filter isOdd
        |> List.map square
    oddSquares [1..10]
}
"""

Embora não recomendemos fazer isso muito na prática.

Este recurso implementa F # RFC FS-1001.

Suporte para nameof

F# 5 suporta o nameof operador, que resolve o símbolo para o qual está sendo usado e produz seu nome na fonte F#. Isso é útil em vários cenários, como o registro em log, e protege seu registro contra alterações no código-fonte.

let months =
    [
        "January"; "February"; "March"; "April";
        "May"; "June"; "July"; "August"; "September";
        "October"; "November"; "December"
    ]

let lookupMonth month =
    if (month > 12 || month < 1) then
        invalidArg (nameof month) (sprintf "Value passed in was %d." month)

    months[month-1]

printfn $"{lookupMonth 12}"
printfn $"{lookupMonth 1}"
printfn $"{lookupMonth 13}"

A última linha lançará uma exceção e "mês" será mostrado na mensagem de erro.

Você pode usar um nome de quase todas as construções F#:

module M =
    let f x = nameof x

printfn $"{M.f 12}"
printfn $"{nameof M}"
printfn $"{nameof M.f}"

Três adições finais são alterações na forma como os operadores trabalham: a adição do formulário para parâmetros de tipo genéricos e a capacidade de usar nameof como um padrão em uma expressão de correspondência de nameof<'type-parameter> padrão.

Tomar um nome de um operador dá sua cadeia de caracteres de origem. Se você precisar do formulário compilado, use o nome compilado de um operador:

nameof(+) // "+"
nameof op_Addition // "op_Addition"

Tomar o nome de um parâmetro de tipo requer uma sintaxe ligeiramente diferente:

type C<'TType> =
    member _.TypeName = nameof<'TType>

Isto é semelhante ao typeof<'T> e typedefof<'T> operadores.

F# 5 também adiciona suporte para um nameof padrão que pode ser usado em match expressões:

[<Struct; IsByRefLike>]
type RecordedEvent = { EventType: string; Data: ReadOnlySpan<byte> }

type MyEvent =
    | AData of int
    | BData of string

let deserialize (e: RecordedEvent) : MyEvent =
    match e.EventType with
    | nameof AData -> AData (JsonSerializer.Deserialize<int> e.Data)
    | nameof BData -> BData (JsonSerializer.Deserialize<string> e.Data)
    | t -> failwithf "Invalid EventType: %s" t

O código anterior usa 'nameof' em vez da string literal na expressão de correspondência.

Este recurso implementa F # RFC FS-1003.

Declarações de tipo aberto

F# 5 também adiciona suporte para declarações de tipo aberto. Uma declaração de tipo aberta é como abrir uma classe estática em C#, exceto com alguma sintaxe diferente e algum comportamento ligeiramente diferente para ajustar a semântica do F#.

Com declarações de tipo abertas, você pode open qualquer tipo para expor conteúdo estático dentro dele. Além disso, você pode open definir uniões e registros em F# para expor seu conteúdo. Por exemplo, isso pode ser útil se você tiver uma união definida em um módulo e quiser acessar seus casos, mas não quiser abrir o módulo inteiro.

open type System.Math

let x = Min(1.0, 2.0)

module M =
    type DU = A | B | C

    let someOtherFunction x = x + 1

// Open only the type inside the module
open type M.DU

printfn $"{A}"

Ao contrário do C#, quando você open type em dois tipos que expõem um membro com o mesmo nome, o membro do último tipo que está sendo opened sombreia o outro nome. Isso é consistente com a semântica do F# em torno do sombreamento que já existe.

Este recurso implementa F # RFC FS-1068.

Comportamento de fatiamento consistente para tipos de dados internos

O comportamento para fatiar os tipos de FSharp.Core dados internos (matriz, lista, cadeia de caracteres, matriz 2D, matriz 3D, matriz 4D) costumava não ser consistente antes do F# 5. Alguns comportamentos extremos abriram uma exceção e outros não. No F# 5, todos os tipos internos agora retornam fatias vazias para fatias que são impossíveis de gerar:

let l = [ 1..10 ]
let a = [| 1..10 |]
let s = "hello!"

// Before: would return empty list
// F# 5: same
let emptyList = l[-2..(-1)]

// Before: would throw exception
// F# 5: returns empty array
let emptyArray = a[-2..(-1)]

// Before: would throw exception
// F# 5: returns empty string
let emptyString = s[-2..(-1)]

Este recurso implementa F # RFC FS-1077.

Fatias de índice fixo para matrizes 3D e 4D no FSharp.Core

O F# 5 traz suporte para fatiamento com um índice fixo nos tipos de matriz 3D e 4D integrados.

Para ilustrar isso, considere a seguinte matriz 3D:

z = 0

x\y 0 1
0 0 1
1 2 3

z = 1

x\y 0 1
0 4 5
1 6 7

E se você quisesse extrair a fatia [| 4; 5 |] da matriz? Isto agora é muito simples!

// First, create a 3D array to slice

let dim = 2
let m = Array3D.zeroCreate<int> dim dim dim

let mutable count = 0

for z in 0..dim-1 do
    for y in 0..dim-1 do
        for x in 0..dim-1 do
            m[x,y,z] <- count
            count <- count + 1

// Now let's get the [4;5] slice!
m[*, 0, 1]

Este recurso implementa F # RFC FS-1077b.

Melhorias nas cotações do F#

As cotações de código F# agora têm a capacidade de reter informações de restrição de tipo. Considere o seguinte exemplo:

open FSharp.Linq.RuntimeHelpers

let eval q = LeafExpressionConverter.EvaluateQuotation q

let inline negate x = -x
// val inline negate: x: ^a ->  ^a when  ^a : (static member ( ~- ) :  ^a ->  ^a)

<@ negate 1.0 @>  |> eval

A restrição gerada pela inline função é mantida na cotação do código. A negate forma citada da função pode agora ser avaliada.

Este recurso implementa F # RFC FS-1071.

Expressões computacionais de aplicação

Expressões computacionais (CEs) são usadas hoje para modelar "computações contextuais", ou em terminologia mais funcional e amigável à programação, cálculos monádicos.

F# 5 introduz CEs aplicativos, que oferecem um modelo computacional diferente. Os CEs de aplicação permitem cálculos mais eficientes, desde que cada cálculo seja independente e os seus resultados sejam acumulados no final. Quando os cálculos são independentes uns dos outros, eles também são trivialmente paralelizáveis, permitindo que os autores do CE escrevam bibliotecas mais eficientes. No entanto, esse benefício tem uma restrição: cálculos que dependem de valores previamente computados não são permitidos.

O exemplo a seguir mostra um CE aplicativo básico para o Result tipo.

// First, define a 'zip' function
module Result =
    let zip x1 x2 =
        match x1,x2 with
        | Ok x1res, Ok x2res -> Ok (x1res, x2res)
        | Error e, _ -> Error e
        | _, Error e -> Error e

// Next, define a builder with 'MergeSources' and 'BindReturn'
type ResultBuilder() =
    member _.MergeSources(t1: Result<'T,'U>, t2: Result<'T1,'U>) = Result.zip t1 t2
    member _.BindReturn(x: Result<'T,'U>, f) = Result.map f x

let result = ResultBuilder()

let run r1 r2 r3 =
    // And here is our applicative!
    let res1: Result<int, string> =
        result {
            let! a = r1
            and! b = r2
            and! c = r3
            return a + b - c
        }

    match res1 with
    | Ok x -> printfn $"{nameof res1} is: %d{x}"
    | Error e -> printfn $"{nameof res1} is: {e}"

let printApplicatives () =
    let r1 = Ok 2
    let r2 = Ok 3 // Error "fail!"
    let r3 = Ok 4

    run r1 r2 r3
    run r1 (Error "failure!") r3

Se você é um autor de biblioteca que expõe CEs em sua biblioteca hoje, há algumas considerações adicionais que você precisa estar ciente.

Este recurso implementa F # RFC FS-1063.

As interfaces podem ser implementadas em diferentes instanciações genéricas

Agora você pode implementar a mesma interface em diferentes instanciações genéricas:

type IA<'T> =
    abstract member Get : unit -> 'T

type MyClass() =
    interface IA<int> with
        member x.Get() = 1
    interface IA<string> with
        member x.Get() = "hello"

let mc = MyClass()
let iaInt = mc :> IA<int>
let iaString = mc :> IA<string>

iaInt.Get() // 1
iaString.Get() // "hello"

Este recurso implementa F # RFC FS-1031.

Consumo de membro da interface padrão

O F# 5 permite consumir interfaces com implementações padrão.

Considere uma interface definida em C# assim:

using System;

namespace CSharp
{
    public interface MyDim
    {
        public int Z => 0;
    }
}

Você pode consumi-lo em F# através de qualquer um dos meios padrão de implementação de uma interface:

open CSharp

// You can implement the interface via a class
type MyType() =
    member _.M() = ()

    interface MyDim

let md = MyType() :> MyDim
printfn $"DIM from C#: %d{md.Z}"

// You can also implement it via an object expression
let md' = { new MyDim }
printfn $"DIM from C# but via Object Expression: %d{md'.Z}"

Isso permite que você aproveite com segurança o código C# e os componentes .NET escritos em C# moderno quando eles esperam que os usuários possam consumir uma implementação padrão.

Este recurso implementa F # RFC FS-1074.

Interoperabilidade simplificada com tipos de valor anuláveis

Os tipos anuláveis (de valor) (chamados historicamente de Tipos anuláveis) têm sido suportados por F#, mas interagir com eles tem sido tradicionalmente um pouco problemático, já que você teria que construir um Nullable ou Nullable<SomeType> wrapper toda vez que quisesse passar um valor. Agora, o compilador irá converter implicitamente um tipo de valor em um Nullable<ThatValueType> se o tipo de destino corresponder. O seguinte código agora é possível:

#r "nuget: Microsoft.Data.Analysis"

open Microsoft.Data.Analysis

let dateTimes = PrimitiveDataFrameColumn<DateTime>("DateTimes")

// The following line used to fail to compile
dateTimes.Append(DateTime.Parse("2019/01/01"))

// The previous line is now equivalent to this line
dateTimes.Append(Nullable<DateTime>(DateTime.Parse("2019/01/01")))

Este recurso implementa F # RFC FS-1075.

Pré-visualização: índices inversos

O F# 5 também apresenta uma visualização para permitir índices reversos. A sintaxe é ^idx. Veja como você pode obter um valor de elemento 1 a partir do final de uma lista:

let xs = [1..10]

// Get element 1 from the end:
xs[^1]

// From the end slices

let lastTwoOldStyle = xs[(xs.Length-2)..]

let lastTwoNewStyle = xs[^1..]

lastTwoOldStyle = lastTwoNewStyle // true

Você também pode definir índices reversos para seus próprios tipos. Para fazer isso, você precisará implementar o seguinte método:

GetReverseIndex: dimension: int -> offset: int

Aqui está um exemplo para o Span<'T> tipo:

open System

type Span<'T> with
    member sp.GetSlice(startIdx, endIdx) =
        let s = defaultArg startIdx 0
        let e = defaultArg endIdx sp.Length
        sp.Slice(s, e - s)

    member sp.GetReverseIndex(_, offset: int) =
        sp.Length - offset

let printSpan (sp: Span<int>) =
    let arr = sp.ToArray()
    printfn $"{arr}"

let run () =
    let sp = [| 1; 2; 3; 4; 5 |].AsSpan()

    // Pre-# 5.0 slicing on a Span<'T>
    printSpan sp[0..] // [|1; 2; 3; 4; 5|]
    printSpan sp[..3] // [|1; 2; 3|]
    printSpan sp[1..3] // |2; 3|]

    // Same slices, but only using from-the-end index
    printSpan sp[..^0] // [|1; 2; 3; 4; 5|]
    printSpan sp[..^2] // [|1; 2; 3|]
    printSpan sp[^4..^2] // [|2; 3|]

run() // Prints the same thing twice

Este recurso implementa F # RFC FS-1076.

Pré-visualização: sobrecargas de palavras-chave personalizadas em expressões computacionais

As expressões de computação são um recurso poderoso para autores de bibliotecas e estruturas. Eles permitem que você melhore muito a expressividade de seus componentes, permitindo que você defina membros conhecidos e forme uma DSL para o domínio em que está trabalhando.

O F# 5 adiciona suporte de visualização para sobrecarregar operações personalizadas em expressões de computação. Ele permite que o seguinte código seja escrito e consumido:

open System

type InputKind =
    | Text of placeholder:string option
    | Password of placeholder: string option

type InputOptions =
  { Label: string option
    Kind : InputKind
    Validators : (string -> bool) array }

type InputBuilder() =
    member t.Yield(_) =
      { Label = None
        Kind = Text None
        Validators = [||] }

    [<CustomOperation("text")>]
    member this.Text(io, ?placeholder) =
        { io with Kind = Text placeholder }

    [<CustomOperation("password")>]
    member this.Password(io, ?placeholder) =
        { io with Kind = Password placeholder }

    [<CustomOperation("label")>]
    member this.Label(io, label) =
        { io with Label = Some label }

    [<CustomOperation("with_validators")>]
    member this.Validators(io, [<ParamArray>] validators) =
        { io with Validators = validators }

let input = InputBuilder()

let name =
    input {
    label "Name"
    text
    with_validators
        (String.IsNullOrWhiteSpace >> not)
    }

let email =
    input {
    label "Email"
    text "Your email"
    with_validators
        (String.IsNullOrWhiteSpace >> not)
        (fun s -> s.Contains "@")
    }

let password =
    input {
    label "Password"
    password "Must contains at least 6 characters, one number and one uppercase"
    with_validators
        (String.exists Char.IsUpper)
        (String.exists Char.IsDigit)
        (fun s -> s.Length >= 6)
    }

Antes dessa alteração, você podia escrever o InputBuilder tipo como ele é, mas não podia usá-lo da maneira como é usado no exemplo. Como sobrecargas, parâmetros opcionais e agora System.ParamArray tipos são permitidos, tudo funciona como você esperaria.

Este recurso implementa F # RFC FS-1056.