Nieuw in F# 5
F# 5 voegt verschillende verbeteringen toe aan de F#-taal en F# Interactive. Het wordt uitgebracht met .NET 5.
U kunt de nieuwste .NET SDK downloaden via de .NET-downloadpagina.
Aan de slag
F# 5 is beschikbaar in alle .NET Core-distributies en Visual Studio-hulpprogramma's. Zie Aan de slag met F# voor meer informatie.
Pakketverwijzingen in F#-scripts
F# 5 biedt ondersteuning voor pakketverwijzingen in F#-scripts met #r "nuget:..."
syntaxis. Denk bijvoorbeeld aan de volgende pakketreferentie:
#r "nuget: Newtonsoft.Json"
open Newtonsoft.Json
let o = {| X = 2; Y = "Hello" |}
printfn $"{JsonConvert.SerializeObject o}"
U kunt ook een expliciete versie opgeven na de naam van het pakket, zoals deze:
#r "nuget: Newtonsoft.Json,11.0.1"
Pakket verwijst naar ondersteuningspakketten met systeemeigen afhankelijkheden, zoals ML.NET.
Pakketverwijzingen ondersteunen ook pakketten met speciale vereisten over het verwijzen naar afhankelijke .dll
s. Het FParsec-pakket dat wordt gebruikt om bijvoorbeeld te vereisen dat gebruikers handmatig ervoor zorgen dat de afhankelijke FParsecCS.dll
ervan eerst werd verwezen voordat FParsec.dll
naar F# Interactive werd verwezen. Dit is niet meer nodig en u kunt als volgt verwijzen naar het pakket:
#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"
Met deze functie wordt F# Tooling RFC FST-1027 geïmplementeerd. Zie de interactieve F# -zelfstudie voor meer informatie over pakketverwijzingen.
Tekenreeksinterpolatie
F# geïnterpoleerde tekenreeksen zijn vrij vergelijkbaar met C# of JavaScript geïnterpoleerde tekenreeksen, omdat ze u in 'gaten' in een letterlijke tekenreeks laten schrijven. Hier volgt een basisvoorbeeld:
let name = "Phillip"
let age = 29
printfn $"Name: {name}, Age: {age}"
printfn $"I think {3.0 + 0.14} is close to {System.Math.PI}!"
Met F# geïnterpoleerde tekenreeksen kunnen echter ook getypte interpolaties, net als de sprintf
functie, worden afgedwongen dat een expressie in een geïnterpoleerde context voldoet aan een bepaald type. Er worden dezelfde notatieaanduidingen gebruikt.
let name = "Phillip"
let age = 29
printfn $"Name: %s{name}, Age: %d{age}"
// Error: type mismatch
printfn $"Name: %s{age}, Age: %d{name}"
In het voorgaande geïnterpoleerde voorbeeld moet de %s
interpolatie van het type string
zijn, terwijl de %d
interpolatie een integer
.
Daarnaast kan elke willekeurige F#-expressie (of expressies) naast een interpolatiecontext worden geplaatst. Het is zelfs mogelijk om een complexere expressie te schrijven, zoals:
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]
}
"""
Hoewel we dit in de praktijk niet te veel aanbevelen.
Met deze functie wordt F# RFC FS-1001 geïmplementeerd.
Ondersteuning voor naamvan
F# 5 ondersteunt de nameof
operator, waarmee het symbool wordt omgezet waarvoor het wordt gebruikt en de naam wordt geproduceerd in de F#-bron. Dit is handig in verschillende scenario's, zoals logboekregistratie en beschermt uw logboekregistratie tegen wijzigingen in de broncode.
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}"
De laatste regel genereert een uitzondering en 'maand' wordt weergegeven in het foutbericht.
U kunt bijna elke F#-constructie een naam nemen:
module M =
let f x = nameof x
printfn $"{M.f 12}"
printfn $"{nameof M}"
printfn $"{nameof M.f}"
Drie laatste toevoegingen zijn wijzigingen in de werking van operators: de toevoeging van het nameof<'type-parameter>
formulier voor algemene typeparameters en de mogelijkheid om te gebruiken nameof
als patroon in een patroonovereenkomstexpressie.
Als u een naam van een operator gebruikt, wordt de brontekenreeks opgegeven. Als u het gecompileerde formulier nodig hebt, gebruikt u de gecompileerde naam van een operator:
nameof(+) // "+"
nameof op_Addition // "op_Addition"
Voor het nemen van de naam van een typeparameter is een iets andere syntaxis vereist:
type C<'TType> =
member _.TypeName = nameof<'TType>
Dit is vergelijkbaar met de typeof<'T>
operators en typedefof<'T>
de operators.
F# 5 voegt ook ondersteuning toe voor een nameof
patroon dat kan worden gebruikt in match
expressies:
[<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
In de voorgaande code wordt 'nameof' gebruikt in plaats van de letterlijke tekenreeks in de overeenkomstexpressie.
Met deze functie wordt F# RFC FS-1003 geïmplementeerd.
Declaraties van open type
F# 5 voegt ook ondersteuning toe voor declaraties van open type. Een declaratie van een open type is vergelijkbaar met het openen van een statische klasse in C#, behalve met een aantal verschillende syntaxis en een iets ander gedrag dat past bij F#-semantiek.
Met declaraties van geopende typen kunt open
u elk type instellen om statische inhoud erin weer te geven. Daarnaast kunt open
u F#-gedefinieerde samenvoegingen en records gebruiken om hun inhoud beschikbaar te maken. Dit kan bijvoorbeeld handig zijn als u een samenvoeging hebt gedefinieerd in een module en toegang wilt krijgen tot de cases, maar de hele module niet wilt openen.
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}"
In tegenstelling tot C#, wanneer u open type
twee typen gebruikt die een lid met dezelfde naam beschikbaar maken, wordt open
het lid van het laatste type schaduwen de andere naam. Dit is consistent met F#-semantiek rond schaduwen die al bestaan.
Met deze functie wordt F# RFC FS-1068 geïmplementeerd.
Consistent segmenteringsgedrag voor ingebouwde gegevenstypen
Gedrag voor het segmenteren van de ingebouwde FSharp.Core
gegevenstypen (matrix, lijst, tekenreeks, 2D-matrix, 3D-matrix, 4D-matrix) die voorheen niet consistent waren voor F# 5. Sommige edge-casegedrag heeft een uitzondering veroorzaakt en sommigen niet. In F# 5 retourneren alle ingebouwde typen nu lege segmenten voor segmenten die niet kunnen worden gegenereerd:
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)]
Met deze functie wordt F# RFC FS-1077 geïmplementeerd.
Segmenten met vaste index voor 3D- en 4D-matrices in FSharp.Core
F# 5 biedt ondersteuning voor segmentering met een vaste index in de ingebouwde 3D- en 4D-matrixtypen.
Bekijk de volgende 3D-matrix om dit te illustreren:
z = 0
x\y | 0 | 1 |
---|---|---|
0 | 0 | 1 |
1 | 2 | 3 |
z = 1
x\y | 0 | 1 |
---|---|---|
0 | 4 | 5 |
1 | 6 | 7 |
Wat moet u doen als u het segment [| 4; 5 |]
uit de matrix wilt extraheren? Dit is nu heel eenvoudig!
// 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]
Met deze functie wordt F# RFC FS-1077b geïmplementeerd.
Verbeteringen in F#-aanhalingstekens
F# -codecitaten hebben nu de mogelijkheid om gegevens over typebeperkingen te behouden. Kijk een naar het volgende voorbeeld:
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
De beperking die door de inline
functie wordt gegenereerd, wordt bewaard in de codecitaat. Het formulier voor aanroepen van de negate
functie kan nu worden geëvalueerd.
Met deze functie wordt F# RFC FS-1071 geïmplementeerd.
Applicatieve berekeningsexpressies
Rekenexpressies (CE's) worden tegenwoordig gebruikt om 'contextuele berekeningen' te modelleren, of in functionele programmeervriendelijke terminologie, monadische berekeningen.
F# 5 introduceert applicatieve CA's, die een ander rekenkundig model bieden. Applicatieve CA's maken efficiëntere berekeningen mogelijk, mits elke berekening onafhankelijk is en hun resultaten aan het einde worden verzameld. Wanneer berekeningen onafhankelijk van elkaar zijn, zijn ze ook triviaal parallelliseerbaar, waardoor CE-auteurs efficiëntere bibliotheken kunnen schrijven. Dit voordeel heeft echter een beperking: berekeningen die afhankelijk zijn van eerder berekende waarden zijn niet toegestaan.
In het volgende voorbeeld ziet u een eenvoudige applicatieve CE voor het Result
type.
// 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
Als u een auteur van een bibliotheek bent die momenteel CA's beschikbaar maakt in hun bibliotheek, moet u rekening houden met enkele aanvullende overwegingen.
Met deze functie wordt F# RFC FS-1063 geïmplementeerd.
Interfaces kunnen worden geïmplementeerd bij verschillende algemene instantiëringen
U kunt nu dezelfde interface implementeren in verschillende algemene instantiëringen:
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"
Met deze functie wordt F# RFC FS-1031 geïmplementeerd.
Standaardverbruik van lid van interface
Met F# 5 kunt u interfaces met standaard implementaties gebruiken.
Overweeg een interface die is gedefinieerd in C# als volgt:
using System;
namespace CSharp
{
public interface MyDim
{
public int Z => 0;
}
}
U kunt deze in F# gebruiken via een van de standaardmethodes voor het implementeren van een 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}"
Hierdoor kunt u veilig profiteren van C#-code en .NET-onderdelen die zijn geschreven in moderne C# wanneer ze verwachten dat gebruikers een standaard-implementatie kunnen gebruiken.
Met deze functie wordt F# RFC FS-1074 geïmplementeerd.
Vereenvoudigde interoperabiliteit met typen null-waarden
Nullable (waarde)-typen (ook wel Nullable Types genoemd) worden al lang ondersteund door F#, maar interactie met deze typen is traditioneel enigszins vervelend omdat u een Nullable
of Nullable<SomeType>
wrapper moet maken telkens wanneer u een waarde wilt doorgeven. Nu converteert de compiler impliciet een waardetype naar een Nullable<ThatValueType>
als het doeltype overeenkomt. De volgende code is nu mogelijk:
#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")))
Met deze functie wordt F# RFC FS-1075 geïmplementeerd.
Preview: omgekeerde indexen
F# 5 introduceert ook een preview voor het toestaan van omgekeerde indexen. De syntaxis is ^idx
. U kunt als volgt een element 1-waarde aan het einde van een lijst gebruiken:
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
U kunt ook omgekeerde indexen definiëren voor uw eigen typen. Hiervoor moet u de volgende methode implementeren:
GetReverseIndex: dimension: int -> offset: int
Hier volgt een voorbeeld voor het Span<'T>
type:
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
Met deze functie wordt F# RFC FS-1076 geïmplementeerd.
Preview: overbelasting van aangepaste trefwoorden in berekeningsexpressies
Rekenexpressies zijn een krachtige functie voor auteurs van bibliotheken en frameworks. Hiermee kunt u de expressiviteit van uw onderdelen aanzienlijk verbeteren door u bekende leden te laten definiëren en een DSL te vormen voor het domein waarin u werkt.
F# 5 voegt preview-ondersteuning toe voor overbelasting van aangepaste bewerkingen in berekeningsexpressies. Hiermee kan de volgende code worden geschreven en verbruikt:
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)
}
Vóór deze wijziging kunt u het InputBuilder
type schrijven zoals het is, maar u kunt het niet gebruiken zoals het in het voorbeeld wordt gebruikt. Omdat overbelastingen, optionele parameters en nu System.ParamArray
typen zijn toegestaan, werkt alles zoals u zou verwachten.
Met deze functie wordt F# RFC FS-1056 geïmplementeerd.