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 test
zostaną 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
, Array
i 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
, Choices
i 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!
zand!
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#.
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
):