Udostępnij za pośrednictwem


Co nowego w języku F# 9

Język F# 9 wprowadza szereg ulepszeń, które czynią Twoje programy bezpieczniejszymi, bardziej odpornymi i wydajnymi. W tym artykule przedstawiono główne zmiany w języku F# 9 opracowane w repozytorium kodu open source języka F# F#.

Język F# 9 jest dostępny na platformie .NET 9. Najnowszy .NET SDK można pobrać ze strony pobierania .NET .

Typy referencyjne dopuszczane do wartości null

Mimo że język F# został zaprojektowany tak, aby uniknąć null, może wkradać się podczas łączenia się z bibliotekami platformy .NET napisanymi w języku C#. Język F# zapewnia teraz bezpieczny dla typu sposób radzenia sobie z typami referencyjnymi, które mogą mieć null jako prawidłową wartość.

Aby uzyskać więcej informacji, zobacz wpis na blogu o typach referencyjnych dopuszczających wartości null w F# 9.

Oto kilka przykładów:

// Declared type at let-binding
let notAValue: string | null = null

let isAValue: string | null = "hello world"

let isNotAValue2: string = null // gives a nullability warning

let getLength (x: string | null) = x.Length // gives a nullability warning since x is a nullable string

// Parameter to a function
let len (str: string | null) =
    match str with
    | null -> -1
    | s -> s.Length  // binds a non-null result - compiler eliminated "null" after the first clause

// Parameter to a function
let len (str: string | null) =
    let s = nullArgCheck "str" str // Returns a non-null string
    s.Length  // binds a non-null result

// Declared type at let-binding
let maybeAValue: string | null = hopefullyGetAString()

// Array type signature
let f (arr: (string | null)[]) = ()

// Generic code, note 'T must be constrained to be a reference type
let findOrNull (index: int) (list: 'T list) : 'T | null when 'T : not struct =
    match List.tryItem index list with
    | Some item -> item
    | None -> null

Właściwości unii dyskryminowanej .Is*

Związki dyskryminowane mają teraz właściwości generowane automatycznie dla każdego przypadku, co pozwala sprawdzić, czy wartość należy do konkretnego przypadku. Na przykład dla następującego typu:

type Contact =
    | Email of address: string
    | Phone of countryCode: int * number: string

type Person = { name: string; contact: Contact }

Wcześniej trzeba było napisać coś takiego jak:

let canSendEmailTo person =
    match person.contact with
    | Email _ -> true
    | _ -> false

Teraz możesz zamiast tego napisać:

let canSendEmailTo person =
    person.contact.IsEmail

Częściowe aktywne wzorce mogą zwracać bool zamiast unit option

Wcześniej częściowe aktywne wzorce zwracały Some (), aby wskazać dopasowanie, a None w przeciwnym razie. Teraz mogą również zwrócić bool.

Na przykład aktywny wzorzec dla następujących elementów:

match key with
| CaseInsensitive "foo" -> ...
| CaseInsensitive "bar" -> ...

Wcześniej został napisany jako:

let (|CaseInsensitive|_|) (pattern: string) (value: string) =
    if String.Equals(value, pattern, StringComparison.OrdinalIgnoreCase) then
        Some ()
    else
        None

Teraz możesz zamiast tego napisać:

let (|CaseInsensitive|_|) (pattern: string) (value: string) =
    String.Equals(value, pattern, StringComparison.OrdinalIgnoreCase)

Preferuj metody rozszerzenia zamiast właściwości wewnętrznych, gdy podano argumenty

Aby dopasować się do wzorca widocznego w niektórych bibliotekach platformy .NET, gdzie metody rozszerzenia są definiowane z takimi samymi nazwami jak właściwości wewnętrzne typu, język F# rozpoznaje teraz te metody rozszerzenia zamiast niepowodzenia sprawdzania typu.

Przykład:

type Foo() =
    member val X : int = 0 with get, set

[<Extension>]
type FooExt =
    [<Extension>]
    static member X (f: Foo, i: int) = f.X <- i; f

let f = Foo()

f.X(1) // We can now call the extension method to set the property and chain further calls

Puste wyrażenia obliczeniowe

Język F# obsługuje teraz puste wyrażenia obliczeniowe .

let xs = seq { } // Empty sequence
let html =
    div {
        p { "Some content." }
        p { } // Empty paragraph
    }

Napisanie pustego wyrażenia obliczeniowego spowoduje wywołanie metody Zero konstruktora wyrażeń obliczeniowych.

Jest to bardziej naturalna składnia w porównaniu z wcześniej dostępnymi builder { () }.

Dyrektywy z haszem mogą przyjmować argumenty, które nie są łańcuchami znaków.

Dyrektywy preprocesora dla kompilatora wcześniej zezwalały tylko na argumenty tekstowe umieszczane w cudzysłowach. Teraz mogą przyjmować dowolny typ argumentu.

Wcześniej miałeś:

#nowarn "0070"
#time "on"

Teraz możesz napisać:

#nowarn 0070
#time on

Wiąże się to również z dwoma kolejnymi zmianami.

Rozszerzona dyrektywa #help w programie fsi w celu wyświetlenia dokumentacji w środowisku REPL

Dyrektywa #help w języku F# Interactive zawiera teraz dokumentację dla danego obiektu lub funkcji, którą można teraz przekazać bez cudzysłowów.

> #help List.map;;

Description:
Builds a new collection whose elements are the results of applying the given function
to each of the elements of the collection.

Parameters:
- mapping: The function to transform elements from the input list.
- list: The input list.
Returns:
The list of transformed elements.

Examples:
let inputs = [ "a"; "bbb"; "cc" ]

inputs |> List.map (fun x -> x.Length)
// Evaluates to [ 1; 3; 2 ]

Full name: Microsoft.FSharp.Collections.ListModule.map
Assembly: FSharp.Core.dll

Aby uzyskać więcej informacji, zobacz Ulepszanie #help w blogu F# Interactive.

Zezwalaj #nowarn na obsługę prefiksu FS dla kodów błędów w celu wyłączenia ostrzeżeń

Wcześniej, gdy chciałeś wyłączyć ostrzeżenie i napisałeś #nowarn "FS0057", otrzymywałeś Invalid warning number 'FS0057'. Mimo że numer ostrzeżenia jest poprawny, po prostu nie powinien mieć prefiksu FS.

Teraz nie trzeba poświęcać czasu na ustalenie tego, ponieważ numery ostrzegawcze są akceptowane nawet z prefiksem.

Wszystkie te funkcje będą teraz działać:

#nowarn 57
#nowarn 0057
#nowarn FS0057

#nowarn "57"
#nowarn "0057"
#nowarn "FS0057"

Dobrym pomysłem jest użycie tego samego stylu w całym projekcie.

Ostrzeżenie o atrybucie TailCall w funkcjach niecyklicznych lub wartościach let-bound

Język F# emituje teraz ostrzeżenie, gdy umieścisz atrybut [<TailCall>] gdzieś, do którego nie należy. Chociaż nie ma wpływu na to, co robi kod, może to mylić kogoś, kto go czyta.

Na przykład te użycia będą teraz emitować ostrzeżenie:

[<TailCall>]
let someNonRecFun x = x + x

[<TailCall>]
let someX = 23

[<TailCall>]
let rec someRecLetBoundValue = nameof(someRecLetBoundValue)

Wymuszanie celów atrybutów

Kompilator poprawnie wymusza teraz AttributeTargets dla wartości typu let, funkcji, deklaracji przypadków unii, konstruktorów niejawnych, struktur i klas. Może to zapobiec wystąpieniu niektórych trudnych do zauważenia usterek, takich jak zapominanie o dodaniu argumentu jednostki do testu Xunit.

Wcześniej można było napisać:

[<Fact>]
let ``this test always fails`` =
  Assert.True(false)

Po uruchomieniu testów z dotnet testzostaną one wykonane. Ponieważ funkcja testowa nie jest funkcją, została zignorowana przez moduł uruchamiający testy.

Teraz z poprawnym wymuszaniem atrybutów otrzymasz error FS0842: This attribute is not valid for use on this language element.

Aktualizacje standardowej biblioteki (FSharp.Core)

Funkcje losowe dla kolekcji

Moduły List, Arrayi Seq mają nowe funkcje do losowego próbkowania i mieszania. Ułatwia to korzystanie z języka F# w przypadku typowych zastosowań nauki o danych, uczenia maszynowego, tworzenia gier i innych scenariuszy, w których potrzebna jest losowość.

Wszystkie funkcje mają następujące warianty:

  • Taki, który korzysta z niejawnego, wątkowo bezpiecznego, współdzielonego wystąpienia Random
  • Który przyjmuje wystąpienie Random jako argument
  • Ta, która przyjmuje niestandardową funkcję randomizer, która powinna zwracać wartość zmiennoprzecinkową większą lub równą 0,0 i mniejszą niż 1,0

Dostępne są cztery funkcje (z trzema wariantami): Shuffle, Choice, Choicesi Sample.

Tasować

Funkcje Shuffle zwracają nową kolekcję tego samego typu i rozmiaru, z każdym elementem w losowo mieszanej pozycji. Szansa na znalezienie się na dowolnej pozycji jest równo rozłożona względem długości kolekcji.

let allPlayers = [ "Alice"; "Bob"; "Charlie"; "Dave" ]
let round1Order = allPlayers |> List.randomShuffle // [ "Charlie"; "Dave"; "Alice"; "Bob" ]

W przypadku tablic istnieją również warianty InPlace, które mieszają elementy w istniejącej tablicy zamiast tworzyć nowe.

Wybór

Funkcje Choice zwracają pojedynczy element losowy z danej kolekcji. Losowy wybór jest równomiernie ważony w zależności od rozmiaru kolekcji.

let allPlayers = [ "Alice"; "Bob"; "Charlie"; "Dave" ]
let randomPlayer = allPlayers |> List.randomChoice // "Charlie"

Wybory

Funkcje Choices wybierają N elementów z kolekcji wejściowej w kolejności losowej, co umożliwia wybranie elementów więcej niż raz.

let weather = [ "Raining"; "Sunny"; "Snowing"; "Windy" ]
let forecastForNext3Days = weather |> List.randomChoices 3 // [ "Windy"; "Snowing"; "Windy" ]

Próbka

Funkcje Sample wybierają N elementów z kolekcji wejściowej w kolejności losowej bez zezwalania na wybranie elementów więcej niż raz. N nie może być większa niż długość kolekcji.

let foods = [ "Apple"; "Banana"; "Carrot"; "Donut"; "Egg" ]
let today'sMenu = foods |> List.randomSample 3 // [ "Donut"; "Apple"; "Egg" ]

Aby uzyskać pełną listę funkcji i ich wariantów, zobacz (RFC #1135).

Konstruktor bez parametrów dla CustomOperationAttribute

Ten konstruktor ułatwia tworzenie operacji niestandardowej dla konstruktora wyrażeń obliczeniowych. Używa nazwy metody, zamiast nadawać ją jawnie, ponieważ w większości przypadków jest ona już zgodna z nazwą metody.

type FooBuilder() =
    [<CustomOperation>]  // Previously had to be [<CustomOperation("bar")>]
    member _.bar(state) = state

Obsługa wyrażeń kolekcji języka C# dla list i zestawów języka F#

W przypadku używania list i zestawów języka F# z języka C#, można teraz inicjować je wyrażeniami kolekcji.

Zamiast:

FSharpSet<int> mySet = SetModule.FromArray([1, 2, 3]);

Teraz możesz napisać:

FSharpSet<int> mySet = [ 1, 2, 3 ];

Wyrażenia kolekcji ułatwiają korzystanie z niezmiennych kolekcji języka F# z języka C#. Możesz użyć kolekcji F#, jeśli potrzebujesz ich równości strukturalnej, której nie mają kolekcje System.Collections.Immutable.

Ulepszenia produktywności deweloperów

Odzyskiwanie analizatora

W procesie odzyskiwania analizatora wprowadzono ciągłe ulepszenia, co oznacza, że narzędzia (na przykład wyróżnianie składni) nadal działają na kodzie, podczas jego edytowania, mimo że kod może nie być składniowo poprawny przez cały czas.

Na przykład analizator odzyska teraz niedokończone wzorce as, wyrażenia obiektów, deklaracje przypadków wyliczeniowych, deklaracje rekordów, złożone wzorce konstruktora podstawowego, nierozwiązane długie identyfikatory, puste klauzule dopasowania, brakujące pola przypadków sumy i brakujące typy pól przypadków sumy.

Diagnostyka

Diagnostyka, czyli zrozumienie, co w kodzie nie odpowiada kompilatorowi, jest ważną częścią doświadczenia użytkownika w języku F#. W języku F# 9 istnieje wiele nowych lub ulepszonych komunikatów diagnostycznych lub bardziej precyzyjnych lokalizacji diagnostycznych.

Należą do nich:

  • Niejednoznaczna metoda zastąpienia w wyrażeniu obiektu
  • Abstrakcyjne elementy członkowskie, gdy są używane w klasach nie-abstrakcyjnych
  • Właściwość, która ma taką samą nazwę jak dyskryminowana sprawa związkowa
  • Niezgodność liczby argumentów aktywnego wzorca
  • Unii ze zduplikowanymi polami
  • Używanie use! z and! w wyrażeniach obliczeniowych

Istnieje również nowy błąd kompilacji dla klas zawierających ponad 65 520 metod w wygenerowanym IL. Takie klasy nie są ładowalne przez CLR i powodują błąd czasu wykonywania. (Nie utworzysz wielu metod, ale wystąpiły przypadki z wygenerowanym kodem).

Realna widoczność

Istnieje specyfika w sposobie, w jaki F# generuje zestawy, co powoduje, że prywatne członki są zapisywane w IL jako wewnętrzne. Umożliwia to niewłaściwy dostęp do prywatnych elementów członkowskich z projektów innych niż F#, które mają dostęp do projektu F# za pośrednictwem InternalsVisibleTo.

Teraz istnieje poprawka zgody dla tego zachowania dostępna za pośrednictwem flagi kompilatora --realsig+. Wypróbuj rozwiązanie, aby sprawdzić, czy którykolwiek z projektów zależy od tego zachowania. Możesz dodać go do plików .fsproj w następujący sposób:

<PropertyGroup>
    <RealSig>true</RealSig>
</PropertyGroup>

Ulepszenia wydajności

Zoptymalizowane kontrole równości

Sprawdzanie równości jest teraz szybsze i zużywa mniej pamięci.

Na przykład:

[<Struct>]
type MyId =
    val Id: int
    new id = { Id = id }

let ids = Array.init 1000 MyId
let missingId = MyId -1

// used to box 1000 times, doesn't box anymore
let _ = ids |> Array.contains missingId

Wyniki testów porównawczych dla funkcji tablicowych stosowanych do struktury z 2 elementami, których dotyczy problem.

Przed:

Metoda Znaczyć Błąd Gen0 Przydzielone
ZbiórZawieraIstniejące 15.48 ns 0,398 ns 0.0008 48 B
TablicaZawieraNieistniejące 5190,95 ns 103.533 ns 0.3891 24000 B
ArrayExistsExisting 17.97 ns 0,389 ns 0.0012 72 B
ArrayExistsNonexisting 5 316,64 ns 103.776 ns 0.3891 24024 B
ArrayTryFindExisting 24.80 ns 0,554 ns 0.0015 96 B
ArrayTryFindNonexisting (SzukajNieistniejącyTablica) 5139,58 ns 260.949 ns 0.3891 24024 B
ZnajdzIndeksTablicyIstniejący 15,92 ns 0,526 ns 0.0015 96 B
ArrayTryFindIndexNonexisting 4349,13 ns 100,750 ns 0.3891 24024 B

Po:

Metoda Znaczyć Błąd Gen0 Przydzielone
TablicaZawieraIstniejące 4.865 ns 0,3452 ns - -
TablicaZawieraNieistniejący 766.005 ns 15.2003 ns - -
ArrayIstniejeIstniejący 8.025 ns 0.1966 ns 0.0004 24 B
ArrayExistsNonexisting 834.811 ns 16.2784 ns - 24 B
TablicaSprobujZnajdzIstniejacy 16.401 ns 0,3932 ns 0.0008 48 B
ArrayTryFindNonexisting 1,140,515 ns 22.7372 ns - 24 B
ArrayTryFindIndexExisting 14.864 ns 0.3648 ns 0.0008 48 B
ArraySpróbujZnaleźćIndeksNieistniejący 990.028 ns 19.7157 ns - 24 B

Wszystkie szczegóły można znaleźć tutaj: Historie deweloperów F#: Jak w końcu naprawiliśmy problem z wydajnością trwający 9 lat.

Udostępnianie pól dla związków dyskryminowanych struktur

Jeśli pola w wielu przypadkach unii dyskryminowanej struktury mają taką samą nazwę i typ, mogą one współdzielić tę samą lokalizację pamięci, zmniejszając ilość zużywanej pamięci przez strukturę. (Wcześniej te same nazwy pól nie były dozwolone, więc nie ma problemów ze zgodnością binarną).

Na przykład:

[<Struct>]
type MyStructDU =
    | Length of int64<meter>
    | Time of int64<second>
    | Temperature of int64<kelvin>
    | Pressure of int64<pascal>
    | Abbrev of TypeAbbreviationForInt64
    | JustPlain of int64
    | MyUnit of int64<MyUnit>

sizeof<MyStructDU> // 16 bytes

Porównanie z poprzednimi wersjami (gdzie trzeba było używać unikatowych nazw pól):

[<Struct>]
type MyStructDU =
    | Length of length: int64<meter>
    | Time of time: int64<second>
    | Temperature of temperature: int64<kelvin>
    | Pressure of pressure: int64<pascal>
    | Abbrev of abbrev: TypeAbbreviationForInt64
    | JustPlain of plain: int64
    | MyUnit of myUnit: int64<MyUnit>

sizeof<MyStructDU> // 60 bytes

Optymalizacje zakresu całkowitego

Kompilator generuje teraz zoptymalizowany kod dla większej liczby wystąpień start..finish i wyrażeń start..step..finish. Wcześniej były one zoptymalizowane tylko wtedy, gdy typ był int/int32, a krok miał stałą wartość 1 lub -1. Inne typy całkowite i inne wartości kroków używały nieefektywnej implementacji opartej na IEnumerable. Teraz wszystkie te elementy są zoptymalizowane.

Prowadzi to do od 1,25× do 8× przyspieszenia w pętlach.

for … in start..finish do …

Wyrażenia listy/tablicy:

[start..step..finish]

i zrozumienie:

[for n in start..finish -> f n]

Zoptymalizowane for x in xs -> … na listach i w wyrażeniach tablicowych

W związku z tym, wyrażenia z for x in xs -> … zostały zoptymalizowane pod kątem list i tablic, z godnymi uwagi ulepszeniami szczególnie w przypadku tablic, z przyspieszeniem nawet do 10 razy oraz zmniejszeniem rozmiaru alokacji z 1/3 do 1/4.

Ulepszenia narzędzi

Bufory na żywo w Visual Studio

Ta wcześniej dobrowolna funkcja została dokładnie przetestowana i jest teraz domyślnie włączona. Kompilator w tle zasilający IDE działa teraz z buforami plików na żywo, co oznacza, że nie trzeba zapisywać plików na dysku, aby wprowadzone zmiany zostały zastosowane. Wcześniej może to spowodować nieoczekiwane zachowanie. (Najbardziej notorycznie podczas próby zmiany nazwy symbolu obecnego w pliku, który został edytowany, ale nie został zapisany).

Analizator i poprawka kodu do usuwania niepotrzebnych nawiasów

Czasami dodatkowe nawiasy są używane do jasności, ale czasami są to tylko szumy. W tym ostatnim przypadku uzyskasz poprawkę kodu w programie Visual Studio, aby je usunąć.

Na przykład:

let f (x) = x // -> let f x = x
let _ = (2 * 2) + 3 // -> let _ = 2 * 2 + 3

Obsługa niestandardowego wizualizatora dla F# w Visual Studio

Wizualizator debugera w programie Visual Studio współpracuje teraz z projektami języka F#.

wizualizator debugowania

Etykietki narzędzi podpisów wyświetlane w połowie potoku

Wcześniej pomoc dotycząca sygnatury nie była oferowana w sytuacjach takich jak poniższa, gdzie funkcja w środku potoku miała już złożony parametryzowany przez currying parametr (na przykład lambda) zastosowany do niej. Teraz podpowiedź podpisu jest wyświetlana dla następnego parametru (state):

etykietka narzędzia