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 MyClass
modulnak megfelelőnek MyCode
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 a nyilatkozatokat open
is (és open type
, csak távolabbiként 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 utasításokat a rendszer rétegeinekopen
. 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>/config-options.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:
- Bármely függő állapot leküldése az API-n kívülre.
- A konfiguráció mostantól az API-n kívül is elvégezhető.
- A függő értékek inicializálási hibái valószínűleg nem jelennek meg
TypeInitializationException
. - 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:
- A tartomány idővel történő változásaival könnyebben karbantartható.
- 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 ArgumentNullException
mechanizmusaké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.
let
Az 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.doTransaction
ImplementationLogic.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 nem
public
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ó
private
megtartá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, inline
a 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 is, így a változók hatóköre mutable
közel vagy elméletileg a legkisebb szinten 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 completed
használt mutable-hozdata
.
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 mutable
ref
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 let
kö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.
F# 9 null szintaxis használata az API-határoknál
Az F# 9 szintaxist ad hozzá, hogy explicit módon kijelentse, hogy egy érték null értékű lehet. Úgy lett kialakítva, hogy az API-határoknál használható legyen, hogy a fordító jelezze azokat a helyeket, ahol a null kezelés hiányzik.
Íme egy példa a szintaxis érvényes használatára:
type CustomType(m1, m2) =
member _.M1 = m1
member _.M2 = m2
override this.Equals(obj: obj | null) =
match obj with
| :? CustomType as other -> this.M1 = other.M1 && this.M2 = other.M2
| _ -> false
override this.GetHashCode() =
hash (this.M1, this.M2)
Kerülje el a null értékek továbbadását az F# kódban.
let getLineFromStream (stream: System.IO.StreamReader) : string | null =
stream.ReadLine()
Ehelyett, használjon idiomatikus F#-eszközöket (például beállításokat):
let getLineFromStream (stream: System.IO.StreamReader) =
stream.ReadLine() |> Option.ofObj
A nullhoz kapcsolódó kivételek emeléséhez speciális nullArgCheck
és nonNull
függvényeket használhat. Azért is hasznosak, mert ha az érték nem null, akkor árnyékot az argumentumot a megtisztított értékével – a további kód már nem fér hozzá a lehetséges nullmutatókhoz.
let inline processNullableList list =
let list = nullArgCheck (nameof list) list // throws `ArgumentNullException`
// 'list' is safe to use from now on
list |> List.distinct
let inline processNullableList' list =
let list = nonNull list // throws `NullReferenceException`
// 'list' is safe to use from now on
list |> List.distinct
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ágItem
definiálásával - Szeletelő jelölés (
arr[x..y]
,arr[x..]
,arr[..y]
), tagok definiálásávalGetSlice
- 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
ésIEnumerable
- 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űenint
. A tartománytípusok általában több attribútummal rendelkeznek, és nem primitív típusok, példáulint
. 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.