Co nowego w języku F# 5

Język F# 5 dodaje kilka ulepszeń języka F# i F# Interactive. Jest on udostępniany za pomocą platformy .NET 5.

Najnowszy zestaw .NET SDK można pobrać ze strony pobierania platformy .NET.

Rozpocznij

Język F# 5 jest dostępny we wszystkich dystrybucjach platformy .NET Core i narzędziach programu Visual Studio. Aby uzyskać więcej informacji, zobacz Wprowadzenie do języka F# , aby dowiedzieć się więcej.

Odwołania do pakietów w skryptach języka F#

Język F# 5 zapewnia obsługę odwołań do pakietów w skryptach języka F# ze składnią #r "nuget:..." . Rozważmy na przykład następujące odwołanie do pakietu:

#r "nuget: Newtonsoft.Json"

open Newtonsoft.Json

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

printfn $"{JsonConvert.SerializeObject o}"

Możesz również podać jawną wersję po nazwie pakietu w następujący sposób:

#r "nuget: Newtonsoft.Json,11.0.1"

Odwołania do pakietów obsługują pakiety z natywnymi zależnościami, takimi jak ML.NET.

Odwołania do pakietów obsługują również pakiety ze specjalnymi wymaganiami dotyczącymi odwoływania się do .dllzależności. Na przykład pakiet FParsec używany do ręcznego wymagania, aby użytkownicy ręcznie upewnili się, że jego zależność FParsecCS.dll została przywoływała jako pierwsza, zanim FParsec.dll została odwołana w języku F# Interactive. Nie jest to już potrzebne i możesz odwołać się do pakietu w następujący sposób:

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

Ta funkcja implementuje narzędzia języka F# RFC FST-1027. Aby uzyskać więcej informacji na temat odwołań do pakietów, zobacz samouczek interaktywny języka F#.

Interpolacja ciągów

Ciągi interpolowane w języku F# są dość podobne do ciągów interpolowanych w języku C# lub JavaScript, dzięki czemu umożliwiają one pisanie kodu w "dziurach" wewnątrz literału ciągu. Poniżej przedstawiono prosty przykład:

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

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

Jednak ciągi interpolowane języka F# umożliwiają również stosowanie interpolacji typowych, podobnie jak sprintf funkcja, aby wymusić, że wyrażenie wewnątrz kontekstu interpolowanego jest zgodne z określonym typem. Używa on tych samych specyfikatorów formatu.

let name = "Phillip"
let age = 29

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

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

W poprzednim przykładzie %s interpolacji typizowanej wymaga interpolacji typu string, a %d parametr wymaga interpolacji jako .integer

Ponadto dowolne wyrażenie języka F# (lub wyrażenia) można umieścić po stronie kontekstu interpolacji. Istnieje nawet możliwość napisania bardziej skomplikowanego wyrażenia, w następujący sposó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]
}
"""

Chociaż nie zalecamy robienia tego zbyt wiele w praktyce.

Ta funkcja implementuje F# RFC FS-1001.

Obsługa nazwy

Język F# 5 obsługuje nameof operator, który rozpoznaje symbol używany do obsługi i generuje jego nazwę w źródle języka F#. Jest to przydatne w różnych scenariuszach, takich jak rejestrowanie i chroni rejestrowanie przed zmianami w kodzie źródłowym.

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

Ostatni wiersz zgłosi wyjątek, a komunikat o błędzie zostanie wyświetlony w komunikacie o błędzie.

Możesz przyjąć nazwę niemal każdej konstrukcji języka F#:

module M =
    let f x = nameof x

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

Trzy ostatnie dodatki to zmiany sposobu działania operatorów: dodanie nameof<'type-parameter> formularza dla parametrów typu ogólnego oraz możliwość użycia nameof jako wzorca w wyrażeniu dopasowania wzorca.

Biorąc nazwę operatora, podaje jego ciąg źródłowy. Jeśli potrzebujesz skompilowanego formularza, użyj skompilowanej nazwy operatora:

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

Pobranie nazwy parametru typu wymaga nieco innej składni:

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

Jest to podobne do operatorów typeof<'T> i typedefof<'T> .

Język F# 5 dodaje również obsługę nameof wzorca, który może być używany w match wyrażeniach:

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

Powyższy kod używa ciągu "nameof" zamiast literału ciągu w wyrażeniu dopasowania.

Ta funkcja implementuje F# RFC FS-1003.

Deklaracje typu otwartego

Język F# 5 dodaje również obsługę deklaracji typu otwartego. Deklaracja typu otwartego przypomina otwieranie klasy statycznej w języku C#, z wyjątkiem innej składni i nieco innego zachowania w celu dopasowania semantyki języka F#.

W przypadku deklaracji typu otwartego można open w dowolnym typie uwidocznić zawartość statyczną wewnątrz. Ponadto można uwidocznić ich zawartość, aby uwidocznić ich zawartość, a także można je zdefiniować open w języku F#. Może to być na przykład przydatne, jeśli masz związek zdefiniowany w module i chcesz uzyskać dostęp do jego przypadków, ale nie chcesz otwierać całego modułu.

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

W przeciwieństwie do języka C#, jeśli w dwóch typach uwidaczniasz open type element członkowski o tej samej nazwie, element członkowski z ostatniego typu openjest w tle innej nazwy. Jest to zgodne z semantyki języka F# wokół cieniowania, które już istnieją.

Ta funkcja implementuje F# RFC FS-1068.

Spójne zachowanie fragmentowania dla wbudowanych typów danych

Zachowanie podczas fragmentowania wbudowanych FSharp.Core typów danych (tablica, lista, ciąg, tablica 2D, tablica 3D, tablica 4D) nie była spójna przed F# 5. Niektóre zachowania typu edge-case zgłosiły wyjątek, a niektóre nie. W języku F# 5 wszystkie wbudowane typy zwracają teraz puste wycinki dla wycinków, które są niemożliwe do wygenerowania:

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

Ta funkcja implementuje F# RFC FS-1077.

Wycinki indeksu stałego dla tablic 3D i 4D w technologii FSharp.Core

Język F# 5 zapewnia obsługę fragmentowania ze stałym indeksem we wbudowanych typach tablic 3D i 4D.

Aby to zilustrować, rozważmy następującą tablicę 3D:

z = 0

x\y 0 1
0 0 1
1 2 3

z = 1

x\y 0 1
0 100 5
1 6 7

Co zrobić, jeśli chcesz wyodrębnić fragment [| 4; 5 |] z tablicy? Jest to teraz bardzo proste!

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

Ta funkcja implementuje F# RFC FS-1077b.

Ulepszenia cudzysłowów języka F#

Cudzysłowy kodu języka F# mają teraz możliwość zachowania informacji o ograniczeniu typu. Rozważmy następujący przykład:

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

Ograniczenie generowane przez inline funkcję jest zachowywane w cudzysłowie kodu. Formularz negate cytowany funkcji można teraz ocenić.

Ta funkcja implementuje F# RFC FS-1071.

Wyrażenia obliczeniowe stosowania

Wyrażenia obliczeniowe (CE) są obecnie używane do modelowania "obliczeń kontekstowych" lub w bardziej funkcjonalnej terminologii przyjaznej programistycznej, monadic obliczeniowych.

Język F# 5 wprowadza aplikacje CE, które oferują inny model obliczeniowy. Aplikacje CE umożliwiają bardziej wydajne obliczenia pod warunkiem, że każde obliczenie jest niezależne, a ich wyniki są zbierane na końcu. Gdy obliczenia są niezależne od siebie, są one również trywialnie równoległe, dzięki czemu autorzy CE mogą pisać bardziej wydajne biblioteki. Ta korzyść wiąże się jednak z ograniczeniem: obliczenia, które zależą od wcześniej obliczonych wartości, nie są dozwolone.

Poniższy przykład przedstawia podstawowy stosator CE dla Result typu.

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

Jeśli obecnie jesteś autorem biblioteki, który uwidacznia urzędy certyfikacji w swojej bibliotece, musisz pamiętać o kilku dodatkowych kwestiach.

Ta funkcja implementuje F# RFC FS-1063.

Interfejsy można zaimplementować w różnych wystąpieniach ogólnych

Teraz możesz zaimplementować ten sam interfejs w różnych wystąpieniach ogólnych:

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"

Ta funkcja implementuje F# RFC FS-1031.

Użycie domyślnego elementu członkowskiego interfejsu

Język F# 5 umożliwia korzystanie z interfejsów z domyślnymi implementacjami.

Rozważ użycie interfejsu zdefiniowanego w języku C#w następujący sposób:

using System;

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

Można go używać w języku F# za pomocą dowolnego standardowego sposobu implementowania interfejsu:

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

Dzięki temu można bezpiecznie korzystać z kodu C# i składników platformy .NET napisanych we współczesnym języku C#, gdy oczekują, że użytkownicy będą mogli korzystać z domyślnej implementacji.

Ta funkcja implementuje F# RFC FS-1074.

Uproszczone współdziałanie z typami wartości dopuszczanymi do wartości null

Typy dopuszczające wartość null (nazywane typami dopuszczającym wartość null) od dawna są obsługiwane przez język F#, ale interakcja z nimi tradycyjnie była nieco uciążliwa, ponieważ trzeba by było utworzyć Nullable otokę lub Nullable<SomeType> za każdym razem, gdy chcesz przekazać wartość. Teraz kompilator niejawnie przekonwertuje typ wartości na Nullable<ThatValueType> wartość , jeśli typ docelowy jest zgodny. Teraz jest możliwy następujący kod:

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

Ta funkcja implementuje F# RFC FS-1075.

Podgląd: indeksy odwrotne

W języku F# 5 wprowadzono również wersję zapoznawcza umożliwiającą indeksy odwrotne. Składnia jest następująca: ^idx Oto jak można elementy 1 wartość z końca listy:

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

Można również zdefiniować indeksy odwrotne dla własnych typów. W tym celu należy zaimplementować następującą metodę:

GetReverseIndex: dimension: int -> offset: int

Oto przykład typu Span<'T> :

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

Ta funkcja implementuje F# RFC FS-1076.

Wersja zapoznawcza: przeciążenia niestandardowych słów kluczowych w wyrażeniach obliczeniowych

Wyrażenia obliczeniowe to zaawansowana funkcja dla autorów bibliotek i struktur. Pozwalają one znacznie poprawić wyrazistość składników, umożliwiając definiowanie dobrze znanych elementów członkowskich i tworzenie dsl dla domeny, w której pracujesz.

Język F# 5 dodaje obsługę wersji zapoznawczej na potrzeby przeciążania operacji niestandardowych w wyrażeniach obliczeniowych. Umożliwia napisanie i użycie następującego kodu:

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

Przed tą zmianą można napisać InputBuilder typ w taki sposób, jak jest, ale nie można użyć go w taki sposób, w jaki jest używany w przykładzie. Ponieważ przeciążenia, parametry opcjonalne i teraz System.ParamArray typy są dozwolone, wszystko działa tak samo, jak oczekiwano.

Ta funkcja implementuje F# RFC FS-1056.