Novidades no F# 5

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

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

Introdução

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#.

Referências de pacote em scripts F#

O F# 5 oferece suporte para referências de pacote em scripts F# com a sintaxe #r "nuget:...". 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 da seguinte maneira:

#r "nuget: Newtonsoft.Json,11.0.1"

As referências de pacote dão suporte a pacotes com dependências nativas, como ML.NET.

As referências de pacote também dão suporte a pacotes com requisitos especiais sobre como fazer referência a .dll dependentes. Por exemplo, o pacote FParsec usado para exigir que os usuários garantam manualmente que o FParsecCS.dlldependente tenha sido referenciado antes de FParsec.dll ter sido referenciado no 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"

Esse recurso implementa as Ferramentas do F# RFC FST-1027. Para obter mais informações sobre referências de pacote, consulte o tutorial F# Interativo.

Interpolação de cadeia de caracteres

As cadeias de caracteres interpoladas do F# são semelhantes às cadeias de caracteres interpoladas do C# ou JavaScript, na medida em que permitem que você grave código em "espaços" dentro de um literal de cadeia de caracteres. Este é 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 do F# também permitem interpolações tipadas, assim como a função sprintf, para 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 tipado anterior, %s requer que a interpolação seja do tipo string, enquanto a %d requer que a interpolação seja integer.

Além disso, qualquer expressão (ou expressões) F# arbitrária pode ser inserida em um contexto de interpolação. É até possível gravar uma expressão mais complicada, da seguinte maneira:

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 seja recomendável fazer isso com frequência.

Esse recurso implementa o F# RFC FS-1001.

Suporte para nameof

O F# 5 dá suporte ao operador nameof, que resolve o símbolo para o qual está sendo usado e produz seu nome na origem F#. Isso é útil em vários cenários, como registrar em log, além de proteger seu registro em log 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 gerará uma exceção e "mês" será mostrado na mensagem de erro.

Você pode usar um nome de quase todo o constructo 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 funcionam: a adição do formulário nameof<'type-parameter> para parâmetros de tipo genérico e a capacidade de usar nameof como um padrão em uma expressão de correspondência de padrão.

O nome de um operador fornece sua cadeia de caracteres de origem. Caso o formulário compilado seja necessário, use o nome compilado de um operador:

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

O uso do nome de um parâmetro de tipo requer uma sintaxe ligeiramente diferente:

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

Isso é semelhante aos operadores typeof<'T> e typedefof<'T>.

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

[<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 acima usa 'nameof' em vez do literal de cadeia de caracteres na expressão de correspondência.

Esse recurso implementa o F# RFC FS-1003.

Declarações de tipo aberto

O F# 5 também adiciona suporte às declarações de tipo aberto. Uma declaração de tipo aberto é como abrir uma classe estática no C#, mas com uma sintaxe e um comportamento ligeiramente diferentes para ajustar a semântica do F#.

Com declarações de tipo aberto, é possível open qualquer tipo para expor conteúdo estático. Além disso, é possível open uniões e registros definidos 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 open, sombreia o outro nome. Isso é consistente com a semântica do F# em torno do sombreamento existente.

Esse recurso implementa o F# RFC FS-1068.

Comportamento de fatiamento consistente para tipos de dados internos

O comportamento para cortar os tipos de dados FSharp.Core internos (matriz, lista, cadeia de caracteres, matriz 2D, matriz 3D, matriz 4D) não era consistente antes do F# 5. Alguns comportamentos de caso de borda geraram uma exceção e outros não. No F# 5, todos os tipos internos agora retornam fatias vazias para fatias impossíveis de serem geradas:

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)]

Esse recurso implementa o F# RFC FS-1077.

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

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

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? 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]

Esse recurso implementa o F# RFC FS-1077b.

Melhorias nas citações do F#

As citações de código do 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 função inline é retida na citação do código. A forma entre aspas da função negate agora pode ser avaliada.

Esse recurso implementa o F# RFC FS-1071.

Expressões de computação aplicáveis

As expressões de computação (CEs) são usadas hoje para modelar "computações contextuais", ou na terminologia mais funcional amigável à programação, computações monádicas.

O F# 5 apresenta os CEs aplicáveis, que oferecem um modelo computacional diferente. Os CEs aplicáveis permitem computações mais eficientes, desde que cada computação seja independente e seus resultados sejam acumulados no final. Quando as computações são independentes entre si, elas também são trivialmente paralelizáveis, permitindo que os autores de CE gravem bibliotecas mais eficientes. No entanto, esse benefício possui uma restrição: a computação que depende de valores computados anteriormente não é permitida.

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

// 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ê for um autor de biblioteca que expõe CEs em sua biblioteca hoje, há algumas considerações adicionais das quais você precisará estar ciente.

Esse recurso implementa o F# RFC FS-1063.

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

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

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"

Esse recurso implementa o 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 no C# da seguinte forma:

using System;

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

Você pode consumi-la no F# por 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 gravados no C# moderno quando eles esperam que os usuários possam consumir uma implementação padrão.

Esse recurso implementa o F# RFC FS-1074.

Interoperabilidade simplificada com tipos de valor anuláveis

Tipos (de valor) anuláveis (chamados historicamente de Tipos anuláveis) têm sido suportados por F# há muito tempo, mas a interação com eles tem sido tradicionalmente difícil, pois seria necessário construir um wrapper Nullable ou Nullable<SomeType> sempre que você quisesse passar um valor. Agora, o compilador converterá implicitamente um tipo de valor em Nullable<ThatValueType> se o tipo de destino corresponder. O código a seguir 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")))

Esse recurso implementa o F# RFC FS-1075.

Versão prévia: índices reversos

O F# 5 também apresenta uma versão prévia para permitir índices reversos. A sintaxe é ^idx. Veja como definir um valor de elemento 1 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, será necessário implementar o seguinte método:

GetReverseIndex: dimension: int -> offset: int

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

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

Esse recurso implementa o F# RFC FS-1076.

Versão prévia: sobrecargas de palavras-chave personalizadas em expressões de computação

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

O F# 5 adiciona suporte à versão prévia para sobrecarregar operações personalizadas em Expressões de Computação. Ele permite que o seguinte código seja gravado 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ê poderia gravar o tipo InputBuilder no estado em que se encontra, mas não era possível usá-lo da maneira ilustrada no exemplo. Como sobrecargas, parâmetros opcionais e agora tipos System.ParamArray são permitidos, tudo funciona conforme o esperado.

Esse recurso implementa o F# RFC FS-1056.