Sdílet prostřednictvím


Novinky v F# 9

F# 9 představuje řadu vylepšení, která usnadňují bezpečnější, odolnější a výkon vašich programů. Tento článek popisuje hlavní změny v F# 9, vyvinutém v open source úložišti kódu F#.

F# 9 je k dispozici v .NET 9. Nejnovější sadu .NET SDK si můžete stáhnout ze stránky pro stahování .NET.

Odkazové typy s možnou hodnotou null

I když je jazyk F# navržený tak, aby se vyhnul null, může se tento jev objevit při používání knihoven .NET napsaných v jazyce C#. Jazyk F# teď nabízí bezpečný způsob, jak pracovat s odkazovými typy, které můžou mít null jako platnou hodnotu.

Další podrobnosti najdete v příspěvku na blogu o nulovatelných odkazových typech ve F# 9.

Tady je několik příkladů:

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

Diskriminované sjednocení .Is* vlastností

Diskriminovaná sjednocení nyní mají automaticky generované vlastnosti pro každou variantu, což umožňuje ověřit, zda hodnota odpovídá konkrétní variantě. Například pro následující typ:

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

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

Dříve jste museli napsat něco jako:

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

Teď můžete místo toho psát:

let canSendEmailTo person =
    person.contact.IsEmail

Částečné aktivní vzory můžou místo unit option vracet bool

Dříve částečně aktivní vzory vracely Some () pro označení shody a None jinak. Nyní mohou také vrátit bool.

Například aktivní vzor pro následující:

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

Byl dříve napsán jako:

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

Teď můžete místo toho psát:

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

Při zadání argumentů upřednostněte metody rozšíření k vnitřním vlastnostem.

Pro sladění se vzorem, který je vidět v některých knihovnách .NET, kde jsou metody rozšíření definované se stejnými názvy jako vnitřní vlastnosti typu, jazyk F# teď tyto metody rozšíření vyřeší místo selhání kontroly typu.

Příklad:

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

Výrazy výpočtů s prázdným obsahem

Jazyk F# teď podporuje výpočetní výrazy prázdné .

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

Zápis prázdného výpočetního výrazu způsobí volání metody Zero tvůrce výpočetních výrazů.

Jedná se o přirozenější syntaxi v porovnání s dříve dostupnou builder { () }.

Direktivy hash mohou přijímat neřetězcové argumenty.

Direktivy hash pro kompilátor dříve umožňovaly pouze řetězcové argumenty předané v uvozovkách. Teď můžou použít libovolný typ argumentu.

Dříve jste měli:

#nowarn "0070"
#time "on"

Teď můžete napsat:

#nowarn 0070
#time on

To se také váže k dalším dvěma změnám.

Rozšířená direktiva #help ve fsi pro zobrazení dokumentace v REPL

Direktiva #help v jazyce F# Interactive teď zobrazuje dokumentaci pro daný objekt nebo funkci, kterou teď můžete předat bez uvozovek.

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

Podrobnosti naleznete v blogovém příspěvku F# Interactive o vylepšení #help, viz .

Povolit #nowarn, aby podporoval předponu FS u kódů chyb k zakázání upozornění

Dříve, když jste chtěli zakázat upozornění a napsali #nowarn "FS0057", dostanete Invalid warning number 'FS0057'. I když je číslo upozornění správné, nemělo by mít předponu FS.

Teď nebudete muset trávit čas tím, že zjistíte, že čísla upozornění jsou přijata i s předponou.

Všechny tyto funkce teď budou fungovat:

#nowarn 57
#nowarn 0057
#nowarn FS0057

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

V celém projektu je vhodné použít stejný styl.

Upozornění na atribut TailCall u nerekurzivních funkcí nebo hodnot vázaných na let

Jazyk F# teď vygeneruje upozornění, když někam umístíte atribut [<TailCall>], který nepatří. I když nemá žádný vliv na to, co kód dělá, může zmást někoho, kdo ho čte.

Například tato použití teď vygenerují upozornění:

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

[<TailCall>]
let someX = 23

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

Vynucení cílů atributů

Kompilátor teď správně vynucuje AttributeTargets pro hodnoty let, funkce, deklarace sjednocovacího případu, implicitní konstruktory, struktury a třídy. To může zabránit některým obtížně postřehnutelným chybám, jako je například zapomenutí přidat argument do unit testu Xunit.

Dříve jste mohli napsat:

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

Když jste testy spustili s dotnet test, prošly by. Vzhledem k tomu, že testovací funkce není ve skutečnosti funkcí, byla ignorována spouštěčem testů.

Nyní, se správným vynucováním atributů, získáte error FS0842: This attribute is not valid for use on this language element.

Aktualizace standardní knihovny (FSharp.Core)

Náhodné funkce pro kolekce

Moduly List, Arraya Seq mají nové funkce pro náhodné vzorkování a náhodné prohazování. To usnadňuje použití jazyka F# pro běžné datové vědy, strojové učení, vývoj her a další scénáře, ve kterých je potřeba náhodnost.

Všechny funkce mají následující varianty:

  • Ten, který používá implicitní, vláknově bezpečnou sdílenou instanci Random
  • Jedna, která jako argument přebírá instanci Random
  • Který přijímá vlastní funkci randomizer, která by měla vrátit hodnotu typu float větší nebo rovnou 0,0 a menší než 1,0

K dispozici jsou čtyři funkce (každý se třemi variantami): Shuffle, Choice, Choicesa Sample.

Zamíchat

Funkce Shuffle vrátí novou kolekci stejného typu a velikosti s každou položkou v náhodně smíšené pozici. Šance, že skončí na libovolné pozici, je rovnoměrně vážená na délce kolekce.

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

U polí existují také InPlace varianty, které položky v existujícím poli prohazují místo vytvoření nové.

Volba

Funkce Choice vrací jeden náhodný prvek z dané kolekce. Náhodná volba je rovnoměrně vážená podle velikosti kolekce.

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

Možnosti

Funkce Choices vyberou z vstupní kolekce v náhodném pořadí N elementy, což umožňuje výběr prvků více než jednou.

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

Ukázka

Funkce Sample vyberou Z vstupní kolekce v náhodném pořadí N prvky, aniž by bylo možné vybrat více než jednou prvky. N nemůže být větší než délka kolekce.

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

Úplný seznam funkcí a jejich variant najdete v dokumentu (RFC #1135).

Konstruktor bez parametrů pro CustomOperationAttribute

Tento konstruktor usnadňuje vytvoření vlastní operace pro tvůrce výpočetních výrazů. Místo explicitního pojmenování používá název metody (ve většině případů název odpovídá názvu metody).

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

Podpora kolekčních výrazů jazyka C# pro seznamy a sady jazyka F#

Při použití seznamů a sad jazyka F# z jazyka C# teď můžete použít výrazy kolekce k jejich inicializaci.

Namísto:

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

Teď můžete napsat:

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

Výrazy kolekce usnadňují používání neměnných kolekcí jazyka F# z jazyka C#. Kolekce F# můžete chtít použít v případě, že potřebujete jejich strukturální rovnost, což System.Collections.Immutable kolekce nemají.

Vylepšení produktivity vývojářů

Obnovení analyzátoru

Neustále dochází ke zlepšování obnovy analyzátoru, což znamená, že nástroje (například zvýrazňování syntaxe) stále fungují s kódem, když jste uprostřed jeho úprav a nemusí být syntakticky správný.

Analyzátor se například obnoví u nedokončených vzorů as, objektových výrazů, deklarací případů výčtového typu, deklarací záznamů, složitých vzorů primárního konstruktoru, nevyřešených dlouhých identifikátorů, prázdných klauzulí shody, chybějících polí případů sjednocení a chybějících typů polí případů sjednocení.

Diagnostika

Diagnostika nebo pochopení toho, co se kompilátoru vašemu kódu nelíbí, jsou důležitou součástí uživatelského prostředí s jazykem F#. V jazyce F# 9 existuje řada nových nebo vylepšených diagnostických zpráv nebo přesnějších diagnostických lokalizací.

Patří mezi ně:

  • Nejednoznačná metoda override ve výrazu objektu
  • Abstraktní členy při použití v ne abstraktních třídách
  • Vlastnost, která má stejný název jako diskriminovaný případ sjednocení
  • Neshoda počtu argumentů aktivního vzoru
  • Struktury s duplicitními poli
  • Použití use! s and! ve výpočetních výrazech

Pro třídy s více než 65 520 metodami vygenerovaných ILexistuje také nová chyba v době kompilace . Tyto třídy nejsou načitatelné CLR a způsobují chybu během běhu. (Nebudete vytvářet tolik metod, ale existují případy s vygenerovaným kódem.)

Reálná viditelnost

Existuje zvláštnost v tom, jak F# generuje sestavení, což vede k tomu, že soukromí členové jsou zapsáni do IL jako interní členy. To umožňuje nevhodný přístup k soukromým členům z projektů mimo F#, které mají přístup k projektu F# prostřednictvím InternalsVisibleTo.

Nyní je k dispozici volitelná oprava tohoto chování prostřednictvím kompilačního příznaku --realsig+. Vyzkoušejte ho ve svém řešení, abyste zjistili, jestli některý z vašich projektů závisí na tomto chování. Můžete ho přidat do .fsproj souborů takto:

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

Vylepšení výkonu

Optimalizované kontroly rovnosti

Kontroly rovnosti jsou nyní rychlejší a přidělují méně paměti.

Například:

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

Výsledky srovnávacích testů pro dotčené maticové funkce použité u struktury se 2 členy

Před:

Metoda Znamenat Chyba Gen0 Přidělený
PoleObsahujeExistující 15,48 ns 0,398 ns 0.0008 48 B
PoleObsahujeNeexistující 5 190,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
PoleZkusNajítNeexistující 5 139,58 ns 260,949 ns 0.3891 24024 B
PokusNajítIndexExistující 15,92 ns 0,526 ns 0.0015 96 B
ArrayTryNajítIndexNenalezený 4 349,13 ns 100,750 ns 0.3891 24024 B

Po:

Metoda Znamenat Chyba Gen0 Přidělený
PoleObsahujeExistující 4,865 ns 0,3452 ns - -
PoleObsahujeNeexistující 766.005 ns 15.2003 ns - -
ArrayExistsExisting 8.025 ns 0.1966 ns 0.0004 24 B
ArrayExistsNonexisting 834.811 ns 16.2784 ns - 24 B
ArrayTryFindExisting 16.401 ns 0,3932 ns 0.0008 48 B
ArrayTryFindNonexisting 1 140,515 ns 22.7372 ns - 24 B
PoleZkusNajítIndexStávající 14.864 ns 0.3648 ns 0.0008 48 B
ArrayTryFindIndexNonexisting (Pokuste se najít index neexistujícího prvku v poli) 990.028 ns 19.7157 ns - 24 B

Všechny podrobnosti si můžete přečíst tady: příběhy vývojářů F#: Jak jsme konečně opravili 9letý problém s výkonem.

Sdílení polí pro diskriminované sjednocení struktur

Pokud pole ve více případech diskriminované struktury mají stejný název a typ, můžou sdílet stejné umístění paměti, což snižuje nároky na paměť struktury. (Dříve nebyly povoleny stejné názvy polí, takže nedochází k žádným problémům s binární kompatibilitou.)

Například:

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

Porovnání s předchozími veriony (kde jste museli používat jedinečné názvy polí):

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

Optimalizace integrálních rozsahů

Kompilátor teď generuje optimalizovaný kód pro více instancí start..finish a výrazů start..step..finish. Dříve byly optimalizovány pouze v případě, že byl typ int/int32 a krok byl konstantní 1 nebo -1. Jiné celočíselné typy a různé hodnoty kroku používaly neefektivní implementaci založenou na IEnumerable. Teď jsou všechny optimalizované.

To vede ke zrychlení smyček v rozsahu od 1,25× až po 8×.

for … in start..finish do …

Výrazy seznamu a pole:

[start..step..finish]

a porozumění:

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

Optimalizované for x in xs -> … v porozumění seznamům a polím

Pokud jde o související poznámku, výraz for x in xs -> … byl optimalizován pro seznamy a pole, s pozoruhodnými vylepšeními zejména pro pole, přičemž došlo ke zvýšení rychlosti až o 10× a snížení velikosti přidělené paměti na 1/3 až 1/4.

Vylepšení nástrojů

Živé vyrovnávací paměti v sadě Visual Studio

Tato dříve volitelná funkce byla důkladně otestována a nyní je automaticky zapnutá. Kompilátor na pozadí, který používá integrované vývojové prostředí (IDE), nyní funguje se zpracováním paměti souboru v reálném čase, což znamená, že není třeba ukládat soubory na disk, abyste uplatnili změny. Dříve to mohlo způsobit neočekávané chování. (Nejvíce proslulé při pokusu o přejmenování symbolu v souboru, který byl upraven, ale nebyl uložen.)

Analyzátor a oprava kódu pro odebrání nepotřebných závorek

Někdy se závorky používají k přehlednosti, ale někdy jsou to jen šum. Ve druhém případě nyní získáte opravu kódu v aplikaci Visual Studio, abyste je odebrali.

Například:

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

Podpora vlastního vizualizéru pro jazyk F# v sadě Visual Studio

Vizualizér ladicího programu v sadě Visual Studio teď funguje s projekty jazyka F#.

vizualizér ladění

Popisy podpisů zobrazené uprostřed potrubí

Dříve se nápověda k signatuře nenabízela v situacích, jako je například následující: uprostřed pipeline měla funkce již použitý složitý parametr curried (například lambda). Nyní se zobrazí tooltip podpisu pro další parametr (state):

nápověda