F#-kódolási konvenciók

A következő konvenciók a nagy F#-kódbázisok használata során szerzett tapasztalatokból vannak kialakítva. A jó F#-kód öt alapelve az egyes javaslatok alapja. Ezek az F#-összetevők tervezési irányelveihez kapcsolódnak, de minden F#-kódra alkalmazhatók, nem csak az összetevőkre, például a kódtárakra.

Kód rendszerezése

Az F# a kód rendszerezésének két elsődleges módját tartalmazza: a modulokat és a névtereket. Ezek hasonlóak, de a következő különbségek vannak:

  • A névterek .NET-névterekként vannak lefordítva. A modulok statikus osztályokként vannak lefordítva.
  • A névterek mindig legfelső szintűek. A modulok legfelső szintűek és más modulokba ágyazhatók.
  • A névterek több fájlra is kiterjedhetnek. A modulok nem.
  • Modulok díszíthetők és [<RequireQualifiedAccess>][<AutoOpen>].

Az alábbi irányelvek segítségével rendszerezheti a kódot.

Névterek előnyben részesítése a legfelső szinten

A nyilvánosan használható kódok esetében a névterek előnyben részesíthetők a legfelső szintű modulok számára. Mivel .NET-névterekként vannak lefordítva, a C#-ból használhatóak anélkül, hogy ehhez folyamodnának using static.

// Recommended.
namespace MyCode

type MyClass() =
    ...

A legfelső szintű modul használata nem feltétlenül jelenik meg másként, ha csak az F#-tól van meghívva, de A C#-felhasználók számára a hívók meglepődhetnek, ha a MyCode modulnak megfelelőnek MyClass kell lenniük, ha nem ismerik az adott using static C# szerkezetet.

// Will be seen as a static class outside F#
module MyCode

type MyClass() =
    ...

Óvatosan alkalmazza [<AutoOpen>]

A [<AutoOpen>] szerkezet szennyezi a hívók számára elérhető hatókört, és a válasz arra, hogy honnan származik valami, a "varázslat". Ez nem jó dolog. A szabály alól kivételt képez maga az F# Core-kódtár (bár ez a tény egy kicsit ellentmondásos).

Ez azonban kényelmes, ha rendelkezik a nyilvános API-k segédfunkcióival, amelyeket az adott nyilvános API-tól elkülönítve szeretne rendszerezni.

module MyAPI =
    [<AutoOpen>]
    module private Helpers =
        let helper1 x y z =
            ...

    let myFunction1 x =
        let y = ...
        let z = ...

        helper1 x y z

Ez lehetővé teszi, hogy tisztán elkülönítse a megvalósítás részleteit egy függvény nyilvános API-jától anélkül, hogy minden híváskor teljes mértékben ki kellene minősítenie egy segítőt.

Emellett a névtér szintjén a kiterjesztési metódusok és kifejezésszerkesztők felfedése jól kifejezhető a következővel [<AutoOpen>]: .

Használjon [<RequireQualifiedAccess>] minden olyan esetet, amikor a nevek ütközhetnek, vagy ha úgy érzi, hogy segít az olvashatóságban

Az [<RequireQualifiedAccess>] attribútum modulhoz való hozzáadása azt jelzi, hogy a modul nem nyitható meg, és a modul elemeire mutató hivatkozások explicit minősített hozzáférést igényelnek. A modul például ezt az Microsoft.FSharp.Collections.List attribútumot tartalmazza.

Ez akkor hasznos, ha a modul függvényei és értékei olyan neveket tartalmaznak, amelyek valószínűleg ütköznek más modulok neveivel. A minősített hozzáférés megkövetelése jelentősen növelheti a hosszú távú karbantarthatóságot és a tárak fejlődési képességét.

[<RequireQualifiedAccess>]
module StringTokenization =
    let parse s = ...

...

let s = getAString()
let parsed = StringTokenization.parse s // Must qualify to use 'parse'

Utasítások topológiai rendezése open

Az F#-ban a deklarációk sorrendje számít, beleértve az állításokat open is (és open typecsak a távolabbiakra hivatkozva open ). Ez ellentétben áll a C#-jal, ahol az adott fájlban lévő utasítások hatása using és using static sorrendje független.

Az F#-ban a hatókörbe megnyitott elemek árnyékot adhatnak a már meglévőknek. Ez azt jelenti, hogy az utasítások átrendezése open megváltoztathatja a kód jelentését. Ennek eredményeképpen az összes open utasítás tetszőleges rendezése (például alfanumerikusan) nem ajánlott, ne pedig eltérő viselkedést generáljon, amelyet elvárhat.

Ehelyett azt javasoljuk, hogy topológiai sorrendben rendezze őket, azaz rendezze az open utasításokat a rendszer rétegeinek meghatározásának sorrendjében. A különböző topológiai rétegeken belüli alfanumerikus rendezés is megfontolható.

Példaként itt látható az F# fordítószolgáltatás nyilvános API-fájljának topológiai rendezése:

namespace Microsoft.FSharp.Compiler.SourceCodeServices

open System
open System.Collections.Generic
open System.Collections.Concurrent
open System.Diagnostics
open System.IO
open System.Reflection
open System.Text

open FSharp.Compiler
open FSharp.Compiler.AbstractIL
open FSharp.Compiler.AbstractIL.Diagnostics
open FSharp.Compiler.AbstractIL.IL
open FSharp.Compiler.AbstractIL.ILBinaryReader
open FSharp.Compiler.AbstractIL.Internal
open FSharp.Compiler.AbstractIL.Internal.Library

open FSharp.Compiler.AccessibilityLogic
open FSharp.Compiler.Ast
open FSharp.Compiler.CompileOps
open FSharp.Compiler.CompileOptions
open FSharp.Compiler.Driver

open Internal.Utilities
open Internal.Utilities.Collections

A vonaltörések elválasztják a topológiai rétegeket, és az egyes rétegek alfanumerikusan vannak rendezve. Ez tisztán rendszerezi a kódot az értékek véletlen árnyékolása nélkül.

Osztályok használata mellékhatásokat tartalmazó értékekhez

Az értékek inicializálásának számos esetben lehetnek mellékhatásai, például egy környezet adatbázisba vagy más távoli erőforrásba való példányosítása. Csábító, hogy inicializálja az ilyen dolgokat egy modulban, és használja azt a következő függvényekben:

// Not recommended, side-effect at static initialization
module MyApi =
    let dep1 = File.ReadAllText "/Users/<name>/connectionstring.txt"
    let dep2 = Environment.GetEnvironmentVariable "DEP_2"

    let private r = Random()
    let dep3() = r.Next() // Problematic if multiple threads use this

    let function1 arg = doStuffWith dep1 dep2 dep3 arg
    let function2 arg = doStuffWith dep1 dep2 dep3 arg

Ez több okból is gyakran problémás:

Először az alkalmazáskonfiguráció le lesz küldve a kódbázisba a következővel dep1 : és dep2. Ez a nagyobb kódbázisokban nehezen tartható fenn.

Másodszor, a statikusan inicializált adatok nem tartalmazhatnak olyan értékeket, amelyek nem szálbiztosak, ha az összetevő maga is több szálat fog használni. Ezt egyértelműen megsérti a dep3.

Végül a modul inicializálása a teljes fordítási egység statikus konstruktorává alakul. Ha hiba történik az adott modul let-bound értékének inicializálásában, az az alkalmazás teljes élettartama során gyorsítótárazottként TypeInitializationException jelenik meg. Ezt nehéz lehet diagnosztizálni. Általában van egy belső kivétel, amelyről megpróbálhat érvelni, de ha nincs, akkor nem lehet megmondani, hogy mi a kiváltó ok.

Ehelyett egyszerűen használjon egy egyszerű osztályt a függőségek tárolásához:

type MyParametricApi(dep1, dep2, dep3) =
    member _.Function1 arg1 = doStuffWith dep1 dep2 dep3 arg1
    member _.Function2 arg2 = doStuffWith dep1 dep2 dep3 arg2

Ez lehetővé teszi a következőket:

  1. Bármely függő állapot leküldése az API-n kívülre.
  2. A konfiguráció mostantól az API-n kívül is elvégezhető.
  3. A függő értékek inicializálási hibái valószínűleg nem jelennek meg TypeInitializationException.
  4. Az API-t most már könnyebb tesztelni.

Hibakezelés

A nagy rendszerek hibakezelése összetett és árnyalt törekvés, és nincsenek ezüstjelek annak biztosításában, hogy a rendszerek hibatűrőek és jól viselkedjenek. Az alábbi irányelveknek útmutatást kell nyújtaniuk e nehéz terület navigálásához.

Hibaeseteket és érvénytelen állapotokat jelölhet a tartományhoz tartozó típusokban

A diszkriminált unionok esetén az F# lehetővé teszi, hogy hibás programállapotokat jelöljön a típusrendszerben. Példa:

type MoneyWithdrawalResult =
    | Success of amount:decimal
    | InsufficientFunds of balance:decimal
    | CardExpired of DateTime
    | UndisclosedFailure

Ebben az esetben három ismert módszer létezik arra, hogy a bankszámláról történő pénzfelvétel meghiúsuljon. Az egyes hibaeseteket a rendszer a típusban jeleníti meg, így a program egész területén biztonságosan kezelhető.

let handleWithdrawal amount =
    let w = withdrawMoney amount
    match w with
    | Success am -> printfn $"Successfully withdrew %f{am}"
    | InsufficientFunds balance -> printfn $"Failed: balance is %f{balance}"
    | CardExpired expiredDate -> printfn $"Failed: card expired on {expiredDate}"
    | UndisclosedFailure -> printfn "Failed: unknown"

Általánosságban elmondható, hogy ha modellezheti azokat a különböző módszereket, amelyekkel valami meghiúsulhat a tartományban, akkor a hibakezelő kód már nem olyanként lesz kezelve, amellyel a szokásos programfolyamat mellett foglalkoznia kell. Ez egyszerűen a normál programfolyamat része, és nem tekinthető kivételesnek. Ennek két elsődleges előnye van:

  1. A tartomány idővel történő változásaival könnyebben karbantartható.
  2. A hibaeseteket könnyebb egyesíteni.

Kivételek használata, ha a hibák nem jeleníthetők meg típusokkal

Nem minden hiba jeleníthető meg egy problémás tartományban. Az ilyen típusú hibák kivételes jellegűek, ezért képesek kivételeket emelni és elkapni az F#-ban.

Először is ajánlott elolvasni a kivételtervezési irányelveket. Ezek az F#-ra is érvényesek.

A kivételek emelése céljából az F#-ban elérhető fő szerkezeteket a következő sorrendben kell figyelembe venni:

Függvény Syntax Cél
nullArg nullArg "argumentName" A megadott argumentumnévvel emel ki egy System.ArgumentNullException értéket.
invalidArg invalidArg "argumentName" "message" System.ArgumentException Egy megadott argumentumnévvel és üzenettel rendelkező értéket ad meg.
invalidOp invalidOp "message" A megadott üzenettel együtt ad meg egy System.InvalidOperationException üzenetet.
raise raise (ExceptionType("message")) Általános célú mechanizmus a kivételek kivetésére.
failwith failwith "message" A megadott üzenettel együtt ad meg egy System.Exception üzenetet.
failwithf failwithf "format string" argForFormatString A formázási sztring System.Exception és a bemenetek által meghatározott üzenetet jelenít meg.

Használja nullArg, invalidArgés invalidOp a dobás ArgumentNullExceptionmechanizmusaként, ArgumentExceptionés InvalidOperationException ha szükséges.

A failwith függvényeket általában failwithf kerülni kell, mert az alaptípust Exception emelik ki, nem pedig egy adott kivételt. A kivételtervezési irányelveknek megfelelően konkrétabb kivételeket szeretne létrehozni, amikor csak lehet.

Kivételkezelési szintaxis használata

Az F# a szintaxison keresztül támogatja a try...with kivételmintákat:

try
    tryGetFileContents()
with
| :? System.IO.FileNotFoundException as e -> // Do something with it here
| :? System.Security.SecurityException as e -> // Do something with it here

A mintaegyeztetéssel kapcsolatos kivételekkel szemben végrehajtandó funkciók egyeztetése kissé bonyolult lehet, ha tisztán szeretné tartani a kódot. Ennek egyik módja az aktív minták használata a hibaeseteket körülölelő funkciók csoportosítására egy kivétellel. Előfordulhat például, hogy olyan API-t használ, amely kivétel esetén értékes információkat tartalmaz a kivétel metaadataiban. Az aktív mintában rögzített kivétel törzsében lévő hasznos érték kibontása és az érték visszaadása bizonyos helyzetekben hasznos lehet.

Ne használjon hibakezelést a kivételek cseréjéhez

A kivételeket gyakran tabunak tekintik a tiszta funkcionális paradigmában. A kivételek valóban sértik a tisztaságot, ezért nyugodtan tekintheti őket nem teljesen funkcionálisan tisztanak. Ez azonban figyelmen kívül hagyja a kód futtatásának helyét, és hogy futásidejű hibák léphetnek fel. Általában írjon kódot arra a feltételezésre, hogy a legtöbb dolog nem tiszta vagy teljes, hogy minimalizálja a kellemetlen meglepetéseket (hasonló a C#-ban való ürességhez catch vagy a verem nyomkövetésének helytelen kezeléséhez, az információk elvetéséhez).

Fontos figyelembe venni a kivételek alábbi alapvető erősségeit/szempontjait a .NET-futtatókörnyezetben és a nyelvközi ökoszisztémában való relevanciájuk és megfelelőségük szempontjából:

  • Részletes diagnosztikai információkat tartalmaznak, ami hasznos lehet egy probléma hibakeresése során.
  • Ezeket jól ismerik a futtatókörnyezet és más .NET-nyelvek.
  • Csökkenthetik a jelentős kazánlemezt olyan kódokkal összehasonlítva, amelyek elkerülik a kivételek elkerülését azáltal, hogy a szemantikák bizonyos részhalmazát ad-hoc alapon implementálják.

Ez a harmadik pont kritikus fontosságú. Nemtriviális összetett műveletek esetén a kivételek használatának elmulasztása az alábbihoz hasonló struktúrák kezelését eredményezheti:

Result<Result<MyType, string>, string list>

Ami könnyen törékeny kódhoz vezethet, például a "sztringbegépelt" hibákhoz hasonló mintamegfeleltetéshez:

let result = doStuff()
match result with
| Ok r -> ...
| Error e ->
    if e.Contains "Error string 1" then ...
    elif e.Contains "Error string 2" then ...
    else ... // Who knows?

Emellett csábító lehet lenyelni minden kivételt az "egyszerű" függvény vágyában, amely "szebb" típust ad vissza:

// Can be problematic due to discarding the cause of error.
let tryReadAllText (path : string) =
    try System.IO.File.ReadAllText path |> Some
    with _ -> None

tryReadAllText Sajnos számos kivételt vethet ki a fájlrendszeren előforduló számos dolog alapján, és ez a kód elvet minden információt arról, hogy mi lehet a hiba a környezetben. Ha ezt a kódot eredménytípusra cseréli, akkor a "sztringen beírt" hibaüzenet elemzéséhez visszatér:

// Problematic, callers only have a string to figure the cause of error.
let tryReadAllText (path : string) =
    try System.IO.File.ReadAllText path |> Ok
    with e -> Error e.Message

let r = tryReadAllText "path-to-file"
match r with
| Ok text -> ...
| Error e ->
    if e.Contains "uh oh, here we go again..." then ...
    else ...

És maga a kivételobjektum a konstruktorban Error való elhelyezése csak arra kényszeríti, hogy megfelelően kezelje a kivételtípust a hívási helyen, nem pedig a függvényben. Ezzel gyakorlatilag ellenőrzött kivételeket hoz létre, amelyek hírhedten nem használhatók az API hívójaként való kezeléshez.

A fenti példák jó alternatívája az adott kivételek elfogása és a kivétel kontextusában értelmezhető érték visszaadása. Ha a függvényt az tryReadAllText alábbiak szerint módosítja, None több jelentése van:

let tryReadAllTextIfPresent (path : string) =
    try System.IO.File.ReadAllText path |> Some
    with :? FileNotFoundException -> None

Ahelyett, hogy mindenhatóként működjön, ez a függvény most már megfelelően kezeli az esetet, amikor egy fájl nem található, és ezt a jelentést egy visszatéréshez rendeli. Ez a visszatérési érték megfeleltethető erre a hibaesetre, miközben nem vet el semmilyen környezeti információt, és nem kényszeríti a hívókat egy olyan eset kezelésére, amely nem feltétlenül releváns a kód azon pontján.

Az olyan Result<'Success, 'Error> alapműveletekhez megfelelő típusok, amelyek nem beágyazottak, és az F# választható típusok tökéletesek arra, ha valami visszaadhat valamit vagy semmit. Ezek azonban nem helyettesítik a kivételeket, és nem használhatók a kivételek helyettesítésére tett kísérletekben. Ehelyett célszerű megfontoltan alkalmazni őket a kivétel- és hibakezelési szabályzat bizonyos aspektusainak célzott kezelésére.

Részleges alkalmazás- és pontmentes programozás

Az F# támogatja a részleges alkalmazásokat, és így a programozás különböző módjait pontmentes stílusban. Ez hasznos lehet a kód újrafelhasználása egy modulon belül vagy valami implementációja esetén, de ezt nem lehet nyilvánosan elérhetővé tenni. Általánosságban elmondható, hogy a pontmentes programozás önmagában nem erény, és jelentős kognitív akadályt adhat azoknak, akik nem merülnek el a stílusban.

Ne használjon részleges alkalmazást és curry-t nyilvános API-kban

A részleges alkalmazás nyilvános API-kban való használata kis kivétellel zavaró lehet a fogyasztók számára. letAz F#-kódban a -bound értékek általában értékek, nem függvényértékek. Az értékek és a függvényértékek összevonása néhány sornyi kód mentését eredményezheti a kognitív többletterhelésekért cserébe, különösen akkor, ha olyan operátorokkal kombinálva, mint a >> függvények írása.

Fontolja meg a pontmentes programozás eszközhasználati következményeit

A curried függvények nem címkézik meg az argumentumaikat. Ennek eszközhatásai vannak. Vegye figyelembe a következő két függvényt:

let func name age =
    printfn $"My name is {name} and I am %d{age} years old!"

let funcWithApplication =
    printfn "My name is %s and I am %d years old!"

Mindkettő érvényes függvény, de funcWithApplication egy curried függvény. Amikor egy szerkesztőben a típusuk fölé viszi az egérmutatót, a következő látható:

val func : name:string -> age:int -> unit

val funcWithApplication : (string -> int -> unit)

A híváswebhelyen az eszközleírások, például a Visual Studio, megadja a típus-aláírást, de mivel nincsenek definiálva nevek, a nevek nem jelennek meg. A nevek kritikus fontosságúak a jó API-tervezés szempontjából, mivel segítenek a hívóknak jobban megérteni az API mögötti jelentést. A nyilvános API-ban pontmentes kód használata megnehezítheti a hívók számára a megértést.

Ha olyan pont nélküli kóddal találkozik, mint funcWithApplication ami nyilvánosan fogyasztható, javasoljuk, hogy teljes η bővítést hajtson végre, hogy az eszközök értelmes neveket vehessenek fel az argumentumok számára.

Emellett a pontmentes kód hibakeresése kihívást jelenthet, ha nem lehetetlen. A hibakeresési eszközök a nevekhez (például kötésekhez) kötött értékekre támaszkodnak, let így a végrehajtás során félúton megvizsgálhatja a köztes értékeket. Ha a kód nem tartalmaz vizsgálandó értékeket, nincs hibakeresés. A jövőben a hibakeresési eszközök fejlődhetnek, hogy ezeket az értékeket a korábban végrehajtott útvonalak alapján szintetizálják, de nem érdemes a lehetséges hibakeresési funkciókra vonatkozó téteket fedezni.

Fontolja meg a részleges alkalmazást, mint a belső kazánlemez csökkentésének technikáját

Az előző ponttal ellentétben a részleges alkalmazás nagyszerű eszköz az alkalmazáson belüli vagy az API mélyebb belső rétegeinek csökkentésére. Hasznos lehet a bonyolultabb API-k implementálásának egységtesztelése, ahol a kazánlemezzel gyakran nehéz foglalkozni. Az alábbi kód például bemutatja, hogyan valósíthatja meg a legtöbb szimulált keretrendszert anélkül, hogy külső függőséget kellene használnia egy ilyen keretrendszerhez, és meg kell tanulnia egy kapcsolódó bespoke API-t.

Vegyük például a következő megoldástopográfiát:

MySolution.sln
|_/ImplementationLogic.fsproj
|_/ImplementationLogic.Tests.fsproj
|_/API.fsproj

ImplementationLogic.fsproj olyan kódot tehet közzé, mint például:

module Transactions =
    let doTransaction txnContext txnType balance =
        ...

type Transactor(ctx, currentBalance) =
    member _.ExecuteTransaction(txnType) =
        Transactions.doTransaction ctx txnType currentBalance
        ...

Az egységtesztelés Transactions.doTransactionImplementationLogic.Tests.fsproj egyszerű:

namespace TransactionsTestingUtil

open Transactions

module TransactionsTestable =
    let getTestableTransactionRoutine mockContext = Transactions.doTransaction mockContext

A szimulált környezeti objektumokkal részlegesen alkalmazva doTransaction meghívhatja a függvényt az összes egységtesztben anélkül, hogy minden alkalommal létre kellene készítenie egy kicsúsztatott környezetet:

module TransactionTests

open Xunit
open TransactionTypes
open TransactionsTestingUtil
open TransactionsTestingUtil.TransactionsTestable

let testableContext =
    { new ITransactionContext with
        member _.TheFirstMember() = ...
        member _.TheSecondMember() = ... }

let transactionRoutine = getTestableTransactionRoutine testableContext

[<Fact>]
let ``Test withdrawal transaction with 0.0 for balance``() =
    let expected = ...
    let actual = transactionRoutine TransactionType.Withdraw 0.0
    Assert.Equal(expected, actual)

Ne alkalmazza ezt a technikát univerzálisan a teljes kódbázisra, de ez egy jó módszer a bonyolult belső műveletek és a belső egységek tesztelésére szolgáló kazánlemez csökkentésére.

Hozzáférés-vezérlés

Az F# több lehetőséggel is rendelkezik a hozzáférés-vezérléshez, amely a .NET-futtatókörnyezetben elérhető lehetőségektől öröklődik. Ezek nem csak a típusok esetében használhatók – függvényekhez is használhatja őket.

A széles körben használt kódtárakhoz kapcsolódó ajánlott eljárások:

  • A nempublic típusok és tagok előnyben részesíthetők, amíg nem szeretné, hogy nyilvánosan felhasználhatók legyenek. Ez minimalizálja azt is, amit a fogyasztók párosítanak.
  • Törekedjen az összes segítő funkció privatemegtartására.
  • Fontolja meg a segédfüggvények privát moduljának [<AutoOpen>] használatát, ha azok számosvá válnak.

Típuskövetkeztetés és általános adatok

A típuskövetkeztetés megkímélheti önt attól, hogy sok kazántáblát írjon be. Az F#-fordító automatikus általánosítása pedig segíthet az általánosabb kód írásában, és szinte semmilyen extra erőfeszítést nem igényel. Ezek a funkciók azonban nem általánosan jóak.

  • Fontolja meg az argumentumnevek explicit típusokkal való címkézését a nyilvános API-kban, és ne támaszkodhat erre a típuskövetkeztetésre.

    Ennek az az oka, hogy az API alakját kell szabályoznia, nem pedig a fordítót. Bár a fordító nagyszerű munkát végezhet a típusok következtetésénél, az API-nak az alakja megváltozhat, ha az általa használt belső típusok módosultak. Lehet, hogy ez az, amit szeretne, de ez szinte biztosan egy kompatibilitástörő API-változást fog eredményezni, amellyel az alsóbb rétegbeli felhasználóknak foglalkozniuk kell. Ehelyett, ha explicit módon szabályozza a nyilvános API alakját, akkor szabályozhatja ezeket a kompatibilitástörő változásokat. A DDD szempontjából ez egy korrupcióellenes rétegnek tekinthető.

  • Érdemes lehet értelmes nevet adni az általános argumentumoknak.

    Hacsak nem olyan általános kódot ír, amely nem egy adott tartományra vonatkozik, egy értelmes név segíthet más programozóknak megérteni azt a tartományt, amelyben dolgoznak. Egy dokumentum-adatbázissal való interakció kontextusában elnevezett 'Document típusparaméter például egyértelműbbé teszi, hogy az általános dokumentumtípusokat a használt függvény vagy tag elfogadhatja.

  • Fontolja meg az általános típusparaméterek elnevezését a PascalCase használatával.

    Ez az általános módszer a .NET-ben történő műveletekre, ezért ajánlott a PascalCaset használni snake_case vagy camelCase helyett.

Végül pedig az automatikus általánosítás nem mindig boon az F# vagy egy nagy kódbázis új felhasználói számára. Az általános összetevők használata kognitív többletterhelést jelent. Továbbá, ha az automatikusan általánosított függvényeket nem használják különböző bemeneti típusokkal (nem is beszélve arról, hogy ezeket a függvényeket ilyenként kívánják használni), akkor nincs valódi előnye annak, hogy általánosak legyenek. Mindig gondolja át, hogy az éppen megírt kód valóban hasznos-e az általánosság szempontjából.

Teljesítmény

Fontolja meg a nagy foglalási arányú kis típusok szerkezetét

A szerkezetek (más néven értéktípusok) használata gyakran nagyobb teljesítményt eredményezhet bizonyos kódok esetében, mivel általában elkerüli az objektumok kiosztását. A struktúra azonban nem mindig "gyorsabb" gomb: ha egy struktúra adatainak mérete meghaladja a 16 bájtot, az adatok másolása gyakran több processzoridőt eredményezhet, mint egy referenciatípus használata.

Annak megállapításához, hogy használnia kell-e egy szerkezetet, vegye figyelembe a következő feltételeket:

  • Ha az adatok mérete 16 bájt vagy kisebb.
  • Ha valószínűleg sok ilyen típusú példány található a memóriában egy futó programban.

Ha az első feltétel érvényes, általában egy szerkezetet kell használnia. Ha mindkettő alkalmazható, szinte mindig használjon szerkezetet. Előfordulhatnak olyan esetek, amikor a korábbi feltételek érvényesek, de a szerkezet használata nem jobb vagy rosszabb, mint egy referenciatípus használata, de valószínűleg ritkán fordulnak elő. Fontos azonban, hogy mindig mérjük az ilyen változtatásokat, és ne feltételezve vagy intuícióval működjön.

Fontolja meg a strukturálási folyamatokat, ha kis értéktípusokat csoportosít magas foglalási arányokkal

Vegye figyelembe a következő két függvényt:

let rec runWithTuple t offset times =
    let offsetValues x y z offset =
        (x + offset, y + offset, z + offset)

    if times <= 0 then
        t
    else
        let (x, y, z) = t
        let r = offsetValues x y z offset
        runWithTuple r offset (times - 1)

let rec runWithStructTuple t offset times =
    let offsetValues x y z offset =
        struct(x + offset, y + offset, z + offset)

    if times <= 0 then
        t
    else
        let struct(x, y, z) = t
        let r = offsetValues x y z offset
        runWithStructTuple r offset (times - 1)

Ha ezeket a függvényeket egy olyan statisztikai teljesítménytesztelő eszközzel méri fel, mint a BenchmarkDotNet, azt fogja tapasztalni, hogy a runWithStructTuple strukturált tupleseket használó függvény 40%-kal gyorsabban fut, és nem foglal le memóriát.

Ezek az eredmények azonban nem mindig lesznek a saját kódjukban. Ha megjelöl egy függvényt, inlinea hivatkozási kódot használó kód további optimalizálásokat kaphat, vagy a lefoglalt kódot egyszerűen optimalizálhatja. Mindig mérnie kell az eredményeket, amikor teljesítményről van szó, és soha ne alapuljon feltételezésen vagy intuíción.

Érdemes lehet strukturálni a rekordokat, ha a típus kicsi és magas foglalási arányokkal rendelkezik

A korábban ismertetett hüvelykujjszabály az F#-rekordtípusokra is érvényes. Vegye figyelembe az őket feldolgozó alábbi adattípusokat és függvényeket:

type Point = { X: float; Y: float; Z: float }

[<Struct>]
type SPoint = { X: float; Y: float; Z: float }

let rec processPoint (p: Point) offset times =
    let inline offsetValues (p: Point) offset =
        { p with X = p.X + offset; Y = p.Y + offset; Z = p.Z + offset }

    if times <= 0 then
        p
    else
        let r = offsetValues p offset
        processPoint r offset (times - 1)

let rec processStructPoint (p: SPoint) offset times =
    let inline offsetValues (p: SPoint) offset =
        { p with X = p.X + offset; Y = p.Y + offset; Z = p.Z + offset }

    if times <= 0 then
        p
    else
        let r = offsetValues p offset
        processStructPoint r offset (times - 1)

Ez hasonló az előző rekordkódhoz, de ezúttal rekordok és beágyazott belső függvények használatával történik.

Ha ezeket a függvényeket egy olyan statisztikai teljesítménytesztelő eszközzel méri fel, mint a BenchmarkDotNet, azt fogja tapasztalni, hogy processStructPoint közel 60%-kal gyorsabban fut, és semmit sem foglal le a felügyelt halomon.

Fontolja meg a diszkriminált uniók szerkezetét, ha az adattípus kicsi, és magas a kiosztási arány

Az F# Diszkriminált Uniók teljesítményével kapcsolatos korábbi megfigyelések és nyilvántartások szintén az F# Diszkriminált Unióra vonatkozik. Tekintse meg az alábbi kódot:

    type Name = Name of string

    [<Struct>]
    type SName = SName of string

    let reverseName (Name s) =
        s.ToCharArray()
        |> Array.rev
        |> System.String
        |> Name

    let structReverseName (SName s) =
        s.ToCharArray()
        |> Array.rev
        |> System.String
        |> SName

A tartománymodellezéshez gyakran definiálunk ilyen egyedi diszkriminált uniókat. Ha ezeket a függvényeket egy statisztikai teljesítménytesztelő eszközzel, például a BenchmarkDotNettel méri össze, az structReverseName körülbelül 25%-kal gyorsabban fut, mint reverseName a kis sztringek esetében. Nagy sztringek esetén mindkettő nagyjából azonos teljesítményt nyújt. Tehát ebben az esetben mindig előnyösebb egy szerkezetet használni. Ahogy korábban említettük, mindig mérje és ne használja a feltételezéseket vagy az intuíciót.

Bár az előző példa azt mutatta, hogy a struct Diszkriminált Unió jobb teljesítményt nyújtott, gyakori, hogy nagyobb diszkriminált uniók vannak egy tartomány modellezése során. Az ilyen nagyobb adattípusok nem feltétlenül fognak megfelelően teljesíteni, ha a rajtuk végzett műveletektől függően strukturálódnak, mivel több másolás is lehetséges.

Nem módosíthatóság és mutáció

Az F#-értékek alapértelmezés szerint nem módosíthatók, így elkerülheti a hibák bizonyos osztályait (különösen az egyidejűséget és a párhuzamosságot). Bizonyos esetekben azonban a végrehajtási idő vagy a memóriafoglalás optimális (vagy akár ésszerű) hatékonyságának elérése érdekében a legjobban a helyszíni állapotmutációval lehet megvalósítani a munkát. Ez az F# kulcsszóval mutable történő jóváhagyással lehetséges.

mutable Az F# használata ellentétes lehet a funkcionális tisztasággal. Ez érthető, de a funkcionális tisztaság mindenhol ellentétes lehet a teljesítménycélokkal. A kompromisszum az, hogy beágyazza a mutációt, hogy a hívóknak nem kell törődniük azzal, hogy mi történik, amikor függvényt hívnak. Ez lehetővé teszi, hogy funkcionális felületet írjon egy mutációalapú implementáción a teljesítmény szempontjából kritikus kódhoz.

Az F# let -kötési szerkezetek lehetővé teszik a kötések beágyazását egy másikba, így a változó hatóköre mutable közel vagy a legkisebb teoritikus értéken tartható.

let data =
    [
        let mutable completed = false
        while not completed do
            logic ()
            // ...
            if someCondition then
                completed <- true   
    ]

Egyetlen kód sem fér hozzá a csak a let bound érték inicializálásához data használt mutable-hozcompleted.

Kód tördelése nem módosítható felületeken

Mivel a hivatkozási átláthatóság a cél, kritikus fontosságú olyan kódot írni, amely nem teszi elérhetővé a teljesítmény szempontjából kritikus fontosságú függvények mutable alázatát. Az alábbi kód például az Array.contains F#-magtárban implementálja a függvényt:

[<CompiledName("Contains")>]
let inline contains value (array:'T[]) =
    checkNonNull "array" array
    let mutable state = false
    let mutable i = 0
    while not state && i < array.Length do
        state <- value = array[i]
        i <- i + 1
    state

A függvény többszöri meghívása nem változtatja meg a mögöttes tömböt, és nem követeli meg, hogy a használat során bármilyen mutable állapotot tartson fenn. Ez hivatkozásilag transzparens, annak ellenére, hogy a benne lévő kódsorok szinte minden sora mutációt használ.

Fontolja meg a mutable data beágyazását osztályokba

Az előző példa egyetlen függvényt használt a műveletek beágyazásához a mutable data használatával. Ez nem mindig elegendő összetettebb adathalmazokhoz. Vegye figyelembe a következő függvénykészleteket:

open System.Collections.Generic

let addToClosureTable (key, value) (t: Dictionary<_,_>) =
    if t.ContainsKey(key) then
        t[key] <- value
    else
        t.Add(key, value)

let closureTableCount (t: Dictionary<_,_>) = t.Count

let closureTableContains (key, value) (t: Dictionary<_, HashSet<_>>) =
    match t.TryGetValue(key) with
    | (true, v) -> v.Equals(value)
    | (false, _) -> false

Ez a kód teljesít, de elérhetővé teszi a hívók által a karbantartásért felelős mutációalapú adatstruktúrát. Ez egy olyan osztályba csomagolható, amely nem rendelkezik olyan mögöttes tagokkal, amelyek módosíthatók:

open System.Collections.Generic

/// The results of computing the LALR(1) closure of an LR(0) kernel
type Closure1Table() =
    let t = Dictionary<Item0, HashSet<TerminalIndex>>()

    member _.Add(key, value) =
        if t.ContainsKey(key) then
            t[key] <- value
        else
            t.Add(key, value)

    member _.Count = t.Count

    member _.Contains(key, value) =
        match t.TryGetValue(key) with
        | (true, v) -> v.Equals(value)
        | (false, _) -> false

Closure1Table beágyazza a mögöttes mutációalapú adatstruktúrát, így nem kényszeríti a hívókat a mögöttes adatstruktúra fenntartására. Az osztályok hatékony módszert jelentenek a mutáción alapuló adatok és rutinok beágyazására anélkül, hogy a hívóknak felfedné a részleteket.

Szívesebben let mutableref

A referenciacellák az értékre mutató hivatkozást jelölik, nem pedig magát az értéket. Bár teljesítménykritikus kódhoz is használhatók, nem ajánlott. Vegyük a következő példát:

let kernels =
    let acc = ref Set.empty

    processWorkList startKernels (fun kernel ->
        if not ((!acc).Contains(kernel)) then
            acc := (!acc).Add(kernel)
        ...)

    !acc |> Seq.toList

A referenciacella használata mostantól "szennyezi" az összes további kódot, mivel el kell halasztani és újra kell hivatkozni a mögöttes adatokra. Ehelyett fontolja meg a következőt let mutable:

let kernels =
    let mutable acc = Set.empty

    processWorkList startKernels (fun kernel ->
        if not (acc.Contains(kernel)) then
            acc <- acc.Add(kernel)
        ...)

    acc |> Seq.toList

A lambda kifejezés közepén lévő egyetlen mutációs ponton kívül minden más, az érintett acc kód úgy is megteheti ezt, hogy az nem különbözik a normál letkötött megváltoztathatatlan értékek használatától. Ez megkönnyíti az idő múlásával történő módosítást.

Null értékek és alapértelmezett értékek

A null értékeket általában kerülni kell az F#-ban. Alapértelmezés szerint az F#-deklarált típusok nem támogatják a null literál használatát, és minden érték és objektum inicializálva lesz. Egyes gyakori .NET API-k azonban null értéket adnak vissza vagy fogadnak el, és néhány gyakori. A NET-deklarált típusok, például tömbök és sztringek null értékeket engedélyeznek. Az értékek előfordulása null azonban nagyon ritka az F# programozásban, és az F# használatának egyik előnye a nullhivatkozási hibák elkerülése a legtöbb esetben.

Az attribútum használatának AllowNullLiteral elkerülése

Alapértelmezés szerint az F#-deklarált típusok nem támogatják a null literál használatát. Ennek engedélyezéséhez manuálisan megjegyzéseket fűzhet az F#-típusokhoz AllowNullLiteral . Azonban szinte mindig jobb elkerülni ezt.

Az attribútum használatának Unchecked.defaultof<_> elkerülése

Az F#-típushoz létrehozhat egy null vagy nulla inicializált értéket a használatával Unchecked.defaultof<_>. Ez hasznos lehet bizonyos adatstruktúrák tárolójának inicializálásakor, vagy valamilyen nagy teljesítményű kódolási mintában vagy az együttműködésben. Ennek a szerkezetnek a használatát azonban el kell kerülni.

Az attribútum használatának DefaultValue elkerülése

Alapértelmezés szerint az F# rekordokat és objektumokat megfelelően kell inicializálni az építéskor. Az DefaultValue attribútum használható objektumok egyes mezőinek null kitöltésére egy vagy nulla inicializált értékkel. Erre a szerkezetre ritkán van szükség, és a használatát el kell kerülni.

Ha null értékű bemeneteket keres, az első lehetőségnél kivételeket emelhet ki

Új F#-kód írásakor a gyakorlatban nem kell null bemeneteket keresni, hacsak nem számít arra, hogy a kódot C# vagy más .NET-nyelvekből fogják használni.

Ha úgy dönt, hogy null értékű bemeneteket ad hozzá, végezze el az ellenőrzéseket az első lehetőségnél, és tegyen kivételt. Példa:

let inline checkNonNull argName arg =
    if isNull arg then
        nullArg argName

module Array =
    let contains value (array:'T[]) =
        checkNonNull "array" array
        let mutable result = false
        let mutable i = 0
        while not state && i < array.Length do
            result <- value = array[i]
            i <- i + 1
        result

Régi okokból az FSharp.Core egyes sztringfüggvényei továbbra is üres sztringekként kezelik a null értékeket, és nem hiúsulnak meg null argumentumokon. Ezt azonban ne tekintse útmutatásnak, és ne használjon olyan kódolási mintákat, amelyek bármilyen szemantikai jelentést "nullnak" tulajdonítanak.

Objektumprogramozás

Az F# teljes mértékben támogatja az objektumokat és az objektumorientált (OO) fogalmakat. Bár számos OO-fogalom hatékony és hasznos, nem mindegyik ideális a használatra. Az alábbi listák útmutatást nyújtanak az OO-funkciók magas szintű kategóriáihoz.

Ezeket a funkciókat számos esetben érdemes lehet használni:

  • Pont jelölése (x.Length)
  • Példánytagok
  • Implicit konstruktorok
  • Statikus tagok
  • Indexelő jelölése (arr[x]) tulajdonság Item definiálásával
  • Szeletelő jelölés (arr[x..y], arr[x..], arr[..y]), tagok definiálásával GetSlice
  • Névvel ellátott és nem kötelező argumentumok
  • Interfészek és felületi implementációk

Először ne nyúljon ezekhez a funkciókhoz, de célszerűen alkalmazza őket, ha kényelmesen meg tudják oldani a problémát:

  • Metódus túlterhelése
  • Beágyazott mutable adatok
  • Típusok operátorai
  • Automatikus tulajdonságok
  • Implementálás IDisposable és IEnumerable
  • Típuskiterjesztések
  • esemény
  • Struktúrák
  • Delegáltak
  • Enumerációk

Általában kerülje ezeket a funkciókat, hacsak nem kell használnia őket:

  • Öröklésalapú típusú hierarchiák és implementációöröklés
  • Null értékek és Unchecked.defaultof<_>

A kompozíció előnyben részesítése az öröklés helyett

Az öröklődés feletti összetétel hosszú távú kifejezés, amelyet a jó F#-kód képes betartani. Az alapelv az, hogy ne tegye közzé az alaposztályt, és kényszerítse a hívókat arra, hogy az alaposztálytól örököljenek a funkciók eléréséhez.

Objektumkifejezések használata interfészek implementálásához, ha nincs szüksége osztályra

Az objektumkifejezések lehetővé teszik, hogy menet közben implementáljon interfészeket, és a implementált felületet egy értékhez kötje anélkül, hogy ezt egy osztályon belül kellene megtennie. Ez kényelmes, különösen akkor, ha csak az interfészt kell implementálnia, és nincs szüksége teljes osztályra.

Az Ionide-ben futtatott kód például egy kódjavítási művelet megadására szolgál, ha olyan szimbólumot adott hozzá, amely nem rendelkezik open utasítással:

    let private createProvider () =
        { new CodeActionProvider with
            member this.provideCodeActions(doc, range, context, ct) =
                let diagnostics = context.diagnostics
                let diagnostic = diagnostics |> Seq.tryFind (fun d -> d.message.Contains "Unused open statement")
                let res =
                    match diagnostic with
                    | None -> [||]
                    | Some d ->
                        let line = doc.lineAt d.range.start.line
                        let cmd = createEmpty<Command>
                        cmd.title <- "Remove unused open"
                        cmd.command <- "fsharp.unusedOpenFix"
                        cmd.arguments <- Some ([| doc |> unbox; line.range |> unbox; |] |> ResizeArray)
                        [|cmd |]
                res
                |> ResizeArray
                |> U2.Case1
        }

Mivel a Visual Studio Code API-val való interakcióhoz nincs szükség osztályra, az Object Expressions ideális eszköz erre. Emellett értékesek az egységteszteléshez is, ha improvizált módon szeretné kicsúszni a tesztelési rutinok felületét.

Az aláírások rövidítéséhez fontolja meg a Típus rövidítések használatát

A típus-rövidítések kényelmesen hozzárendelhetők egy címkéhez egy másik típushoz, például egy függvény-aláíráshoz vagy egy összetettebb típushoz. Az alábbi alias például egy címkét rendel hozzá ahhoz, ami a CNTK-val, egy mélytanulási kódtárral való számítás definiálásához szükséges:

open CNTK

// DeviceDescriptor, Variable, and Function all come from CNTK
type Computation = DeviceDescriptor -> Variable -> Function

A Computation név egy kényelmes módja annak, hogy bármilyen függvényt jelöljön, amely megfelel az aliasként megadott aláírásnak. Az ilyen típusú rövidítések használata kényelmes, és tömörebb kódot tesz lehetővé.

Kerülje a type abbreviations használatát a tartomány ábrázolásához

Bár a típus-rövidítések kényelmesen adhatnak nevet a függvény-aláírásoknak, más típusok rövidítésekor zavaróak lehetnek. Fontolja meg ezt a rövidítést:

// Does not actually abstract integers.
type BufferSize = int

Ez több szempontból is zavaró lehet:

  • BufferSize nem absztrakció; ez csak egy egész szám neve.
  • Ha BufferSize nyilvános API-ban van közzétéve, könnyen félreérthető, hogy többet jelent, mint egyszerűen int. A tartománytípusok általában több attribútummal rendelkeznek, és nem primitív típusok, például int. Ez a rövidítés sérti ezt a feltételezést.
  • A (PascalCase) burkolata BufferSize azt jelenti, hogy ez a típus több adatot tárol.
  • Ez az alias nem biztosít nagyobb egyértelműséget ahhoz képest, hogy elnevezett argumentumot ad meg egy függvénynek.
  • A rövidítés nem jelenik meg a lefordított IL-ben; ez csak egy egész szám, és ez az alias egy fordítási idő szerkezet.
module Networking =
    ...
    let send data (bufferSize: int) = ...

Összefoglalva, a Típus rövidítések buktatója az, hogy nem absztrakciók az általuk rövidített típusokkal szemben. Az előző példában BufferSize csak egy int fedél alatt található, extra adatok nélkül, és a típusrendszer előnyei sem a már meglévőken int kívül.

A típus rövidítések tartományt ábrázoló rövidítéseinek másik módszere az egy-egyes megkülönböztetést alkalmazó egyesítők használata. Az előző minta a következőképpen modellelhető:

type BufferSize = BufferSize of int

Ha olyan kódot ír, amely a mögöttes értékével és értékével BufferSize kapcsolatos, akkor ahelyett, hogy tetszőleges egész számokat ad át, létre kell készítenie egyet:

module Networking =
    ...
    let send data (BufferSize size) =
    ...

Ez csökkenti annak a valószínűségét, hogy véletlenül egy tetszőleges egész számot ad át a send függvénynek, mivel a hívónak olyan típust BufferSize kell létrehoznia, amely a függvény meghívása előtt körbefuttat egy értéket.