Neues in F# 5

In F# 5 wurden einige Verbesserungen an F# und F# Interactive vorgenommen. Die Version 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. Berücksichtigen Sie beispielsweise 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 angeben:

#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 speziellen Anforderungen zum Verweisen auf abhängige .dlls. Beispielsweise erforderte das FParsec-Paket, dass Benutzer manuell sicherstellten, dass in F# Interactive auf das abhängige FParsecCS.dll-Paket verwiesen wurde, bevor auf 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#-Tool RFC FST-1027. Weitere Informationen zu Paketverweise finden Sie im F# Interactive-Tutorial.

Zeichenfolgeninterpolierung

Interpolierte F#-Zeichenfolgen ähneln in etwa interpolierten C#- oder JavaScript-Zeichenfolgen, da Sie Code in „Löcher“ innerhalb eines Zeichenfolgenliterals schreiben können. 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}!"

Interpolierte F#-Zeichenfolgen ermöglichen jedoch auch genau wie die sprintf-Funktion typisierte Interpolationen, um zu erzwingen, dass ein Ausdruck innerhalb eines interpolierten Kontexts einem bestimmten Typ entspricht. Es werden dieselben 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 typisierten Interpolationsbeispiel erfordert %s, dass die Interpolation vom Typ string ist, während %d erfordert, dass die Interpolation vom Typ integer ist.

Darüber hinaus können beliebige F#-Ausdrücke neben einem Interpolationskontext platziert werden. Es ist sogar möglich, einen komplizierteren Ausdruck wie diesen zu schreiben:

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

Sie sollten dies in der Praxis allerdings nicht übertreiben.

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 er 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 einen Namen von fast jedem F#-Konstrukt nehmen:

module M =
    let f x = nameof x

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

Drei abschließende 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 Mustervergleichsausdruck zu verwenden.

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

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

Die Verwendung des Namens eines Typparameters erfordert eine etwas andere Syntax:

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

Das entspricht den Operatoren typeof<'T> und typedefof<'T>.

F# 5 unterstützt auch ein nameof-Muster, das in match-Ausdrücken verwendet werden 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

Der vorangehende Code verwendet „nameof“ anstelle des Zeichenfolgenliterals im Vergleichsausdruck.

Dieses Feature implementiert F# RFC FS-1003.

Offene Typdeklarationen

F# 5 unterstützt auch offene Typdeklarationen. Eine offene Typdeklaration ist wie das Öffnen einer statischen Klasse in C#, nur mit einer anderen Syntax und einem etwas anderen Verhalten, um der F#-Semantik zu entsprechen.

Mit offenen Typdeklarationen können Sie einen beliebigen Typ öffnen (open), um statische Inhalte darin verfügbar zu machen. Darüber hinaus können Sie F#-definierte Unions und Datensätze öffnen (open), um deren Inhalt verfügbar zu machen. Dies kann z. B. hilfreich sein, wenn Sie eine Union in einem Modul definiert haben und auf die zugehörigen Fälle zugreifen, 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 zwei Typen öffnen (open type), die einen Member mit demselben Namen verfügbar machen, führt im Gegensatz zu C# der Member des letzten geöffneten (opened) Typs ein Shadowing des anderen Namens durch. Dies ist konsistent mit der bereits vorhandenen F#-Semantik rund um das Shadowing.

Dieses Feature implementiert F# RFC FS-1068.

Konsistentes Slicingverhalten für integrierte Datentypen

Das Verhalten beim Slicing der integrierten FSharp.Core-Datentypen (Array, Liste, Zeichenfolge, 2D-Array, 3D-Array, 4D-Array ), war vor F# 5 nicht konsistent. Einige Grenzfallverhaltensweisen haben 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 unterstützt das Slicing mit einem festen Index in den integrierten 3D- und 4D-Arraytypen.

Betrachten Sie zur Veranschaulichung folgendes 3D-Array:

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 ist, wenn Sie den Slice [| 4; 5 |] aus dem Array extrahieren möchten? Das ist jetzt ganz 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#-Zitaten

In F#-Codezitaten können jetzt Informationen zur Typeinschränkung beibehalten werden. Betrachten Sie das folgenden 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 inline-Funktion generierte Einschränkung wird im Codezitat beibehalten. Die zitierte Form der negate-Funktion kann ausgewertet werden.

Dieses Feature implementiert F# RFC FS-1071.

Applikative Berechnungsausdrücke

Berechnungsausdrücke (Computation Expressions, CEs) werden heute verwendet, um „kontextbezogene Berechnungen“ zu modellieren, oder in einer funktionaleren, programmierfreundlicheren Terminologie, monadische Berechnungen.

F# 5 führt applikative CEs ein, die ein anderes Berechnungsmodell bieten. Applicative CEs ermöglichen effizientere Berechnungen, vorausgesetzt, dass jede Berechnung unabhängig ist und ihre Ergebnisse am Ende kumuliert werden. Wenn Berechnungen unabhängig voneinander sind, sind sie auch leicht parallelisierbar, sodass CE-Autoren effizientere Bibliotheken schreiben können. Dieser Vorteil hat jedoch eine Einschränkung: Berechnungen, die von zuvor berechneten Werten abhängen, sind nicht zulässig.

Das folgende Beispiel zeigt eine einfache applikative 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 als Bibliotheksautor heute CEs in ihrer Bibliothek verfügbar machen, müssen Sie einige zusätzliche Überlegungen anstellen.

Dieses Feature implementiert F# RFC FS-1063.

Schnittstellen können mit verschiedenen generischen Instanziierungen implementiert werden.

Sie können jetzt dieselbe Schnittstelle mit 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.

Standardmäßige Schnittstellenmembernutzung

F# 5 ermöglicht die Nutzung von Schnittstellen mit Standardimplementierungen.

Betrachten Sie eine in C# definierte Schnittstelle wie diese:

using System;

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

Sie können sie in F# über eine beliebige Standardmethode 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}"

Auf diese Weise können Sie in modernem C# geschriebene C#-Codes und .NET-Komponenten sicher nutzen, wenn sie erwarten, dass Benutzer eine Standardimplementierung nutzen können.

Dieses Feature implementiert F# RFC FS-1074.

Vereinfachte Interoperabilität mit Nullable-Werttypen

Historisch als Nullable-Typen bezeichnete Nullable-(Wert)typen wurden von F# lange unterstützt, aber die Interaktion mit ihnen war immer etwas mühsam, da Sie jedes Mal, wenn Sie einen Wert übergeben wollten, einen Nullable- oder Nullable<SomeType>-Wrapper erstellen mussten. 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

F# 5 bietet auch eine Vorschau zum Zulassen umgekehrter Indizes. Die Syntax lautet ^idx. So können Sie einen Element-1-Wert am Ende einer Liste nutzen:

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 von benutzerdefinierten Schlüsselwörtern in Berechnungsausdrücken

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

F# 5 unterstützt die Vorschau des Überladens von benutzerdefinierten Vorgängen in Berechnungsausdrücken. Dies ermöglicht, folgenden Code zu schreiben und zu verwenden:

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.