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 open
ed 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.