Neues in F# 5

F# 5 fügt der Sprache F# und der F# Interactive mehrere Verbesserungen hinzu. Es wird mit .NET 5 veröffentlicht.

Sie können das neueste .NET SDK über die .NET-Downloadseite herunterladen.

Erste Schritte

F# 5 ist in allen .NET Core-Distributionen und Visual Studio Tools verfügbar. Weitere Informationen finden Sie unter Erste Schritte mit F#.

Paketverweise in F#-Skripts

F# 5 bietet Unterstützung für Paketverweise in F#-Skripts mit #r "nuget:..." Syntax. Betrachten Sie beispielsweise den folgenden Paketverweis:

#r "nuget: Newtonsoft.Json"

open Newtonsoft.Json

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

printfn $"{JsonConvert.SerializeObject o}"

Sie können auch eine explizite Version nach dem Namen des Pakets wie folgt bereitstellen:

#r "nuget: Newtonsoft.Json,11.0.1"

Paketverweise unterstützen Pakete mit nativen Abhängigkeiten, z. B. ML.NET.

Paketverweise unterstützen auch Pakete mit besonderen Anforderungen zum Verweisen auf abhängige .dll s. Beispiel: Das FParsec-Paket, mit dem Benutzer manuell sicherstellen müssen, dass zuerst auf die abhängige Datei FParsecCS.dll verwiesen wurde, bevor in F# Interactive auf sie FParsec.dll verwiesen wurde. Dies ist nicht mehr erforderlich, und Sie können wie folgt auf das Paket verweisen:

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

Dieses Feature implementiert F#-Tools RFC FST-1027. Weitere Informationen zu Paketverweisen finden Sie im F# Interactive-Tutorial.

Zeichenfolgeninterpolierung

Interpolierte F#-Zeichenfolgen ähneln eher C#- oder JavaScript-interpolierten Zeichenfolgen, da sie ihnen das Schreiben von Code in "Lücken" innerhalb eines Zeichenfolgenliterals erlauben. Im Folgenden finden Sie ein einfaches Beispiel:

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

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

Allerdings ermöglichen interpolierte F#-Zeichenfolgen auch typisierte Interpolationen wie die sprintf -Funktion, um zu erzwingen, dass ein Ausdruck innerhalb eines interpolierten Kontexts einem bestimmten Typ entspricht. Es werden die gleichen Formatbezeichner verwendet.

let name = "Phillip"
let age = 29

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

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

Im vorherigen Beispiel für typisierte Interpolation erfordert , %s dass die Interpolation vom Typ string ist, während die %d -Interpolation erfordert, dass ein integer ist.

Darüber hinaus können beliebige F#-Ausdrücke (oder -Ausdrücke) an der Seite eines Interpolationskontexts platziert werden. Es ist sogar möglich, einen komplizierteren Ausdruck zu schreiben, z. B.:

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

Es wird jedoch nicht empfohlen, dies in der Praxis zu sehr zu tun.

Dieses Feature implementiert F# RFC FS-1001.

Unterstützung für nameof

F# 5 unterstützt den nameof -Operator, der das Symbol auflöst, für das es verwendet wird, und erzeugt seinen Namen in der F#-Quelle. Dies ist in verschiedenen Szenarien nützlich, z. B. bei der Protokollierung, und schützt Ihre Protokollierung vor Änderungen im Quellcode.

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

Die letzte Zeile löst eine Ausnahme aus, und "month" wird in der Fehlermeldung angezeigt.

Sie können fast jedes F#-Konstrukt benennen:

module M =
    let f x = nameof x

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

Drei letzte Ergänzungen sind Änderungen an der Funktionsweise von Operatoren: das Hinzufügen des nameof<'type-parameter> Formulars für generische Typparameter und die Möglichkeit, nameof als Muster in einem Musterabgleichsausdruck zu verwenden.

Wenn Sie einen Namen eines Operators verwenden, wird seine Quellzeichenfolge angezeigt. Wenn Sie das kompilierte Formular benötigen, verwenden Sie den kompilierten Namen eines Operators:

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

Der Name eines Typparameters erfordert eine etwas andere Syntax:

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

Dies ähnelt den typeof<'T> Operatoren und typedefof<'T> .

F# 5 fügt auch Unterstützung für ein nameof Muster hinzu, das in Ausdrücken verwendet werden match kann:

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

Im vorangehenden Code wird "nameof" anstelle des Zeichenfolgenliterals im Übereinstimmungsausdruck verwendet.

Dieses Feature implementiert F# RFC FS-1003.

Öffnen von Typdeklarationen

F# 5 fügt auch Unterstützung für offene Typdeklarationen hinzu. Eine offene Typdeklaration entspricht dem Öffnen einer statischen Klasse in C#, mit Ausnahme einer anderen Syntax und eines etwas anderen Verhaltens, um die F#-Semantik anzupassen.

Mit offenen Typdeklarationen können Sie open einen beliebigen Typ angeben, um darin statische Inhalte verfügbar zu machen. Darüber hinaus können Sie open F#-definierte Unions und Datensätze verwenden, um ihre Inhalte verfügbar zu machen. Dies kann beispielsweise nützlich sein, wenn Sie eine Union in einem Modul definiert haben und auf die Fälle zugreifen möchten, aber nicht das gesamte Modul öffnen möchten.

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

Wenn Sie im Gegensatz zu C# zwei Typen verwenden, open type die einen Member mit dem gleichen Namen verfügbar machen, überschatten die Member aus dem letzten Typ, der open ed ist, den anderen Namen. Dies entspricht der F#-Semantik um schattende, bereits vorhandene Schatten.

Dieses Feature implementiert F# RFC FS-1068.

Konsistentes Slicingverhalten für integrierte Datentypen

Das Verhalten beim Aufschneiden der integrierten FSharp.Core Datentypen (Array, Liste, Zeichenfolge, 2D-Array, 3D-Array, 4D-Array) war vor F# 5 nicht konsistent. Ein Edgefallverhalten hat eine Ausnahme ausgelöst, andere nicht. In F# 5 geben alle integrierten Typen jetzt leere Slices für Slices zurück, die nicht generiert werden können:

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

Dieses Feature implementiert F# RFC FS-1077.

Slices mit festem Index für 3D- und 4D-Arrays in FSharp.Core

F# 5 bietet Unterstützung für das Aufschneiden mit einem festen Index in den integrierten 3D- und 4D-Arraytypen.

Betrachten Sie das folgende 3D-Array, um dies zu veranschaulichen:

z = 0

x\y 0 1
0 0 1
1 2 3

z = 1

x\y 0 1
0 4 5
1 6 7

Was geschieht, wenn Sie den Slice aus dem Array extrahieren [| 4; 5 |] möchten? Dies ist jetzt sehr einfach!

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

Dieses Feature implementiert F# RFC FS-1077b.

Verbesserungen bei F#-Anführungszeichen

F#-Codeanführungszeichen haben jetzt die Möglichkeit, Typeinschränkungsinformationen beizubehalten. Betrachten Sie das folgende Beispiel:

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

Die von der Funktion generierte Einschränkung inline wird im Codeanführungszeichen beibehalten. Das Formular in negate Anführungszeichen der Funktion kann jetzt ausgewertet werden.

Dieses Feature implementiert F# RFC FS-1071.

Anwendungsberechnungsausdrücke

Berechnungsausdrücke (CEs) werden heute verwendet, um "kontextbezogene Berechnungen" zu modellieren, oder in funktionaler programmierfreundlicherer Terminologie, mithilfe von rechenorientierten Berechnungen.

In F# 5 werden anwendungsbasierte CEs eingeführt, die ein anderes Berechnungsmodell bieten. Anwendungs-CEs ermöglichen effizientere Berechnungen, vorausgesetzt, jede Berechnung ist unabhängig, und ihre Ergebnisse werden am Ende gesammelt. Wenn Berechnungen voneinander unabhängig sind, sind sie auch trivial parallelisierbar, sodass CE-Autoren effizientere Bibliotheken schreiben können. Dieser Vorteil ist jedoch eingeschränkt: Berechnungen, die von zuvor berechneten Werten abhängen, sind nicht zulässig.

Das folgende Beispiel zeigt eine grundlegende Anwendbarkeits-CE für den Result Typ.

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

Wenn Sie ein Bibliotheksautor sind, der heute CEs in seiner Bibliothek verfügbar macht, müssen Sie einige zusätzliche Überlegungen beachten.

Dieses Feature implementiert F# RFC FS-1063.

Schnittstellen können bei verschiedenen generischen Instanziierungen implementiert werden.

Sie können jetzt dieselbe Schnittstelle in verschiedenen generischen Instanziierungen implementieren:

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"

Dieses Feature implementiert F# RFC FS-1031.

Nutzung von Standardschnittstellenmembern

Mit F# 5 können Sie Schnittstellen mit Standardimplementierungennutzen.

Betrachten Sie eine in C# definierte Schnittstelle wie folgt:

using System;

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

Sie können sie in F# über eine der Standard-Möglichkeiten zum Implementieren einer Schnittstelle nutzen:

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

Dadurch können Sie C#-Code und .NET-Komponenten, die in modernem C# geschrieben wurden, sicher nutzen, wenn sie erwarten, dass Benutzer eine Standardimplementierungen nutzen können.

Dieses Feature implementiert F# RFC FS-1074.

Vereinfachte Interop mit Nullable-Werttypen

Nullable-Typen (Werttypen), die in der Vergangenheit als Nullable-Typen bezeichnet wurden, werden schon lange von F# unterstützt, aber die Interaktion mit ihnen war bisher etwas mühsam, da Sie jedes Mal, wenn Sie einen Wert übergeben möchten, einen Nullable - oder Nullable<SomeType> -Wrapper erstellen müssten. Der Compiler konvertiert nun implizit einen Werttyp in einen Nullable<ThatValueType> , wenn der Zieltyp übereinstimmt. Der folgende Code ist jetzt möglich:

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

Dieses Feature implementiert F# RFC FS-1075.

Vorschau: Umgekehrte Indizes

In F# 5 wird auch eine Vorschauversion eingeführt, um umgekehrte Indizes zuzulassen. Die Syntax lautet ^idx. So können Sie einen Element 1-Wert am Ende einer Liste erstellen:

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

Sie können auch umgekehrte Indizes für Ihre eigenen Typen definieren. Dazu müssen Sie die folgende Methode implementieren:

GetReverseIndex: dimension: int -> offset: int

Hier sehen Sie ein Beispiel für den Span<'T> Typ:

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

Dieses Feature implementiert F# RFC FS-1076.

Vorschau: Überladungen benutzerdefinierter Schlüsselwörter in Berechnungsausdrücken

Berechnungsausdrücke sind ein leistungsstarkes Feature für Bibliotheks- und Frameworkautoren. Sie ermöglichen es Ihnen, die Ausdrucksstärke Ihrer Komponenten erheblich zu verbessern, indem Sie bekannte Member definieren und eine DSL für die Domäne bilden können, in der Sie arbeiten.

F# 5 fügt Vorschauunterstützung für das Überladen von benutzerdefinierten Vorgängen in Berechnungsausdrücken hinzu. Dadurch kann der folgende Code geschrieben und genutzt werden:

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

Vor dieser Änderung konnten Sie den InputBuilder Typ so schreiben, wie er ist, aber Sie konnten ihn nicht so verwenden, wie er im Beispiel verwendet wird. Da Überladungen, optionale Parameter und jetzt System.ParamArray Typen zulässig sind, funktioniert alles wie erwartet.

Dieses Feature implementiert F# RFC FS-1056.