F# kodlama kuralları

Aşağıdaki kural, büyük F# kod tabanıyla çalışma deneyiminden formüle edildi. Her önerinin temeli, iyi F# kodunun beş ilkesidir. Bunlar F# bileşen tasarım yönergeleriyle ilgilidir,ancak yalnızca kitaplıklar gibi bileşenler için değil tüm F# kodlar için geçerlidir.

Kodu düzenleme

F# kodu düzenlemenin iki temel yolu vardır: modüller ve ad alanları. Bunlar benzerdir, ancak aşağıdaki farklar vardır:

  • Ad alanları .NET ad alanları olarak derlenmiş. Modüller statik sınıflar olarak derlenmiş.
  • Ad alanları her zaman en üst düzeydir. Modüller üst düzey olabilir ve diğer modüllerin içinde iç içe geçmiş olabilir.
  • Ad alanları birden çok dosyaya yayma. Modüller kullanılamaz.
  • Modüller ve ile birlikte dekore [<RequireQualifiedAccess>] edilmiş [<AutoOpen>] olabilir.

Aşağıdaki yönergeler, kodunuzu düzenlemek için bunları kullanmanıza yardımcı olur.

Ad alanlarını en üst düzeyde tercih etmek

Genel olarak tüketilebilir tüm kodlar için, ad alanları en üst düzeyde modüller için tercih eder. .NET ad alanları olarak derlenmiş olduğundan, C# ile kullanılabilir ve hiçbir sorun yoktur.

// Good!
namespace MyCode

type MyClass() =
    ...

Üst düzey bir modülün kullanımı yalnızca F# ile çağrıldıklarında farklı görüne görünebilir, ancak C# tüketicileri için, modülü kullanmaları gerekerek MyClass çağıranlar MyCode şaşırtabilirsiniz.

// Bad!
module MyCode

type MyClass() =
    ...

Dikkatle uygula [<AutoOpen>]

yapısı, çağıranların nelerin kullanılabilir olduğu konusunda bilgi sahibi olabilir ve bir şeyin nereden geldiğine yanıt [<AutoOpen>] "sihirli" olabilir. Bu iyi bir şey değildir. Bu kuralın bir istisnası F# Çekirdek Kitaplığı'nın kendisidir (ancak bu durum biraz tartışmalıdır).

Ancak, genel API'den ayrı olarak düzenlemek istediğiniz bir genel API için yardımcı işlevselliğine sahip olmak size kolaylık sağlar.

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

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

        helper1 x y z

Bu, her çağrıda bir yardımcıyı tam olarak nitelendirmeden uygulama ayrıntılarını bir işlevin genel API'lerinden temiz bir şekilde ayırmanıza olanak sağlar.

Ayrıca, uzantı yöntemlerinin ve ifade oluşturucuların ad alanı düzeyinde açığa çıkararak ile düzgün bir şekilde ifade [<AutoOpen>] edebilirsiniz.

Adların [<RequireQualifiedAccess>] çakışması veya okunabilirlik açısından yardımcı olduğunu hissetmeniz için kullanın

Bir modüle özniteliğini eklemek, modülün açılamay olduğunu ve modülün öğelerine yapılan başvurular için açık [<RequireQualifiedAccess>] tam erişim gerektirmektedir. Örneğin, Microsoft.FSharp.Collections.List modülde bu öznitelik vardır.

Modülde yer alan işlevlerin ve değerlerin diğer modüllerde adlarla çakışma olasılığı olan adları olduğunda bu yararlı olur. Nitelikli erişim gerektirerek uzun süreli bakım ve kitaplığın gelişme becerisini önemli ölçüde artırabilirsiniz.

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

...

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

Deyimleri open topolojik olarak sıralama

F# içinde deyimler dahil olmak üzere bildirimlerin sırası open önemlidir. Bu, ve etkisinin bir dosyada bu deyimlerin sıralamadan bağımsız olduğu C# using using static değil.

F# içinde, bir kapsamda açılan öğeler zaten mevcut olan diğer öğeleri gölgeler. Başka bir ifadeyle deyimleri open yeniden sıralamak kodun anlamını değiştirebilir. Sonuç olarak, tüm deyimlerin rastgele sıralamaları (örneğin, alfasayısal) önerilmez; beklediğiniz farklı bir davranış open oluşturmanız önerilir.

Bunun yerine, bunları topolojik olarak sıralamayı öneririz; başka bir ifadeyle open deyimlerinizi sistem katmanlarının tanımlandığı sırada sırala. Farklı topolojik katmanlarda alfasayısal sıralama yapmak da dikkate alınmalıdır.

Örneğin, F# derleyici hizmeti genel API dosyası için topolojik sıralama şöyledir:

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

Çizgi sonu topolojik katmanları birbirinden ayırarak her katman daha sonra alfasayısal olarak sıralanır. Bu, yanlışlıkla değerleri gölgelemeden kodu temiz bir şekilde organize eder.

Yan etkileri olan değerleri içermek için sınıfları kullanma

Bir değeri başlatmanın, bir veritabanına veya başka bir uzak kaynağa bağlam örneği oluşturma gibi yan etkileri olabilir. Bu tür şeyleri bir modülde başlatmak ve sonraki işlevlerde kullanmak cazip bir fikirdir:

// This is bad!
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 = doSutffWith dep1 dep2 dep3 arg

Bu genellikle birkaç nedenden dolayı kötü bir fikirdir:

İlk olarak, uygulama yapılandırması ve ile kod tabanına dep1 dep2 itilir. Bunun daha büyük kodbase'lerde korunması zordur.

İkinci olarak, bileşeniniz birden çok iş parçacığı kullanacaksa statik olarak başlatılan veriler iş parçacığı güvenli olmayan değerleri içermez. Bu açıkça tarafından ihlal dep3 edildi.

Son olarak, modül başlatma tüm derleme birimi için statik bir oluşturucuda derler. Bu modülde let-bound değer başlatmada herhangi bir hata oluşursa, uygulamanın tüm ömrü boyunca önbelleğe alınan bir TypeInitializationException olarak bildirim alır. Bu tanılama zor olabilir. Genellikle hakkında gerekçeli bir gerekçe denemesi yapmaya çalışabilecek bir iç özel durum vardır, ancak yoksa, kök nedenin ne olduğunu söylemek yoktur.

Bunun yerine, bağımlılıkları tutmak için basit bir sınıf kullanın:

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

Bu, şunları sağlar:

  1. Herhangi bir bağımlı durumu API'nin dışına itme.
  2. Yapılandırma artık API'nin dışında yapılabilir.
  3. Bağımlı değerler için başlatma hatalarının olarak bildirileme olasılığı TypeInitializationException düşük.
  4. API'yi test etmek artık daha kolaydır.

Hata yönetimi

Büyük sistemlerde hata yönetimi karmaşık ve nüanslı bir çabadır ve sistemlerinizin hataya karşı iyi davranmasını ve iyi davranmasını sağlamada gümüş madde yoktur. Aşağıdaki yönergeler, bu zor alanda gezinme konusunda rehberlik sunmalıdır.

Etki alanınız için iç türlerde hata durumlarını ve geçersiz durumu temsil etme

Ayrımlı Unions ileF# size tür sisteminizin hatalı program durumunu temsil etme olanağı sağlar. Örnek:

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

Bu durumda, bir banka hesabından para çekmenin başarısız olması için bilinen üç yol vardır. Her hata durumu türünde temsil edilen ve bu nedenle program genelinde güvenli bir şekilde ele alınabilirsiniz.

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"

Genel olarak, etki alanınıza bir şeyin başarısız olması için farklı yollar modelleyebilirsiniz. Hata işleme kodu artık normal program akışına ek olarak ele atayacak bir şey olarak kabul edilebilir. Bu yalnızca normal program akışının bir parçasıdır ve olağanüstü olarak kabullanmaz. Bunun iki temel faydası vardır:

  1. Etki alanınız zaman içinde değiştikleri için bakım yapmak daha kolaydır.
  2. Hata durumlarının birim testi daha kolaydır.

Hatalar türlerle temsili olamazken özel durumları kullanma

Bir sorun etki alanında tüm hatalar temsili değildir. Bu tür hatalar doğası gereği olağanüstüdür, bu nedenle F# içinde özel durumları yükseltebilme ve yakalama özelliğidir.

İlk olarak, Özel Durum Tasarımı Yönergeleri'nin okuması önerilir. Bunlar F# için de geçerlidir.

F# içinde özel durum oluşturma amacıyla kullanılabilen ana yapılar aşağıdaki tercih sırasına göre dikkate alınmalıdır:

İşlev Syntax Amaç
nullArg nullArg "argumentName" Belirtilen bağımsız System.ArgumentNullException değişken adıyla bir değerine neden olur.
invalidArg invalidArg "argumentName" "message" Belirtilen bağımsız değişken System.ArgumentException adı ve iletisiyle bir değerine neden olur.
invalidOp invalidOp "message" Belirtilen System.InvalidOperationException iletiyle bir'i yukarılar.
raise raise (ExceptionType("message")) Özel durumlar için genel amaçlı mekanizma.
failwith failwith "message" Belirtilen System.Exception iletiyle bir'i yukarılar.
failwithf failwithf "format string" argForFormatString Biçim dizesi System.Exception ve girdileri tarafından belirlenen bir ileti ile bir döndürür.

Uygun nullArg olduğunda , , invalidArg invalidOp ve'i atacak ArgumentNullException mekanizma olarak , ve ArgumentException InvalidOperationException kullanın.

Ve failwith işlevleri failwithf genellikle kaçınılmalıdır çünkü bunlar belirli bir özel Exception durum değil, temel türü oluşturur. Özel Durum Tasarımı Yönergeleri'negöre, mümkün olduğunda daha belirli özel durumlar oluşturabilirsiniz.

Özel durum işleme söz dizimi kullanma

F# söz dizimi aracılığıyla özel durum desenlerini try...with destekler:

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

Kodu temiz tutmak isterseniz desen eşleştirme ile bir özel durumla karşı karşıya gerçekleştirmek için işlevselliğin mu mutabakatı biraz karmaşık olabilir. Bunu işlemenin böyle bir yolu, özel durumla birlikte hata örneğinin çevresindeki işlevselliği grup için bir yol olarak etkin desenleri kullanmaktır. Örneğin, bir özel durum edesnak esnederek özel durum meta verilerine değerli bilgileri kapsayan bir API'yi kullanabilirsiniz. Etkin Desenin içinde yakalanan özel durumun gövdesinde yararlı bir değerin yeniden yakalanması ve bu değerin döndürilmesi bazı durumlarda yararlı olabilir.

Özel durumları değiştirmek için monadic hata işlemeyi kullanma

Özel durumlar genellikle işlevsel programlamada taboo olarak görülür. Aslında, özel durumlar ihlalleri ihlal ettiği için bunları işlevsel değil olarak düşünmek güvenlidir. Ancak bu, kodun çalışması gereken yeri ve çalışma zamanı hataları oluşabilir gerçekliği yok sayar. Genel olarak, istenmeyen sürprizleri en aza indirmek için çoğu şeyin saf veya toplam olmadığını varsayımı üzerine kod yazın.

Özel Durumların .NET çalışma zamanı ve diller arası ekosisteminin bir bütün olarak ilgi düzeyi ve uygunluğu açısından aşağıdaki temel güçlü/farklı yönlerini göz önünde bulundurarak göz önünde bulundurulur:

  • Bunlar, bir sorunda hata ayıklarken yararlı olan ayrıntılı tanılama bilgileri içerir.
  • Bunlar çalışma zamanı ve diğer .NET dilleri tarafından iyi anlaşılır.
  • Özel durumlardan kaçınmak için semantiklerinin bir alt kümesini geçici olarak uygulayan kodla karşılaştırıldığında önemli ortakları azaltabilir.

Bu üçüncü nokta kritik öneme sahip. Önemsiz karmaşık işlemler için özel durumların kullanılamaması aşağıdaki gibi yapılarla ilgilenmeye neden olabilir:

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

Bu da "dize türünde" hatalarda desen eşleştirme gibi hassas kodlara kolayca yol açabiliyor:

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?

Buna ek olarak, "daha iyi" bir tür döndüren "basit" bir işlevin isteğinde herhangi bir özel durumu ifade etmek cazip olabilir:

// This is bad!
let tryReadAllText (path : string) =
    try System.IO.File.ReadAllText path |> Some
    with _ -> None

Ne yazık ki, bir dosya sisteminde neler olabileceğine bağlı olarak çok sayıda özel durum oluşturur ve bu kod ortamınız içinde gerçekten yanlış olan durumla ilgili tüm tryReadAllText bilgileri atar. Bu kodu bir sonuç türüyle değiştirirsanız, "dizeli türe sahip" hata iletisi ayrıştırmaya geri dönersiniz:

// This is bad!
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 ...

Ayrıca özel durum nesnesinin kendisini oluşturucuya yerleştirmek sizi işlev yerine çağrı sitesinde özel durum türüyle düzgün Error bir şekilde başa çağırmaya zorlar. Bunu yapmak, bir API'yi çağıran olarak iş yapmak için kötü amaçlı olmayan işaretli özel durumlar oluşturur.

Yukarıdaki örneklerin iyi bir alternatifi, belirli özel durumları yakalamak ve bu özel durum bağlamında anlamlı bir değer dönmektir. İşlevi aşağıdaki tryReadAllText gibi değiştirirsanız daha None anlamı vardır:

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

Bu işlev artık bir dosya bulunamadıklarında durumu düzgün bir şekilde ele atayacak ve bu anlamı bir dönüşe atayacak. Bu dönüş değeri, bağlamsal bilgileri atmayacak veya çağıranları kodda o noktada uygun olmayacak bir durumla başa dönmeye zorlayarak bu hata durumuyla eş olabilir.

Gibi türler, iç içe geçmiş olmayan temel işlemler için uygundur ve F# isteğe bağlı türleri, bir şeyin bir şey veya hiçbir şey getirene zamanları temsil Result<'Success, 'Error> etme için mükemmeldir. Ancak bunlar, özel durumların yerini alan bir uygulama değildir ve özel durumları değiştirme girişiminde kullanılmamaları gerekir. Bunun yerine, özel durum ve hata yönetimi ilkesine yönelik belirli yönleri hedefli yöntemlerle ele etmek için bu ilkeler aynı şekilde uygulanmalıdır.

Kısmi uygulama ve noktasız programlama

F# kısmi uygulamayı ve bu nedenle, noktasız stilde program etmenin çeşitli yollarını destekler. Bu, kodun bir modülde yeniden kullanılması veya bir şeyin uygulanması için yararlı olabilir, ancak genel kullanıma açık bir şey değildir. Genel olarak, noktasız programlama hem kendisi hem de kendisi için bir fazilet değildir ve stile dalmış kişiler için önemli bir bilişsel engel ekleyebilir.

Kısmi uygulama ve genel API'lerde currying kullanma

Çok az istisnayla, genel API'lerde kısmi uygulama kullanımı tüketiciler için kafa karıştırıcı olabilir. F# let kodundaki -bound değerleri genellikle işlev değerleri değil değerleridir. Değerlerin ve işlev değerlerinin bir arada çalışması, özellikle de işlev oluşturma gibi işleçlerle birlikte oldukça fazla bilişsel ek yük karşılığında birkaç kod satırı >> kaydetmeye neden olabilir.

Noktadan ücretsiz programlama için araç etkilerini göz önünde bulundurabilirsiniz

Curried işlevleri bağımsız değişkenlerini etiketlemez. Bunun araç üzerinde etkileri vardır. Aşağıdaki iki işlevi düşünün:

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!"

Her ikisi de geçerli funcWithApplication işlevlerdir, ancak bir curried işlevidir. Bir düzenleyicide türlerinin üzerine gelindiğinde şunları görüyorsunuz:

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

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

Çağrı sitesinde, Visual Studio gibi araç ipucu size tür imzası sağlar, ancak ad tanımlanmamış olduğu için adları görüntülemez. Çağrıyı yapanların API'nin arkasındaki anlamı daha iyi anlamaları için adlar iyi API tasarımı açısından kritik öneme sahiptir. Genel API'de noktasız kod kullanmak, çağıranların ansını zorlaştırabilir.

Genel olarak tüketilebilir gibi noktasız kodlarla karşılaşırsanız, aracın bağımsız değişkenler için anlamlı adlar ala η tam bir genişletmesi funcWithApplication önerilir.

Ayrıca, mümkün olmayan nokta kodlarda hata ayıklamak zor olabilir. Hata ayıklama araçları, yürütme boyunca ara değerleri incelerken adlara (örneğin let bağlamalar) bağlı değerlere bağımlıdır. Kodunuz incelenecek değere sahip değilken, hata ayıklamak için bir şey yoktur. Gelecekte, hata ayıklama araçları daha önce yürütülen yollara göre bu değerleri sentezlemek için gelişiyor olabilir, ancak olası hata ayıklama işlevselliğiyle ilgili tahminlerinizi yapmak iyi bir fikir değildir.

Kısmi uygulamayı iç ortak kullanım süresini azaltmak için bir teknik olarak düşünün

Önceki noktadan farklı olarak kısmi uygulama, uygulamanın içindeki ortak özellikleri veya API'nin daha derin iç içlerini azaltmak için harika bir araçtır. Daha karmaşık API'leri uygulamanın birim testi için yararlı olabilir; burada ortak bir sorun genellikle başa çıkan bir sorun olur. Örneğin, aşağıdaki kod, böyle bir çerçeveye dış bağımlılık yapmadan ve ilgili bir özel API'yi öğrenmek zorunda kalmadan, sahte çerçevelerin çoğunun size neleri yaptığını nasıl gerçekleştirebilirsiniz?

Örneğin, aşağıdaki çözüm topografisi düşünün:

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

ImplementationLogic.fsproj gibi kodu açığa çıkarabilirsiniz:

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

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

içinde birim Transactions.doTransaction ImplementationLogic.Tests.fsproj testi kolaydır:

namespace TransactionsTestingUtil

open Transactions

module TransactionsTestable =
    let getTestableTransactionRoutine mockContext = Transactions.doTransaction mockContext

Sahte bağlam nesnesiyle kısmen uygulamak, her zaman sahte bir bağlam oluşturmak zorunda kalmadan işlevi tüm birim testlerinde doTransaction çağırmanıza olanak sağlar:

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)

Bu tekniği kod tabanının tamamına evrensel olarak uygulatma, ancak karmaşık içler ve bu içlerin birim testi için ortak kullanımları azaltmanın iyi bir yoludur.

Erişim denetimi

F# içinde Erişim denetimi için .NETçalışma zamanında kullanılabilenden devralınan birden çok seçeneği vardır. Bunlar yalnızca türler için kullanılabilir değildir; bunları işlevler için de kullanabilirsiniz.

  • Türlerin ve public üyelerin genel kullanıma açık olması gerekinceye kadar türleri ve üyeleri tercih edersiniz. Bu, tüketicilerin hangi tüketicilere çift olduğunu da en aza indirger.
  • tüm yardımcı işlevlerini tutmak için private çabayın.
  • Çok sayıda yardımcı [<AutoOpen>] işleve dönüşen özel bir modülde kullanımını göz önünde bulundurabilirsiniz.

Tür çıkarlığı ve genel türler

Tür çıkarlığı, çok fazla ortak yazmaktan tasarruf edebilirsiniz. Ayrıca F# derleyicisinde otomatik genelleştirme, neredeyse hiç ek çabayla daha genel kod yazmanıza yardımcı olabilir. Ancak, bu özellikler evrensel olarak iyi değildir.

  • Genel API'lerde açık türlerle bağımsız değişken adlarını etiketlemeyi göz önünde bulundurarak bunun için tür çıkarması gerekmez.

    Bunun nedeni, derleyicinin değil API'nizin şeklinin denetiminde olmasıdır. Derleyici sizin için çıkarım türlerinde iyi bir iş çıkarsa da, bağlı olduğu içlerin türleri değişmişse API'nizin şeklinin değişmesi mümkündür. Bu istediğiniz şey olabilir, ancak aşağı akış tüketicilerinin daha sonra ilgilenecekleri yeni bir API değişikliğine neden olur. Bunun yerine, genel API'nizin şeklini açıkça kontrol ediyorsanız, bu yeni değişiklikleri de kontrol edin. DDD açısından bu bir Bozulma önleyici katman olarak düşünebilirsiniz.

  • Genel bağımsız değişkenlerinize anlamlı bir ad vermeyi düşünün.

    Belirli bir etki alanına özgü olmayan gerçekten genel bir kod yazmadıkça, anlamlı bir ad diğer programcıların çalışmakta olduğu etki alanını anlamalarında yardımcı olabilir. Örneğin, bir belge veritabanıyla etkileşim kurma bağlamında adlı bir tür parametresi, genel belge türlerinin birlikte çalışmakta olan işlev veya üye tarafından kabul edilegeleni 'Document daha net bir şekilde ifade eder.

  • Genel tür parametrelerini PascalCase ile adlandırmayı göz önünde bulundurarak.

    Bu, .NET'te işleri yapmak için genel bir yol olduğu için, ya da camelCase yerine PascalCase snake_case önerilir.

Son olarak, otomatik genelleştirme F# veya büyük bir kod tabanına yeni sahip olan kişiler için her zaman bir boon değildir. Genel bileşenlerin kullanımında bilişsel ek yük vardır. Ayrıca, otomatik olarak genelleştirilmiş işlevler farklı giriş türleriyle birlikte kullanılmazsa (bu şekilde kullanılmaları tek başına), genel olmalarına gerçek bir avantaj yoktur. Her zaman, yazmakta olan kodun gerçekten genel olmaktan yararlanacak olup olamayacaklarını göz önünde bulundurabilirsiniz.

Performans

Yüksek ayırma oranlarına sahip küçük türler için yapılarını göz önünde bulundurarak

Yapıların (Değer Türleri olarak da adlandırılan) kullanımı, genellikle nesnelerin başka bir kodda yer alan nesnelerden kaçınması nedeniyle bazı kodlar için daha yüksek performansa neden olabilir. Ancak, yapılar her zaman "daha hızlı git" düğmesi değildir: bir yapıda yer alan verilerin boyutu 16 baytı aşarsa, verilerin kopyalandırıldığında genellikle başvuru türüne göre daha fazla CPU süresi harcanmasına neden olabilir.

Bir yapı kullanmanın gerek olup olmadığını belirlemek için aşağıdaki koşulları göz önünde bulundurarak:

  • Verilerinizin boyutu 16 bayt veya daha küçükse.
  • Bu türlerde çalışan bir programda bellekte yer alan birçok örneğine sahip olma olasılığınız varsa.

İlk koşul geçerli ise genellikle bir yapı kullansanız iyi olur. Her ikisi de geçerli olursa, neredeyse her zaman bir yapı kullansanız iyi olur. Önceki koşulların geçerli olduğu bazı durumlar olabilir, ancak bir yapının kullanımı başvuru türü kullanmaktan daha iyi veya daha kötü değildir ancak nadir de olabilir. Ancak bu gibi değişiklikler yaparken her zaman ölçmeniz ve varsayım veya tahminle çalışmamanız önemlidir.

Küçük değer türlerini yüksek ayırma oranlarıyla gruplarken yapı gruplamalarını göz önünde bulundurarak

Aşağıdaki iki işlevi düşünün:

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)

Bu işlevleri BenchmarkDotNetgibi istatistiksel bir karşılaştırma aracıyla kıyaslarken, yapı verisi kullanan işlevin %40 daha hızlı çalıştır kullandığını ve bellek ayırmaya gerek olmadığını runWithStructTuple bulur.

Ancak, bu sonuçlar kendi kodunda her zaman böyle olmayacaktır. Bir işlevi olarak işaretlersanız, başvuruupluplarını kullanan kod bazı ek iyileştirmeler veya ayıracak inline kod basitçe iyileştirilmiş olabilir. Performans söz konusu olduğunda her zaman sonuçları ölçmeli ve hiçbir zaman varsayım veya tahmine dayalı olarak çalışmamalı.

Tür küçük olduğunda ve yüksek ayırma oranlarına sahip olduğunda yapı kayıtlarını göz önünde bulundurabilirsiniz

Daha önce açıklanan başparmak kuralı, F# kayıt türleri için de tutar. Bunları işleyen aşağıdaki veri türlerini ve işlevleri göz önünde önünden yapın:

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)

Bu, önceki demet koduna benzerdir, ancak bu kez örnek kayıtları ve satır içi bir iç işlevi kullanır.

Bu işlevleri Benchmarkdotnetgibi istatistiksel bir değerlendirme aracı ile kıyaslandığınızda, processStructPoint %60 daha hızlı bir şekilde çalıştığını ve yönetilen yığında hiçbir şey ayırdığını görürsünüz.

Veri türü yüksek ayırma oranları ile küçük olduğunda struct ayrılmış birleşimler kullanın

Struct tanımlama grupları ve kayıtlarıyla performans hakkında önceki gözlemler Ayrıca, F # ayrılmış birleşimleriçin de geçerlidir. Aşağıdaki kodu inceleyin:

    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

Bu, etki alanı modelleme için bunun gibi tek büyük harfli ayırt edici birleşimler tanımlamak yaygındır. Bu işlevleri Benchmarkdotnetgibi istatistiksel bir değerlendirme aracı ile kıyaslandığınızda, structReverseName reverseName küçük dizeler için %25 daha hızlı bir şekilde çalıştığını fark edeceksiniz. Büyük dizeler için her ikisi de aynı şekilde gerçekleştirilir. Bu nedenle, bu durumda her zaman bir struct kullanılması tercih edilir. Daha önce belirtildiği gibi, varsayımlar veya ıntuklarda her zaman ölçüm ve işlem kullanmayın.

Önceki örnekte, bir yapının ayırt edici UNION birleşimi daha iyi performans olduğunu gösterdi, ancak bir etki alanını modellemesi sırasında daha büyük ayırt edici birleşimler olması yaygındır. Daha büyük veri türleri, daha fazla kopyalama söz konusu olduğundan, bunlar üzerinde işlemlere göre yapılar olmaları halinde de gerçekleştirilemeyebilir.

Fonksiyonel programlama ve mutasyon

F # değerleri varsayılan olarak sabittir. Bu, belirli hata sınıflarından kaçınmanızı sağlar (özellikle eşzamanlılık ve paralellik dahil olanlar). Bununla birlikte, belirli durumlarda, yürütme süresi veya bellek ayırmaları için en iyi (veya hatta makul) verimlilik elde etmek üzere bir iş yayılımı en iyi duruma geçen durum ile uygulanabilir. Bu, anahtar sözcüğü ile F # ile bir katılım temelinde mümkündür mutable .

mutableF # içinde öğesinin kullanımı, gürültü 'yi işlevsel bir şekilde kullanabilir. Bu, anlaşılır değildir ancak her yerde işlevsel anlaya, performans hedeflerine sahip gürültü 'de bulunabilir. Bir uzlaşma, çağrı yapanların bir işlevi çağırdıkları sırada ne olacağı hakkında İlgilenmemeleri gereken her şeyi kapsüllemesidir. Bu, performans açısından kritik kod için bir mutasyon tabanlı uygulama üzerinde işlevsel bir arabirim yazmanızı sağlar.

Değişmez arabirimlerde kesilebilir kodu sarın

Amaç olarak bilgi saydamlığı ile, performans açısından kritik işlevlerin değişebilir işlevlerini açığa çıkaran bir kod yazmak çok önemlidir. Örneğin, aşağıdaki kod Array.contains Işlevi F # Çekirdek Kitaplığı 'nda uygular:

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

Bu işlevi birden çok kez çağırmak, temel alınan diziyi değiştirmez veya onu tükettiği herhangi bir kesilebilir durumu korumanıza gerek duyar. Neredeyse her bir kod satırı mutation kullandığından bile, bu değer saydam bir şekilde görünür.

Sınıflarda kesilebilir verileri kapsüllemek için kullanın

Önceki örnekte, değiştirilebilir verileri kullanarak işlemleri kapsüllemek için tek bir işlev kullanılmıştır. Bu, daha karmaşık veri kümeleri için her zaman yeterli değildir. Aşağıdaki işlev kümelerini göz önünde bulundurun:

open System.Collections.Generic

let addToClosureTable (key, value) (t: Dictionary<_,_>) =
    if not (t.ContainsKey(key)) then
        t.Add(key, value)
    else
        t[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

Bu kod performanyadır, ancak çağıranların korunmasından sorumlu olduğu mutasyon tabanlı veri yapısını kullanıma sunar. Bu, değiştireyebilecek temel üye olmadan bir sınıfın içine sarmalanabilir:

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 not (t.ContainsKey(key)) then
            t.Add(key, value)
        else
            t[key] <- value

    member _.Count = t.Count

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

Closure1Table temel alınan mutasyon tabanlı veri yapısını kapsüller, böylece çağıranlar temel alınan veri yapısını sürdürmek üzere zorlar. Sınıflar, çağıranların ayrıntılarını açığa çıkarmadan tabanlı verileri ve yordamları kapsüllemek için güçlü bir yoldur.

Hücrelere başvuru yapmayı tercih et let mutable

Başvuru hücreleri değerin kendisi yerine bir değere başvuruyu temsil etmenin bir yoludur. Performans açısından kritik kod için kullanılabilmesine rağmen, bunlar önerilmez. Aşağıdaki örneği inceleyin:

let kernels =
    let acc = ref Set.empty

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

    !acc |> Seq.toList

Başvuru hücresinin kullanımı artık "pollutes", temel alınan verilere başvuru yapmak ve yeniden başvuru yapmak zorunda olan tüm izleyen koddur. Bunun yerine şunları göz önünde bulundurun 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

Lambda ifadesinin ortasında yer alan tek bir noktadan sonra, dokunduğu diğer tüm kodlar, acc normal let bağlantılı sabit değerin kullanılmasına farklı olmayan bir şekilde bunu yapabilir. Bu, zaman içinde değişiklik yapmayı kolaylaştırır.

Nesne programlama

F #, nesneler ve nesne yönelimli (OO) kavramları için tam desteğe sahiptir. Birçok OO kavramı güçlü ve yararlı olsa da, bunların hepsi kullanım için idealdir. Aşağıdaki listeler, en yüksek düzeyde, OO özelliklerinin kategorileri üzerinde rehberlik sunar.

Bu özellikleri birçok durumda kullanmayı göz önünde bulundurun:

  • Nokta gösterimi ( x.Length )
  • Örnek üyeleri
  • Örtük oluşturucular
  • Statik üyeler
  • Dizin Oluşturucu gösterimi ( arr[x] ), bir Item özellik tanımlayarak
  • arr[x..y]Üyeleri tanımlayarak gösterimi (, arr[x..] , arr[..y] ) GetSlice
  • Adlandırılmış ve Isteğe bağlı bağımsız değişkenler
  • Arabirimler ve arabirim uygulamaları

Önce bu özelliklere ulaşmayın, ancak bir sorunu çözmek için uygun olmaları durumunda bozacağından uygulayın:

  • Yöntem aşırı yüklemesi
  • Encapsulated kesilebilir veriler
  • Türlerde işleçler
  • Otomatik Özellikler
  • Uygulama IDisposable ve IEnumerable
  • Tür uzantıları
  • Ekinlikler
  • Yapılar
  • Temsilciler
  • Numaralandırmalar

Bunları kullanmanız gerekmedikçe bu özelliklerden genellikle kaçının:

  • Devralma tabanlı tür hiyerarşileri ve uygulama devralma
  • Null değerleri ve Unchecked.defaultof<_>

Devralma üzerine oluşturmayı tercih et

Devralma üzerinden bileşim , Iyi bir F # kodunun bağlı kalacağının uzun sürme deyimidir. Temel prensibi, temel bir sınıfı kullanıma sunmamalıdır ve arayanların işlevselliği almak için bu temel sınıftan devralmasını zorlamaktır.

Sınıf gerekmiyorsa arabirim uygulamak için nesne ifadelerini kullanın

Nesne ifadeleri , bir sınıfın içinde olması gerekmeden uygulanan arabirimi bir değere bağlayarak, anında arabirim uygulamanıza olanak tanır. Bu, özellikle de yalnızca arabirimini uygulamanız gerekiyorsa ve tam sınıfa gerek duygerekmiyorsa kullanışlı bir yöntemdir.

Örneğin, bir deyiminiz olmayan bir sembol eklediyseniz bir kod düzelme eylemi sağlamak için ıonıde 'de çalıştırılan kod aşağıda verilmiştir open :

    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
        }

Visual Studio Code apı ile etkileşim kurarken bir sınıfa gerek olmadığından, nesne ifadeleri bunun için ideal bir araçtır. Ayrıca, test yordamlarına sahip bir arabirimi improvised bir şekilde sağlamak istediğinizde birim testi için de önem taşır.

İmzaları kısaltmak için kısaltmalar türlerini göz önünde bulundurun

Tür kısaltmaları , bir etiketi bir işlev imzası veya daha karmaşık bir tür gibi başka bir türe atamak için kullanışlı bir yoldur. örneğin, aşağıdaki diğer ad, derinlemesine bir öğrenme kitaplığı olan CNTKbir hesaplama tanımlamak için gereken bir etiketi atar:

open CNTK

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

Ad, daha Computation sonra gelen imzayla eşleşen herhangi bir işlevi göstermek için uygun bir yoldur. Bu gibi tür kısaltmalarının kullanılması kullanışlıdır ve daha fazla kısa kodu sağlar.

Etki alanınızı temsil etmek için tür kısaltmalarının kullanmaktan kaçının

Tür kısaltmaları işlev imzalara bir ad vermek için uygun olsa da, abbreviating diğer türler olduğunda kafa karıştırıcı olabilir. Bu kısaltmayı göz önünde bulundurun:

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

Bu, birden çok şekilde kafa karıştırıcı olabilir:

  • BufferSize bir soyutlama değil; tamsayı için yalnızca başka bir addır.
  • BufferSizeOrtak BIR API 'de açığa çıkarılacası, kolayca yanlışlıkla büyük bir şekilde yorumlanabilmektedir int . Genellikle, etki alanı türlerinin kendileri için birden çok özniteliği vardır ve gibi basit türler değildir int . Bu kısaltma Bu varsayımını ihlal ediyor.
  • Büyük/küçük harf BufferSize (PascalCase), bu türün daha fazla veri bulundurduğunu gösterir.
  • Bu diğer ad, bir işleve adlandırılmış bir bağımsız değişken sağlamaya kıyasla daha fazla açıklık sunmaz.
  • Kısaltma derlenmiş Il 'de bildirim içermez; yalnızca bir tamsayıdır ve bu diğer ad derleme zamanı yapısıdır.
module Networking =
    ...
    let send data (bufferSize: int) = ...

Özet olarak, tür kısaltmalarıyla birlikte, abbreviating oldukları türler üzerinde soyutlamalar değildir . Önceki örnekte, BufferSize int hiçbir ek veri olmadan ve tür sisteminden zaten sahip olduğu gibi herhangi bir avantajın altında yer alır int .

Bir etki alanını temsil etmek için tür kısaltmalarının kullanılmasına alternatif bir yaklaşım, tek büyük harf ayrılmış birleşimler kullanmaktır. Önceki örnek aşağıdaki gibi modellenebilir:

type BufferSize = BufferSize of int

BufferSizeVe temel aldığı değer bakımından çalışan kodu yazarsanız, herhangi bir rastgele tamsayı geçirmek yerine bir tane oluşturmanız gerekir:

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

Bu, send çağıran BufferSize işlevi çağırmadan önce bir değeri kaydırmak üzere bir tür oluşturmasının gerektiği için, yanlışlıkla rastgele bir tamsayıyı işleve dönüştürmek olasılığını azaltır.