Dela via


Nyheter i F# 5

F# 5 lägger till flera förbättringar av F#-språket och F# Interactive. Den släpps med .NET 5.

Du kan ladda ned den senaste .NET SDK:en från nedladdningssidan för .NET.

Kom igång

F# 5 finns i alla .NET Core-distributioner och Visual Studio-verktyg. Mer information finns i Kom igång med F# för mer information.

Paketreferenser i F#-skript

F# 5 ger stöd för paketreferenser i F#-skript med #r "nuget:..." syntax. Tänk till exempel på följande paketreferens:

#r "nuget: Newtonsoft.Json"

open Newtonsoft.Json

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

printfn $"{JsonConvert.SerializeObject o}"

Du kan också ange en explicit version efter namnet på paketet så här:

#r "nuget: Newtonsoft.Json,11.0.1"

Paketreferenser stöder paket med inbyggda beroenden, till exempel ML.NET.

Paketreferenser stöder även paket med särskilda krav för att referera till beroende .dlls. FParsec-paketet som används för att kräva att användarna manuellt ser till att dess beroende FParsecCS.dll refererades först innan FParsec.dll refererades till i F# Interactive. Detta behövs inte längre och du kan referera till paketet på följande sätt:

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

Den här funktionen implementerar F# Tooling RFC FST-1027. Mer information om paketreferenser finns i den interaktiva självstudien F# .

Stränginterpolering

F#-interpolerade strängar är ganska lika C# eller JavaScript-interpolerade strängar, eftersom de låter dig skriva kod i "hål" inuti en strängliteral. Här är ett grundläggande exempel:

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

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

Men F#-interpolerade strängar gör det också möjligt för typade interpoleringar, precis som sprintf funktionen, att framtvinga att ett uttryck inuti en interpolerad kontext överensstämmer med en viss typ. Den använder samma formatspecificerare.

let name = "Phillip"
let age = 29

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

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

I exemplet med föregående typ av interpolering %s kräver interpoleringen att den är av typen string, medan %d den kräver att interpolationen är en integer.

Dessutom kan godtyckliga F#-uttryck (eller uttryck) placeras i en interpoleringskontext. Det går till och med att skriva ett mer komplicerat uttryck, så här:

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

Även om vi inte rekommenderar att du gör detta för mycket i praktiken.

Den här funktionen implementerar F# RFC FS-1001.

Stöd för nameof

F# 5 stöder operatorn nameof , vilket löser den symbol som används för och genererar dess namn i F#-källan. Detta är användbart i olika scenarier, till exempel loggning, och skyddar din loggning mot ändringar i källkoden.

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

Den sista raden genererar ett undantag och "månad" visas i felmeddelandet.

Du kan ta ett namn på nästan alla F#-konstruktioner:

module M =
    let f x = nameof x

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

Tre sista tillägg är ändringar i hur operatorer fungerar: tillägg av nameof<'type-parameter> formuläret för generiska typparametrar och möjligheten att använda nameof som ett mönster i ett mönstermatchningsuttryck.

Om du tar ett namn på en operator får du dess källsträng. Om du behöver det kompilerade formuläret använder du det kompilerade namnet på en operator:

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

Om du tar namnet på en typparameter krävs en något annorlunda syntax:

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

Detta liknar operatorerna typeof<'T> och typedefof<'T> .

F# 5 lägger också till stöd för ett nameof mönster som kan användas i match uttryck:

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

Föregående kod använder "nameof" i stället för strängliteralen i matchningsuttrycket.

Den här funktionen implementerar F# RFC FS-1003.

Öppna typdeklarationer

F# 5 lägger också till stöd för öppna typdeklarationer. En öppen typdeklaration är som att öppna en statisk klass i C#, förutom med viss annan syntax och något annorlunda beteende för att passa F#-semantik.

Med öppna typdeklarationer kan open du välja vilken typ som helst för att exponera statiskt innehåll i den. Dessutom kan open du F#-definierade fackföreningar och poster för att exponera deras innehåll. Detta kan till exempel vara användbart om du har en union definierad i en modul och vill komma åt dess ärenden, men inte vill öppna hela modulen.

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

Till skillnad från C#, när du open type använder två typer som exponerar en medlem med samma namn, skuggar medlemmen från den sista typen som openär ed det andra namnet. Detta överensstämmer med F#-semantik kring skuggning som redan finns.

Den här funktionen implementerar F# RFC FS-1068.

Konsekvent segmenteringsbeteende för inbyggda datatyper

Beteende vid segmentering av inbyggda datatyper (matris, lista, sträng, 2D-matris, 3D-matris, 4D-matris FSharp.Core ) används för att inte vara konsekvent före F# 5. Vissa kantfallsbeteende utlöste ett undantag och andra inte. I F# 5 returnerar alla inbyggda typer nu tomma sektorer för sektorer som är omöjliga att generera:

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

Den här funktionen implementerar F# RFC FS-1077.

Segment med fast index för 3D- och 4D-matriser i FSharp.Core

F# 5 ger stöd för segmentering med ett fast index i de inbyggda 3D- och 4D-matristyperna.

För att illustrera detta bör du överväga följande 3D-matris:

z = 0

x\y 0 1
0 0 1
1 2 3

z = 1

x\y 0 1
0 4 5
1 6 7

Vad händer om du vill extrahera sektorn [| 4; 5 |] från matrisen? Detta är nu mycket enkelt!

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

Den här funktionen implementerar F# RFC FS-1077b.

Förbättringar av F#-citat

F#- kodofferter har nu möjlighet att behålla typbegränsningsinformation. Ta följande som exempel:

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

Begränsningen som genereras av inline funktionen behålls i kodofferten. Funktionens negate angivna formulär kan nu utvärderas.

Den här funktionen implementerar F# RFC FS-1071.

Applicativa beräkningsuttryck

Beräkningsuttryck används idag för att modellera "kontextuella beräkningar" eller i mer funktionell programmeringsvänlig terminologi, monatiska beräkningar.

F# 5 introducerar applicativa CE:er, som erbjuder en annan beräkningsmodell. Applicativa CE:er möjliggör effektivare beräkningar förutsatt att varje beräkning är oberoende och att deras resultat ackumuleras i slutet. När beräkningar är oberoende av varandra är de också trivialt parallella, vilket gör det möjligt för CE-författare att skriva effektivare bibliotek. Den här förmånen har dock en begränsning: beräkningar som är beroende av tidigare beräknade värden tillåts inte.

I följande exempel visas en grundläggande applikativ CE för Result typen.

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

Om du är en biblioteksförfattare som exponerar CEs i sitt bibliotek idag finns det några ytterligare överväganden som du måste vara medveten om.

Den här funktionen implementerar F# RFC FS-1063.

Gränssnitt kan implementeras på olika allmänna instansier

Nu kan du implementera samma gränssnitt på olika allmänna instansier:

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"

Den här funktionen implementerar F# RFC FS-1031.

Standardförbrukning för gränssnittsmedlem

Med F# 5 kan du använda gränssnitt med standardimplementeringar.

Överväg ett gränssnitt som definierats i C# så här:

using System;

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

Du kan använda det i F# via något av standardsätten för att implementera ett gränssnitt:

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

På så sätt kan du på ett säkert sätt dra nytta av C#-kod och .NET-komponenter som skrivits i modern C# när de förväntar sig att användarna ska kunna använda en standardimplementering.

Den här funktionen implementerar F# RFC FS-1074.

Förenklad interop med nullbara värdetyper

Nullbara (värde) typer (kallas nullbara typer historiskt) har länge stötts av F#, men att interagera med dem har traditionellt varit något av en smärta eftersom du måste konstruera en Nullable eller Nullable<SomeType> wrapper varje gång du vill skicka ett värde. Nu konverterar kompilatorn implicit en värdetyp till en Nullable<ThatValueType> om måltypen matchar. Följande kod är nu möjlig:

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

Den här funktionen implementerar F# RFC FS-1075.

Förhandsversion: omvända index

F# 5 introducerar också en förhandsversion för att tillåta omvända index. Syntax: ^idx. Så här kan du använda ett element 1-värde från slutet av en lista:

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

Du kan också definiera omvända index för dina egna typer. För att göra det måste du implementera följande metod:

GetReverseIndex: dimension: int -> offset: int

Här är ett exempel på Span<'T> typen:

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

Den här funktionen implementerar F# RFC FS-1076.

Förhandsversion: överlagringar av anpassade nyckelord i beräkningsuttryck

Beräkningsuttryck är en kraftfull funktion för biblioteks- och ramverksförfattare. De gör att du kan förbättra uttrycksfullheten hos dina komponenter avsevärt genom att du kan definiera välkända medlemmar och skapa en DSL för den domän som du arbetar i.

F# 5 lägger till förhandsversionsstöd för överlagring av anpassade åtgärder i beräkningsuttryck. Det gör att följande kod kan skrivas och användas:

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

Innan den här ändringen kan du skriva InputBuilder typen som den är, men du kunde inte använda den som den används i exemplet. Eftersom överlagringar, valfria parametrar och nu System.ParamArray typer tillåts fungerar allt precis som förväntat.

Den här funktionen implementerar F# RFC FS-1056.