Az F# 5 újdonságai
Az F# 5 számos fejlesztést tartalmaz az F# nyelvhez és az F# Interactivehoz. A .NET 5-tel jelenik meg.
A legújabb .NET SDK-t a .NET letöltési oldaláról töltheti le.
Első lépések
Az F# 5 minden .NET Core-disztribúcióban és Visual Studio-eszközben elérhető. További információt az F# használatának első lépései című témakörben talál.
Csomaghivatkozások F#-szkriptekben
Az F# 5 támogatja a szintaxist tartalmazó #r "nuget:..."
F#-szkriptekben található csomaghivatkozásokat. Vegyük például a következő csomaghivatkozást:
#r "nuget: Newtonsoft.Json"
open Newtonsoft.Json
let o = {| X = 2; Y = "Hello" |}
printfn $"{JsonConvert.SerializeObject o}"
A csomag neve után a következőhöz hasonló explicit verziót is megadhat:
#r "nuget: Newtonsoft.Json,11.0.1"
A csomaghivatkozások natív függőségekkel rendelkező csomagokat támogatnak, például ML.NET.
A csomaghivatkozások olyan csomagokat is támogatnak, amelyek speciális követelményeket támasztanak a függőkre .dll
való hivatkozással kapcsolatban. Az FParsec-csomag például megköveteli, hogy a felhasználók manuálisan győződjenek meg arról, hogy a függőre FParsecCS.dll
először hivatkoztak, mielőtt FParsec.dll
az F# Interactiveban hivatkoztak rá. Erre már nincs szükség, és az alábbiak szerint hivatkozhat a csomagra:
#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"
Ez a funkció implementálja az F# Tooling RFC FST-1027-et. A csomaghivatkozásokról további információt az F# interaktív oktatóanyagában talál.
Sztring interpolációja
Az F# interpolált sztringek meglehetősen hasonlítanak a C# vagy JavaScript interpolált sztringekhez, mivel lehetővé teszik a sztringkonstanson belüli "lyukakba" írt kódot. Íme egy egyszerű példa:
let name = "Phillip"
let age = 29
printfn $"Name: {name}, Age: {age}"
printfn $"I think {3.0 + 0.14} is close to {System.Math.PI}!"
Az F# interpolált sztringek azonban a függvényhez hasonlóan lehetővé teszik a sprintf
beírt interpolációk használatát is annak kikényszerítéséhez, hogy egy interpolált kontextusban lévő kifejezés megfeleljen egy adott típusnak. Ugyanazokat a formátumkijelölőket használja.
let name = "Phillip"
let age = 29
printfn $"Name: %s{name}, Age: %d{age}"
// Error: type mismatch
printfn $"Name: %s{age}, Age: %d{name}"
Az előző beírt interpolációs példában az %s
interpoláció típusának string
kell lennie , míg az %d
interpolációnak integer
egy .
Emellett tetszőleges F#-kifejezés (vagy kifejezés) elhelyezhető az interpolációs környezet oldalán. Még egy bonyolultabb kifejezést is meg lehet írni, például:
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]
}
"""
Bár nem javasoljuk, hogy ezt túl sokat tegye a gyakorlatban.
Ez a funkció implementálja az F# RFC FS-1001-et.
A névtámogatás
Az F# 5 támogatja az nameof
operátort, amely feloldja a használt szimbólumot, és létrehozza a nevét F# forrásban. Ez különböző helyzetekben hasznos, például naplózás esetén, és védelmet nyújt a naplózásnak a forráskód változásaival szemben.
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}"
Az utolsó sor kivételt eredményez, és a "hónap" megjelenik a hibaüzenetben.
Szinte minden F#-szerkezet nevét felveheti:
module M =
let f x = nameof x
printfn $"{M.f 12}"
printfn $"{nameof M}"
printfn $"{nameof M.f}"
Három utolsó kiegészítés az operátorok működésének módosítása: az űrlap hozzáadása az nameof<'type-parameter>
általános típusparaméterekhez, valamint a mintaegyeztetési kifejezés mintaként való használatának nameof
képessége.
Egy operátor nevének megadása megadja a forrássztringet. Ha a lefordított űrlapra van szüksége, használja az operátor lefordított nevét:
nameof(+) // "+"
nameof op_Addition // "op_Addition"
Egy típusparaméter nevének megadása kissé eltérő szintaxist igényel:
type C<'TType> =
member _.TypeName = nameof<'TType>
Ez hasonló az és typedefof<'T>
az typeof<'T>
operátorokhoz.
Az F# 5 emellett támogatja a nameof
kifejezésekben match
használható mintákat:
[<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
Az előző kód a "nameof" kifejezést használja a sztringkonstans helyett az egyezés kifejezésben.
Ez a funkció az F# RFC FS-1003-at implementálja.
Típusdeklarációk megnyitása
Az F# 5 támogatja a nyílt típusú deklarációkat is. A nyitott típusú deklaráció olyan, mint egy statikus osztály megnyitása A C#-ban, kivéve néhány különböző szintaxist és kissé eltérő viselkedést az F# szemantikájának megfelelően.
Nyitott típusú deklarációkkal bármilyen típussal közzéteheti open
a benne lévő statikus tartalmakat. Emellett F#-ként definiált egyesítőket és rekordokat is használhat open
a tartalom nyilvánosságra hozásához. Ez például akkor lehet hasznos, ha egy modulban egy egyesítés van definiálva, és hozzá szeretne férni az esetekhez, de nem szeretné megnyitni a teljes modult.
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 C#-tól eltérően, open type
ha két olyan típuson dolgozik, amelyek egy azonos nevű tagot fednek fel, az utoljára használt tag open
árnyékot ad a másik névnek. Ez összhangban van a már meglévő árnyékolásT körülvevő F#-szemantikával.
Ez a funkció az F# RFC FS-1068-at implementálja.
Konzisztens szeletelési viselkedés a beépített adattípusokhoz
A beépített FSharp.Core
adattípusok (tömb, lista, sztring, 2D tömb, 3D tömb, 4D tömb) szeletelésének viselkedése, amely az F# 5 előtt nem konzisztens. Néhány peremes eset viselkedése kivételt eredményezett, mások pedig nem. Az F# 5-ben az összes beépített típus üres szeleteket ad vissza olyan szeletekhez, amelyeket lehetetlen létrehozni:
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)]
Ez a funkció az F# RFC FS-1077-et implementálja.
Rögzített indexszeletek 3D és 4D tömbökhöz az FSharp.Core-ban
Az F# 5 támogatja a szeletelést a beépített 3D és 4D tömbtípusok rögzített indexével.
Ennek szemléltetéséhez vegye figyelembe a következő 3D tömböt:
z = 0
x\y | 0 | 0 |
---|---|---|
0 | 0 | 0 |
1 | 2 | 3 |
z = 1
x\y | 0 | 0 |
---|---|---|
0 | 4 | 5 |
1 | 6 | 7 |
Mi a teendő, ha ki szeretné nyerni a szeletet [| 4; 5 |]
a tömbből? Ez most nagyon egyszerű!
// 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]
Ez a funkció az F# RFC FS-1077b verziót implementálja.
Az F#-idézetek fejlesztései
Az F# -kód idézőjelei mostantól megőrizhetik a típusmegkötési információkat. Vegyük a következő példát:
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 függvény által inline
létrehozott kényszer megmarad a kód idézőjelében. A negate
függvény idézett űrlapja mostantól kiértékelhető.
Ez a funkció az F# RFC FS-1071-et implementálja.
Alkalmazásalapú számítási kifejezések
A számítási kifejezések (CE-k) ma a "környezetfüggő számítások" modellezésére szolgálnak, vagy funkcionálisabb, programozási barát terminológiában, a monadikus számításokban.
Az F# 5 alkalmazásalapú CE-ket vezet be, amelyek egy másik számítási modellt kínálnak. Az applicative CEs lehetővé teszi a hatékonyabb számítások elvégzését, feltéve, hogy minden számítás független, és az eredmények a végén halmozódnak fel. Ha a számítások egymástól függetlenek, akkor is triviálisan párhuzamosak, így a CE-szerzők hatékonyabb kódtárakat írhatnak. Ez az előny azonban korlátozással jár: a korábban kiszámított értékektől függő számítások nem engedélyezettek.
Az alábbi példa egy egyszerű, a típushoz használható Result
CE-t mutat be.
// 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
Ha Ön olyan könyvtárszerző, aki ma teszi elérhetővé a CES-eket a tárukban, néhány további szempontot is figyelembe kell vennie.
Ez a funkció az F# RFC FS-1063-at implementálja.
A felületek különböző általános példányokban implementálhatók
Most már implementálhatja ugyanazt a felületet különböző általános példányokon:
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"
Ez a funkció az F# RFC FS-1031-et implementálja.
Alapértelmezett felülettag-használat
Az F# 5 lehetővé teszi az alapértelmezett implementációkkal rendelkező felületek használatát.
Fontolja meg a C#-ban definiált felületet a következő módon:
using System;
namespace CSharp
{
public interface MyDim
{
public int Z => 0;
}
}
Az interfész implementálásának bármely szabványos eszközén keresztül használhatja az F#-ban:
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}"
Így biztonságosan kihasználhatja a modern C# nyelven írt C#-kód és .NET-összetevők előnyeit, amikor elvárják, hogy a felhasználók használhassák az alapértelmezett implementációt.
Ez a funkció az F# RFC FS-1074-et implementálja.
Egyszerűsített interop null értékű típusokkal
A null értékű (korábbi nevén Nullable Types) típusokat már régóta támogatja az F#, de a velük való interakció hagyományosan némi fájdalommal jár, mivel minden alkalommal létre kell hoznunk egy Nullable
vagy Nullable<SomeType>
több burkolót, amikor értéket szeretnénk átadni. Most a fordító implicit módon átalakít egy értéktípust egy Nullable<ThatValueType>
olyan értékgé, amely megfelel a céltípusnak. A következő kód már lehetséges:
#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")))
Ez a funkció implementálja az F# RFC FS-1075-öt.
Előzetes verzió: fordított indexek
Az F# 5 egy előzetes verziót is bevezet a fordított indexek engedélyezéséhez. A szintaxis a következő: ^idx
. Az 1. elem értéke a lista végéről a következőképpen érhető el:
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
A fordított indexeket a saját típusaihoz is definiálhatja. Ehhez a következő módszert kell implementálnia:
GetReverseIndex: dimension: int -> offset: int
Íme egy példa a Span<'T>
típusra:
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
Ez a funkció az F# RFC FS-1076-ot implementálja.
Előzetes verzió: az egyéni kulcsszavak túlterhelése számítási kifejezésekben
A számítási kifejezések hatékony funkciókat jelentenek a kódtárak és a keretrendszerek szerzői számára. Lehetővé teszik, hogy jelentősen javítsa az összetevők kifejezőképességét azáltal, hogy lehetővé teszi, hogy jól ismert tagokat definiáljon, és DSL-t alakítson ki a tartomány számára, amelyben dolgozik.
Az F# 5 előzetes verzióval támogatja az egyéni műveletek túlterhelését a Számítási kifejezésekben. Lehetővé teszi a következő kód írását és használatát:
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)
}
A módosítás előtt megírhatja a InputBuilder
típust, de a példában használt módon nem használhatja. Mivel a túlterhelések, az opcionális paraméterek és a típusok System.ParamArray
engedélyezettek, minden úgy működik, ahogy elvárná.
Ez a funkció az F# RFC FS-1056-ot implementálja.