Condividi tramite


Novità di F# 5

F# 5 aggiunge diversi miglioramenti al linguaggio F# e F# Interactive. Viene rilasciato con .NET 5.

È possibile scaricare la versione più recente di .NET SDK dalla pagina di download di .NET.

Operazioni preliminari

F# 5 è disponibile in tutte le distribuzioni di .NET Core e gli strumenti di Visual Studio. Per altre informazioni, vedere Introduzione a F# per altre informazioni.

Riferimenti ai pacchetti negli script F#

F# 5 offre il supporto per i riferimenti ai pacchetti negli script F# con #r "nuget:..." sintassi. Si consideri ad esempio il riferimento al pacchetto seguente:

#r "nuget: Newtonsoft.Json"

open Newtonsoft.Json

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

printfn $"{JsonConvert.SerializeObject o}"

È anche possibile specificare una versione esplicita dopo il nome del pacchetto, come illustrato di seguito:

#r "nuget: Newtonsoft.Json,11.0.1"

I riferimenti ai pacchetti supportano pacchetti con dipendenze native, ad esempio ML.NET.

I riferimenti ai pacchetti supportano anche pacchetti con requisiti speciali per fare riferimento a s dipendenti .dll. Ad esempio, il pacchetto FParsec usato per richiedere che gli utenti assicurino manualmente che il relativo dipendente FParsecCS.dll sia stato fatto riferimento prima di FParsec.dll essere fatto riferimento in F# Interactive. Questa operazione non è più necessaria ed è possibile fare riferimento al pacchetto come indicato di seguito:

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

Questa funzionalità implementa gli strumenti F# RFC FST-1027. Per altre informazioni sui riferimenti ai pacchetti, vedere l'esercitazione su F# Interactive .

Interpolazione di stringa

Le stringhe interpolate F# sono abbastanza simili alle stringhe interpolate C# o JavaScript, in quanto consentono di scrivere codice in "buchi" all'interno di un valore letterale stringa. Ecco un esempio di base:

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

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

Tuttavia, le stringhe interpolate F# consentono anche l'interpolazione tipizzata, proprio come la sprintf funzione, per imporre che un'espressione all'interno di un contesto interpolato sia conforme a un particolare tipo. Usa gli stessi identificatori di formato.

let name = "Phillip"
let age = 29

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

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

Nell'esempio di interpolazione tipizzata precedente, richiede che %s l'interpolazione sia di tipo string, mentre richiede %d che l'interpolazione sia un oggetto integer.

Inoltre, qualsiasi espressione F# arbitraria (o espressioni) può essere posizionata sul lato di un contesto di interpolazione. È anche possibile scrivere un'espressione più complessa, ad esempio:

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]
}
"""

Anche se non è consigliabile eseguire questa operazione troppo in pratica.

Questa funzionalità implementa F# RFC FS-1001.

Supporto per nameof

F# 5 supporta l'operatore nameof , che risolve il simbolo usato per e produce il nome nell'origine F#. Ciò è utile in vari scenari, ad esempio la registrazione e protegge la registrazione dalle modifiche apportate al codice sorgente.

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}"

L'ultima riga genererà un'eccezione e "month" verrà visualizzato nel messaggio di errore.

È possibile prendere il nome di quasi ogni costrutto F#:

module M =
    let f x = nameof x

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

Tre aggiunte finali sono modifiche al funzionamento degli operatori: l'aggiunta del nameof<'type-parameter> modulo per i parametri di tipo generico e la possibilità di usare nameof come criterio in un'espressione di corrispondenza del criterio.

L'acquisizione di un nome di un operatore fornisce la stringa di origine. Se è necessario il modulo compilato, usare il nome compilato di un operatore:

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

L'uso del nome di un parametro di tipo richiede una sintassi leggermente diversa:

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

Questo è simile agli typeof<'T> operatori e typedefof<'T> .

F# 5 aggiunge anche il supporto per un nameof criterio che può essere usato nelle match espressioni:

[<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

Il codice precedente usa 'nameof' anziché il valore letterale stringa nell'espressione di corrispondenza.

Questa funzionalità implementa F# RFC FS-1003.

Dichiarazioni di tipo aperto

F# 5 aggiunge anche il supporto per le dichiarazioni di tipi aperti. Una dichiarazione di tipo aperto è simile all'apertura di una classe statica in C#, ad eccezione di una sintassi diversa e di un comportamento leggermente diverso per adattarsi alla semantica F#.

Con le dichiarazioni di tipo aperto, è possibile open qualsiasi tipo per esporre contenuto statico all'interno di esso. Inoltre, è possibile open definire unioni e record F#per esporre il relativo contenuto. Ad esempio, questo può essere utile se si dispone di un'unione definita in un modulo e si vuole accedere ai relativi casi, ma non si vuole aprire l'intero modulo.

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}"

A differenza di C#, quando si utilizzano open type due tipi che espongono un membro con lo stesso nome, il membro dell'ultimo tipo da opened shadow l'altro nome. Ciò è coerente con la semantica F# intorno all'ombreggiatura già esistente.

Questa funzionalità implementa F# RFC FS-1068.

Comportamento di sezionamento coerente per i tipi di dati predefiniti

Comportamento per il sezionamento dei tipi di dati predefiniti FSharp.Core (matrice, elenco, stringa, matrice 2D, matrice 3D, matrice 4D) usata per non essere coerente prima di F# 5. Un comportamento di alcuni casi limite ha generato un'eccezione e alcuni no. In F# 5 tutti i tipi predefiniti ora restituiscono sezioni vuote per le sezioni che non è possibile generare:

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

Questa funzionalità implementa F# RFC FS-1077.

Sezioni di indice fisse per matrici 3D e 4D in FSharp.Core

F# 5 offre il supporto per il sezionamento con un indice fisso nei tipi di matrice 3D e 4D predefiniti.

Per illustrare questo problema, considerare la matrice 3D seguente:

z = 0

x\y 0 1
0 0 1
1 2 3

z = 1

x\y 0 1
0 4 5
1 6 7

Cosa accade se si vuole estrarre la sezione [| 4; 5 |] dalla matrice? Questo è ora molto semplice!

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

Questa funzionalità implementa F# RFC FS-1077b.

Miglioramenti delle offerte F#

Le virgolette di codice F# hanno ora la possibilità di conservare le informazioni sui vincoli di tipo. Si consideri l'esempio seguente:

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

Il vincolo generato dalla inline funzione viene conservato tra virgolette di codice. Il negate formato tra virgolette della funzione può ora essere valutato.

Questa funzionalità implementa F# RFC FS-1071.

Espressioni di calcolo applicativo

Le espressioni di calcolo (CES) vengono oggi usate per modellare "calcoli contestuali" o in una terminologia più funzionale semplice per la programmazione, calcoli monadic.

F# 5 introduce le ca applicative, che offrono un modello di calcolo diverso. Le cae applicative consentono calcoli più efficienti a condizione che ogni calcolo sia indipendente e che i risultati vengano accumulati alla fine. Quando i calcoli sono indipendenti l'uno dall'altro, sono anche banalmente parallelizzabili, consentendo agli autori ce di scrivere librerie più efficienti. Questo vantaggio è una restrizione, tuttavia: i calcoli che dipendono dai valori calcolati in precedenza non sono consentiti.

L'esempio seguente illustra un'analisi della stima di base applicativa per il 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 si è un autore di librerie che espone le CES nella loro libreria, è necessario tenere presenti alcune considerazioni aggiuntive.

Questa funzionalità implementa F# RFC FS-1063.

Le interfacce possono essere implementate in istanze generiche diverse

È ora possibile implementare la stessa interfaccia in istanze generiche diverse:

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"

Questa funzionalità implementa F# RFC FS-1031.

Utilizzo predefinito dei membri dell'interfaccia

F# 5 consente di usare interfacce con implementazioni predefinite.

Si consideri un'interfaccia definita in C# come segue:

using System;

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

È possibile usarlo in F# tramite uno dei mezzi standard per implementare un'interfaccia:

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}"

In questo modo è possibile sfruttare in modo sicuro il codice C# e i componenti .NET scritti in C# moderni quando si prevede che gli utenti possano usare un'implementazione predefinita.

Questa funzionalità implementa F# RFC FS-1074.

Interoperabilità semplificata con tipi valore nullable

I tipi Nullable (value) (chiamati tipi nullable storicamente) sono stati supportati da F#, ma l'interazione con essi è tradizionalmente stata un po' un problema perché sarebbe necessario costruire un Nullable wrapper o ogni Nullable<SomeType> volta che si voleva passare un valore. Ora il compilatore convertirà in modo implicito un tipo di valore in un Nullable<ThatValueType> se il tipo di destinazione corrisponde. Il codice seguente è ora possibile:

#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")))

Questa funzionalità implementa F# RFC FS-1075.

Anteprima: indici inversi

F# 5 introduce anche un'anteprima per consentire indici inversi. La sintassi è ^idx. Ecco come è possibile ottenere un valore di elemento 1 dalla fine di un elenco:

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

È anche possibile definire indici inversi per i propri tipi. A tale scopo, è necessario implementare il metodo seguente:

GetReverseIndex: dimension: int -> offset: int

Ecco un esempio per il 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

Questa funzionalità implementa F# RFC FS-1076.

Anteprima: overload di parole chiave personalizzate nelle espressioni di calcolo

Le espressioni di calcolo sono una funzionalità potente per gli autori di librerie e framework. Consentono di migliorare notevolmente l'espressività dei componenti consentendo di definire membri noti e formare un dsl per il dominio in cui si sta lavorando.

F# 5 aggiunge il supporto in anteprima per l'overload di operazioni personalizzate nelle espressioni di calcolo. Consente di scrivere e utilizzare il codice seguente:

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

Prima di questa modifica, è possibile scrivere il InputBuilder tipo così come è, ma non è stato possibile usarlo nel modo in cui viene usato nell'esempio. Poiché gli overload, i parametri facoltativi e ora System.ParamArray i tipi sono consentiti, tutto funziona come previsto.

Questa funzionalità implementa F# RFC FS-1056.