Aracılığıyla paylaş


F# kodlama kuralları

Aşağıdaki kurallar, büyük F# kod temelleriyle çalışma deneyiminden formüle edilir. İyi F# kodunun Beş ilkesi, her önerinin temelini oluşturur. Bunlar F# bileşeni 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.

Kod düzenleme

F# kodu düzenlemenin iki birincil yolunu içerir: modüller ve ad alanları. Bunlar benzerdir, ancak aşağıdaki farklılıklara sahiptir:

  • Ad boşlukları .NET ad alanları olarak derlenir. Modüller statik sınıflar olarak derlenir.
  • Ad alanları her zaman en üst düzeydir. Modüller üst düzey olabilir ve diğer modüllerin içinde iç içe yerleştirilebilir.
  • Ad alanları birden çok dosyaya yayılabilir. Modüller yapamaz.
  • Modüller [<RequireQualifiedAccess>] ve [<AutoOpen>] ile dekore edilebilir.

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

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

Genel kullanıma açık tüm kodlar için ad alanları, en üst düzeydeki modüller için tercih edilir. .NET ad alanları olarak derlendikleri için, C# içerisinde using static kullanmadan da kullanılabilirler.

// Recommended.
namespace MyCode

type MyClass() =
    ...

Üst düzey bir modülün kullanılması yalnızca F# dilinden çağrıldığında farklı görünmeyebilir, ancak C# kullanıcıları için, belli bir using static C# yapısının farkında olmadıklarında MyCode modülünü belirtmek zorunda kalarak şaşırabilirler.

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

type MyClass() =
    ...

Dikkatli bir şekilde uygula [<AutoOpen>]

Yapı [<AutoOpen>] çağıranların kullanabildiği şeylerin kapsamını kirletebilir ve bir şeyin nereden geldiğinin yanıtı "büyü"dür. Bu iyi bir şey değil. Bu kuralın bir istisnası F# Çekirdek Kitaplığı'nın kendisidir (ancak bu durum biraz tartışmalıdır).

Ancak, ortak API'den ayrı olarak düzenlemek istediğiniz bir genel API için yardımcı işlevselliğiniz varsa 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ğırdığınızda bir yardımcı fonksiyonu tam olarak nitelemenize gerek kalmadan, uygulama detaylarını bir fonksiyonun genel API'sinden net bir şekilde ayırmanızı sağlar.

Ayrıca, uzantı yöntemleri ve ifade oluşturucularının ad alanı düzeyinde görünür hale getirilmesi [<AutoOpen>] ile etkili bir biçimde ifade edilebilir.

Adların çakışabileceği veya okunabilirliğe yardımcı olduğunu hissettiğiniz her durumda kullanın [<RequireQualifiedAccess>]

özniteliğini [<RequireQualifiedAccess>] bir modüle eklemek, modülün açılmayabileceğini ve modülün öğelerine yapılan başvuruların açık nitelikli erişim gerektirdiğini gösterir. Örneğin modülde Microsoft.FSharp.Collections.List bu öznitelik bulunur.

Bu, modüldeki işlevlerin ve değerlerin diğer modüllerdeki adlarla çakışma olasılığı olan adlara sahip olması durumunda kullanışlıdır. Nitelikli erişim gerektirmek, uzun süreli bakım ve bir kitaplığın gelişme becerisini büyük ölçüde artırabilir.

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

...

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

Deyimleri topolojik olarak sıralama open

F# dilinde, open ifadeleri ve open type (daha sonra yalnızca open olarak adlandırılır) dahil olmak üzere, bildirimlerin sırası önemlidir. Bu, ve using etkisinin using static bir dosyadaki bu deyimlerin sıralanmasından bağımsız olduğu C# dilinden farklı bir durumdur.

F# dilinde, bir kapsama açılan öğeler zaten mevcut olan diğer öğeleri gölgeleyebilir. Bu, deyimleri yeniden sıralamanın open kodun anlamını değiştirebileceği anlamına gelir. Sonuç olarak, tüm open deyimlerin (örneğin alfasayısal olarak) rastgele sıralanması önerilmez, çünkü bekleyebileceğiniz farklı davranışlar oluşturursunuz.

Bunun yerine, bunları topolojik olarak sıralamanızı öneririz; yani, deyimlerinizi open sisteminizin katmanlarının tanımlandığı sırayla sıralayın. Alfasayısal sıralamanın farklı topolojik katmanlar içinde yapılması da göz önünde bulundurulabilir.

Örneğin, F# derleyici hizmeti genel API dosyası için topolojik sıralama aşağıda verilmiştir:

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ırır ve her katman daha sonra alfasayısal olarak sıralanır. Bu, değerleri yanlışlıkla gölgelendirmeden kodu temiz bir şekilde düzenler.

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

Bir değeri başlatmanın, sıklıkla 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 caziptir:

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

Bu durum genellikle birkaç nedenden dolayı sorunludur:

İlk olarak, uygulama yapılandırması dep1 ve dep2 ile kod tabanına entegre ediliyor. Bunun daha büyük kod temellerinde tutulması zordur.

İkinci olarak, statik olarak başlatılmış verilerin, bileşeniniz birden çok iş parçacığı kullanacaksa iş parçacığı güvenli olmayan değerleri içermemesi gerekir. Bu, dep3 tarafından açıkça ihlal edilir.

Son olarak, modül başlatma tüm derleme birimi için statik bir oluşturucuya derlenir. Bu modülde let ile bağlı değer başlatma işleminde herhangi bir hata oluşursa, uygulamanın tüm ömrü boyunca önbelleğe alınan bir TypeInitializationException hata olarak gösterilir. Bunu tanılamak zor olabilir. Genellikle hakkında mantık yürütebileceğiniz içsel bir istisna vardır, ancak böyle bir durum yoksa, kök nedeni belirtmek mümkün olmayabilir.

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

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

Bu, aşağıdakileri etkinleştirir:

  1. Bağımlı durumları API'nin dışına gönderme.
  2. Yapılandırma artık API'nin dışında yapılabilir.
  3. Bağımlı değerler için başlatma hatalarının TypeInitializationException biçiminde ortaya çıkma olasılığı düşük.
  4. API'yi test etmek artık daha kolay.

Hata yönetimi

Büyük sistemlerde hata yönetimi karmaşık ve incelikli bir çabadır ve sistemlerinizin hataya dayanıklı ve iyi çalıştığından emin olmak için mucize çözüm yoktur. Aşağıdaki yönergeler bu zor alanda gezinme konusunda rehberlik sunmalıdır.

Etki alanınıza ait türlerde hata durumlarını ve geçersiz durumu temsil etme

Ayrımcı Birleşimler ile F# size tür sisteminizdeki hatalı program durumunu temsil etme olanağı sağlar. Örneğin:

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 olabileceğinin bilinen üç yolu vardır. Her hata olayı türünde temsil edilir ve bu nedenle program boyunca güvenli bir şekilde ele alınabilir.

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ızda bir şeyin başarısız olabileceği farklı yöntemleri modelleyebilirseniz, hata işleme kodu artık normal program akışına ek olarak ilgilenmeniz gereken bir şey olarak değerlendirilmez. Yalnızca normal program akışının bir parçasıdır ve istisnai olarak kabul edilmez. Bunun iki temel avantajı vardır:

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

Hatalar türlerle temsil edilemediğinde özel durumları kullanma

Tüm hatalar bir sorun etki alanında temsil edilemez. Bu tür hatalar doğası gereği istisnaidir, bu nedenle F# dilinde özel durumlar oluşturup yakalama yeteneği.

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

Özel durum oluşturma amacıyla F# dilinde kullanılabilen ana yapılar aşağıdaki tercih sırasına göre değerlendirilmelidir:

İşlev Sözdizimi Amaç
nullArg nullArg "argumentName" Belirtilen bağımsız değişken adıyla bir System.ArgumentNullException oluşturur.
invalidArg invalidArg "argumentName" "message" Belirtilen bağımsız değişken adı ve iletisiyle bir System.ArgumentException oluşturur.
invalidOp invalidOp "message" Belirtilen mesajla bir System.InvalidOperationException tetikler.
raise raise (ExceptionType("message")) Özel durumlar oluşturmak için genel amaçlı mekanizma.
failwith failwith "message" Belirtilen iletiyle bir System.Exception tetikler.
failwithf failwithf "format string" argForFormatString biçim dizesi ve onun girişleri tarafından belirlenen bir ileti ile bir System.Exception oluşturur.

Uygun olduğunda nullArg, invalidArg ve invalidOp mekanizması olarak ArgumentNullException, ArgumentException ve InvalidOperationException kullanın.

failwith ve failwithf işlevleri genellikle, belirli bir özel durum değil, temel Exception türü yükselttiğinden kaçınılmalıdır. Özel Durum Tasarım Yönergeleri'ne göre, daha belirli özel durumlar tetikleyebilirsiniz.

Özel durum işleme söz dizimlerini 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

Eğer kodu temiz tutmak istiyorsanız, bir özel durum karşısında işlevselliği sağlamak amacıyla desen eşleştirme kullanmak biraz karmaşık olabilir. Bu durumu ele almanın bir yolu, bir hata durumunu bir istisna ile çevreleyen işlevselliği gruplandırmak için etkin desenleri kullanmaktır. Örneğin, özel durum oluştururken değerli bilgileri özel durum meta verilerine alan bir API'yi tüketiyor olabilirsiniz. Etkin Desen içerisindeki yakalanan özel durumun gövdesindeki yararlı bir değeri açığa çıkararak bu değeri döndürmek bazı durumlarda faydalı olabilir.

İstisnaları değiştirmek için monadik hata yönetimini kullanmayın

Özel durumlar genellikle saf işlevsel paradigmada tabu olarak görülür. Aslında, istisnalar saflığı ihlal eder, bu nedenle onları tamamen işlevsel saf değil olarak değerlendirmek güvenlidir. Ancak bu, kodun nerede çalıştırılması gerektiğinin gerçekliğini yoksayar ve çalışma zamanı hataları oluşabilir. Genel olarak, hoş olmayan sürprizleri en aza indirmek için, kod yazarken çoğu şeyin saf veya tam olmadığını varsayın (C# dilinde boş catch'e benzer veya yığın izlemesini yanlış yönetip bilgileri yok etmek gibi).

Bir bütün olarak .NET çalışma zamanı ve diller arası ekosistemdeki ilgi ve uygunluk açısından Özel Durumların aşağıdaki temel güçlü yönlerini/yönlerini göz önünde bulundurmak önemlidir:

  • Bunlar, bir sorunda hata ayıklama sırasında yararlı olan ayrıntılı tanılama bilgileri içerir.
  • Çalışma zamanı ve diğer .NET dilleri tarafından iyi anlaşılır.
  • Bir kısmının anlamlarını amaca yönelik uygulayarak, hatalardan kaçınmak için özel olarak yazılmış kodlarla karşılaştırıldığında önemli ölçüde tekrarlayan kod miktarını azaltabilirler.

Bu üçüncü nokta kritik öneme sahiptir. Karmaşık işlemlerde, özel durumların kullanılmaması aşağıdaki gibi yapıların ele alınmasına yol açabilir:

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

Bu, "dizeli yazılan" hatalarda desen eşleştirme gibi kırılgan kodlara kolayca yol açabilir:

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?

Ayrıca, "daha güzel" bir tür döndüren "basit" bir işlev isteğiyle herhangi bir istisnayı görmezden gelmek cazip olabilir.

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

Ne yazık ki, tryReadAllText bir dosya sisteminde gerçekleşen çok sayıda şeye bağlı olarak çok sayıda özel durum oluşturabilir ve bu kod, ortamınızda gerçekten neyin yanlış gittiğiyle ilgili tüm bilgileri atar. Bu kodu daha uygun bir sonuç tipi ile değiştirirseniz, string tabanlı hata iletisi ayrıştırmasına geri dönersiniz.

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

Özel durum nesnesinin kendisini oluşturucuya Error yerleştirmek, sizi işlev yerine çağrı sitesinde özel durum türüyle düzgün şekilde ilgilenmeye zorlar. Bunu yapmak, bir API'nin çağıranı olarak ilgilenmek için kötü şöhretli olmayan denetlenmiş özel durumlar oluşturur.

Yukarıdaki örneklere iyi bir alternatif, belirli özel durumları yakalamak ve bu özel durum bağlamında anlamlı bir değer döndürmektir. İşlevi tryReadAllText aşağıdaki gibi değiştirirseniz, None daha fazla anlamı vardır:

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

Bu işlev, genel bir hata işleme işlevi yerine, artık dosya bulunamadığında durumu doğru bir şekilde işleyecek ve bu durumu bir dönüş değeri olarak atayacaktır. Bu dönüş değeri, bağlamsal bilgileri koruyarak ve çağıranları, kodda o anda ilgili olmayabilecek bir durumla ilgilenmeye zorlamadan bu hata olayıyla eşlenebilir.

gibi Result<'Success, 'Error> türler iç içe yerleştirilmeyen temel işlemler için uygundur ve isteğe bağlı F# türleri, bir şeyin ne zaman bir şey döndürebileceğini veya hiçbir şey döndürmeyebileceğini göstermek için mükemmeldir. Ancak bunlar özel durumların yerini almayabilir ve özel durumları değiştirme girişiminde kullanılmamalıdır. Bunun yerine, özel durum ve hata yönetimi ilkesinin belirli yönlerini hedeflenen yollarla ele almak için bu uygulamalar özenle uygulanmalıdır.

Kısmi uygulama ve noktasız programlama

F# kısmi uygulamayı ve dolayısıyla noktasız stilde programlamanın çeşitli yollarını destekler. Bu, bir modül içinde kod yeniden kullanımı veya bir şeyin uygulanması için yararlı olabilir, ancak genel kullanıma sunulacak bir şey değildir. Genel olarak, noktasız programlama kendi içinde ve kendi içinde bir erdem değildir ve stile dalmış olmayan kişiler için önemli bir bilişsel engel oluşturabilir.

Genel API'lerde kısmi uygulama ve "currying" yöntemini kullanmayın.

Çok az istisna dışında, kısmi uygulamanın genel API'lerde kullanılması tüketiciler için kafa karıştırıcı olabilir. letF# kodundaki -bound değerleri genellikle işlev değerleri değil değerlerdir. Değerleri ve işlev değerlerini bir araya getirmek, özellikle de işlev oluşturma gibi >> işleçlerle birleştirildiğinde, oldukça fazla bilişsel ek yük karşılığında birkaç kod satırı kaydedilmesine neden olabilir.

Noktasız programlama için araç etkilerini göz önünde bulundurun

Curried işlevleri bağımsız değişkenlerini etiketlemez. Bu, araç gereçlerle ilgili etkileri içerir. Aşağıdaki iki işlevi göz önünde bulundurun:

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 functionlardır, ancak funcWithApplication curried bir functiondır. Düzenleyicide türlerinin üzerine geldiğinizde şunu görürsünüz:

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

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

Çağrı noktasında, Visual Studio gibi araçlarda araç ipuçları size tür imzasını verir, ancak tanımlı adlar olmadığından bu adları görüntülemez. Adlar iyi API tasarımı açısından kritik öneme sahiptir çünkü arayanların API'nin arkasındaki anlamı daha iyi anlamalarına yardımcı olur. Genel API'de noktasız kod kullanmak, çağıranların anlamasını zorlaştırabilir.

Genel kullanıma açık olan funcWithApplication gibi point-free kodlarla karşılaşırsanız, araçların bağımsız değişkenler için anlamlı adlar alabilmesi amacıyla tam bir η-expansion yapmanız önerilir.

Ayrıca, noktasız yazım tarzındaki kodlarda hata ayıklamak zor, hatta bazen imkansız olabilir. Hata ayıklama araçları, yürütmenin ortasında ara değerleri inceleyebilmeniz için adlara (örneğin bağlamalara let ) bağlı değerleri kullanır. Kodunuzun denetlenecek değeri olmadığında, hata ayıklamak için hiçbir şey yoktur. Gelecekte, hata ayıklama araçları bu değerleri daha önce yürütülen yollara göre sentezlemek için gelişebilir, ancak olası hata ayıklama işlevselliğine ilişkin bahislerinizi hedge etmek iyi bir fikir değildir.

Kısmi uygulamayı iç gereksiz kodu azaltma tekniği olarak değerlendirin.

Önceki noktadan farklı olarak, kısmi uygulama, bir uygulamanın içinde veya bir API'nin daha derin iç yapılarında tekrarlayan kodları azaltmak için harika bir araçtır. Daha karmaşık API'lerin uygulanması sırasında yinelenen kodlar genellikle uğraşılması gereken sıkıcı bir sorundur, bu yüzden birim testi için yararlı olabilir. Örneğin, aşağıdaki kod, çoğu mock framework'ün sunduklarını, böyle bir framework'e dışa bağımlı olmadan ve ilgili özel bir API'yi öğrenmek zorunda kalmadan nasıl gerçekleştirebileceğinizi gösterir.

Örneğin, aşağıdaki çözüm topografisini göz önünde bulundurun:

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

ImplementationLogic.fsproj aşağıdakiler gibi kodu kullanıma sunar:

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

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

'da Transactions.doTransaction birim testi ImplementationLogic.Tests.fsproj kolaydır:

namespace TransactionsTestingUtil

open Transactions

module TransactionsTestable =
    let getTestableTransactionRoutine mockContext = Transactions.doTransaction mockContext

Bir sahte bağlam nesnesiyle kısmen uygulamak doTransaction , her seferinde sahte bir bağlam oluşturmak zorunda kalmadan tüm birim testlerinizde işlevi çağırmanıza olanak tanır:

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 tüm kod tabanınıza evrensel olarak uygulamayın, ancak karmaşık iç bileşenler ve bu bileşenlerin birim testi için tekrarlanan kodu azaltmanın iyi bir yoludur.

Erişim denetimi

F# erişim denetimi için birden çok seçeneğe sahiptir ve .NET çalışma zamanındaki kullanılabilir seçeneklerden devralınır. Bunlar yalnızca türler için kullanılamaz; işlevler için de kullanabilirsiniz.

Yaygın olarak kullanılan kitaplıklar bağlamında iyi uygulamalar:

  • Genel olarak kullanılabilir olmalarına ihtiyaç duyana kadar türleri ve üyeleri genel olmayan türlerden tercih edin. Bu aynı zamanda tüketicilerin hangi şeylere bağlandığını da azaltır.
  • Tüm yardımcı işlevselliğini privatekorumak için çaba gösterin.
  • Birçok yardımcı işlev haline geldiklerinde [<AutoOpen>] kullanımını özel bir modülde değerlendirin.

Tür çıkarımı ve genel tipler

Tür çıkarımı, çok fazla şablon kod yazmanızı engelleyebilir. Ayrıca F# derleyicisindeki otomatik genelleştirme, sizin için neredeyse hiç fazla çaba harcamadan daha genel kod yazmanıza yardımcı olabilir. Ancak, bu özellikler evrensel olarak iyi değildir.

  • Bağımsız değişken adlarını genel API'lerde açık türlerle etiketlemeyi göz önünde bulundurun ve bunun için tür çıkarımına güvenmeyin.

    Bunun nedeni, derleyicinin değil API'nizin şeklinin denetiminde olmanız gerekir. Derleyici sizin için türleri çıkarsama konusunda iyi bir iş çıkarasa da, bağlı olduğu iç bileşenler türleri değiştirdiyse API'nizin şeklinin değiştirilmesi mümkündür. İstediğiniz şey bu olabilir, ancak neredeyse kesinlikle sonraki kullanıcıların daha sonra başa çıkması gerekecek olan bozucu bir API değişikliğine yol açar. Bunun yerine, genel API'nizin şeklini açıkça denetlerseniz, bu hataya neden olan değişiklikleri denetleyebilirsiniz. DDD açısından, bu bir Bozulma önleme katmanı olarak düşünülebilir.

  • Genel bağımsız değişkenlerinize anlamlı bir ad vermeyi göz önünde bulundurun.

    Belirli bir etki alanına özgü olmayan gerçekten genel bir kod yazmadığınız sürece, anlamlı bir ad diğer programcıların üzerinde çalıştıkları etki alanını anlamalarına yardımcı olabilir. Örneğin, belge veritabanıyla etkileşim kurma bağlamında adlı 'Document bir tür parametresi, genel belge türlerinin çalıştığınız işlev veya üye tarafından kabul edilebileceğini daha net hale getirir.

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

    .NET'te işlem yapmak için genel yöntem budur, bu nedenle snake_case veya camelCase yerine PascalCase kullanmanız önerilir.

Son olarak otomatik genelleştirme, F# veya büyük bir kod tabanında yeni olan kişiler için her zaman bir boon değildir. Genel bileşenlerin kullanılmasında bilişsel ek yük vardır. Ayrıca, otomatik olarak genelleştirilmiş işlevler farklı giriş türleriyle kullanılmıyorsa (bu şekilde kullanılması amaçlanıyorsa bırakın), genel olmalarının gerçek bir avantajı yoktur. Yazdığınız kodun genel olmanın gerçekten yararlı olup olmadığını her zaman göz önünde bulundurun.

Performans

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

Yapıların (Değer Türleri olarak da adlandırılır) kullanılması genellikle nesneleri ayırmayı önlediğinden bazı kodlarda daha yüksek performansa neden olabilir. Ancak yapılar her zaman "daha hızlı git" düğmesi değildir: Yapıdaki verilerin boyutu 16 baytı aşarsa, verilerin kopyalanması genellikle başvuru türünü kullanmaktan daha fazla CPU süresi harcaması ile sonuçlanabilir.

Bir yapı kullanmanız gerekip gerekmediğini belirlemek için aşağıdaki koşulları göz önünde bulundurun:

  • Verilerinizin boyutu 16 bayt veya daha küçükse.
  • Çalışan bir programda bellekte bu türlerin birçok örneğinin yerleşik olarak yer almış olma olasılığınız varsa.

İlk koşul geçerliyse, genellikle bir yapı kullanmanız gerekir. Her ikisi de geçerliyse, neredeyse her zaman bir yapı kullanmanız gerekir. Önceki koşulların geçerli olduğu bazı durumlar olabilir, ancak yapı kullanmak başvuru türünü kullanmaktan daha iyi veya daha kötü değildir, ancak bunlar nadir olabilir. Ancak bu gibi değişiklikler yaparken her zaman ölçüm yapmak ve varsayım veya sezgi üzerinde çalışmamak önemlidir.

Yüksek ayırma oranlarına sahip küçük değer türlerini gruplandırırken yapı demetlerini göz önünde bulundurun

Aşağıdaki iki işlevi göz önünde bulundurun:

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 BenchmarkDotNet gibi bir istatistiksel karşılaştırma aracıyla kıyasladığınızda, yapı demetlerini kullanan işlevin runWithStructTuple %40 daha hızlı çalıştığını ve bellek ayırmadığını göreceksiniz.

Ancak bu sonuçlar her zaman kendi kodunuzda geçerli olmaz. Eğer bir fonksiyonu inline olarak işaretlerseniz, referans demetlerini kullanan kod bazı ek iyileştirmeler alabilir ya da bellek ayıracak olan kod basitçe optimize edilebilir. Performans söz konusu olduğunda sonuçları her zaman ölçmeli ve hiçbir zaman varsayıma veya sezgiye göre çalışmamalısınız.

Türün boyutu küçük olduğunda ve ayırma oranları yüksek olduğunda yapı kayıtlarını dikkate alın.

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

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 tuple koduna benzer, ancak bu kez örnek, kayıt ve satır içi bir iç işlev kullanır.

Bu işlevleri BenchmarkDotNet gibi istatistiksel bir karşılaştırma aracıyla kıyasladığınızda, neredeyse %60 daha hızlı çalıştığını ve yönetilen yığında hiçbir şey ayırmadığını processStructPoint fark edeceksiniz.

Veri türü küçük ve yüksek ayırma oranlarına sahipse, struct ayrışımlı birlikleri göz önünde bulundurun.

Yapı tanımlama demetleri ve kayıtlarıyla performansla ilgili önceki gözlemler F# Ayrımcı Birleşimler iç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

Etki alanı modellemesi için bunun gibi tek durumlu Ayrımcı Birleşimler tanımlamak yaygın bir durumdır. Bu işlevleri BenchmarkDotNet gibi bir istatistiksel karşılaştırma aracıyla karşılaştırdığınızda, küçük dizeler için structReverseName'in reverseName'den yaklaşık %25 daha hızlı çalıştığını göreceksiniz. Büyük dizeler için her ikisi de yaklaşık olarak aynı performansı gösterir. Bu durumda, bir yapı kullanmak her zaman tercih edilir. Daha önce belirtildiği gibi, her zaman ölçün ve varsayımlar veya sezgiler üzerinde çalışmayın.

Önceki örnekte Ayrımcı Birleşim yapısının daha iyi performans gösterdiği gösterilmiş olsa da, bir etki alanı modellendiğinde daha büyük Ayrımcı Birleşimler olması yaygın bir durumdur. Bunun gibi daha büyük veri türleri, üzerlerindeki işlemlere bağlı olarak yapı olmaları durumunda, daha fazla kopyalama söz konusu olabileceğinden daha iyi performans göstermeyebilir.

Değişmezlik ve mutasyon

F# değerleri varsayılan olarak sabittir ve bu da belirli hata sınıflarından (özellikle eşzamanlılık ve paralellik içeren sınıflardan) kaçınmanızı sağlar. Ancak, bazı durumlarda, yürütme süresi veya bellek ayırmalarında en uygun (veya hatta makul) verimliliği elde etmek için, bir çalışma aralığı en iyi şekilde yerinde durum mutasyonu kullanılarak uygulanabilir. Bu, F# dilinde mutable anahtar sözcüğüyle isteğe bağlı olarak mümkündür.

F# dilinde mutable kullanımı, işlevsel saflıkla çelişiyor gibi gelebilir. Bu anlaşılabilir bir durumdur, ancak her yerde işlevsel saflık performans hedefleriyle çelişebilir. Uzlaşma, çağıranların işlev çağırdığında ne olacağıyla ilgilenmemesi için mutasyonu kapsüllemektir. Bu, performans açısından kritik kod için mutasyon tabanlı bir uygulama üzerinde işlevsel bir arabirim yazmanızı sağlar.

Ayrıca F# let bağlama yapıları, bağlamaları başka bir bağlamaya yerleştirmenize olanak tanır. Bu, değişkenin mutable kapsamını yakın veya teorik olarak en küçük düzeyde tutmak için kullanılabilir.

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

Hiçbir kod, data let ile bağlı olan değeri başlatmak için kullanılan değiştirilebilir completed'a erişemez.

Sabit arabirimlerde değiştirilebilir kodu sarmalama

Amaç olarak referans şeffaflığı ile, performans açısından kritik işlevlerin değiştirilebilir temelini kullanıma sunmayan kod yazmak kritik önem arz eder. Örneğin, aşağıdaki kod Array.contains işlevini 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şlevin birden çok kez çağrılması, altta yatan diziyi değiştirmez ve bunu kullanırken herhangi bir değiştirilebilir durumu korumanızı gerektirmez. İçinde hemen her kod satırı mutasyon kullansa da, referans olarak saydamdır.

Sınıflarda değişken veriyi kapsüllemeyi göz önünde bulundurun.

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

Bu kod yüksek performanslıdır, ancak çağıranların korumakla sorumlu olduğu mutasyona dayalı veri yapısını ortaya çıkarır. Bu, değiştirebilecek temel üyeleri olmayan bir sınıfın içinde 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 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 temel alınan mutasyona dayalı veri yapısını kapsüller, böylece çağıranları temel alınan veri yapısını korumaya zorlamaz. Sınıflar, ayrıntıları arayanlara göstermeden mutasyon tabanlı verileri ve rutinleri kapsüllemenin güçlü bir yoludur.

let mutable yerine ref tercih et

Başvuru hücreleri, değerin kendisi yerine bir değere yapılan başvuruyu temsil etmenin bir yoludur. Performans açısından kritik kod için kullanılabilseler de, 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

Bir referans hücresinin kullanımı, temel alınan verilere başvurulması ve yeniden başvurulması gerekliliği ile sonraki tüm kodları "kirletir". Bunun yerine, 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ındaki tek mutasyon noktasının yanı sıra, acc’ye dokunan diğer tüm kodlar, bunu, normal let bağlı sabit bir değerin kullanımından farklı olmayan bir şekilde yapabilir. Bu, zaman içinde değişmeyi kolaylaştırır.

Null değerler ve varsayılan değerler

F# dilinde null değerlerden genellikle kaçınılmalıdır. Varsayılan olarak F# tarafından bildirilen türler literal null kullanımını desteklemez ve tüm değerler ve nesneler başlatılır. Ancak, bazı yaygın .NET API'leri null değer döndürür veya kabul eder ve diziler ve stringler gibi bazı yaygın .NET tarafından bildirilen türler null değerlere izin verir. Ancak, F# programlamada değerlerin null ortaya çıkması çok nadirdir ve F# kullanmanın avantajlarından biri çoğu durumda null başvuru hatalarından kaçınmaktır.

özniteliğini kullanmaktan AllowNullLiteral kaçının

Varsayılan olarak F# tarafından bildirilen türler null sabit değerinin kullanımını desteklemez. Buna izin vermek için F# türlerine el ile AllowNullLiteral açıklama ekleyebilirsiniz. Ancak, bunu yapmaktan kaçınmak neredeyse her zaman daha iyidir.

özniteliğini kullanmaktan Unchecked.defaultof<_> kaçının

Bir F# türü için null veya sıfır başlatılan bir değer oluşturmak Unchecked.defaultof<_> kullanarak mümkündür. Bu, bazı veri yapıları için depolamayı başlatırken veya yüksek performanslı bir kodlama düzeninde veya birlikte çalışabilirlik içinde yararlı olabilir. Ancak bu yapıyı kullanmaktan kaçınılmalıdır.

özniteliğini kullanmaktan DefaultValue kaçının

Varsayılan olarak, F# kayıtları ve nesneleri oluşturma sırasında düzgün şekilde başlatılmalıdır. DefaultValue özniteliği, nesne alanlarının bazılarını null değeri veya sıfır başlatılmış bir değerle doldurmak için kullanılabilir. Bu yapıya nadiren ihtiyaç duyulmaktadır ve kullanımından kaçınılmalıdır.

Null girişleri denetlerseniz, ilk fırsatta istisna oluşturun.

Yeni F# kodu yazarken, kodun C# veya diğer .NET dillerinden kullanılmasını beklemediğiniz sürece uygulamada null girişleri denetlemeniz gerekmez.

Null girişler için denetimler eklemeye karar verirseniz, denetimleri ilk fırsatta gerçekleştirin ve ardından bir istisna oluşturun. Örneğin:

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

Eski nedenlerden dolayı, FSharp.Core'daki belirli dize işlevleri null değerleri boş dizeler gibi ele alır ve null bağımsız değişkenlerde başarısızlık göstermez. Ancak bunu kılavuz olarak almayın ve herhangi bir anlamsal anlamı "null" ile ilişkilendiren kodlama desenlerini benimsemeyin.

API sınırlarında F# 9 null söz dizimini kullanma

F# 9, bir değerin null olabileceğini açıkça belirtmek için söz dizimi ekler. Derleyicinin null işlemenin eksik olduğu yerleri belirtmesini sağlamak için API sınırlarında kullanılacak şekilde tasarlanmıştır.

Aşağıda söz diziminin geçerli kullanımına bir örnek verilmiştir:

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)

F# kodunuzda null değerlerin daha fazla yayılmasını önleyin.

let getLineFromStream (stream: System.IO.StreamReader) : string | null =
    stream.ReadLine()

Bunun yerine, idiomatik F# yollarını kullanın (örneğin, seçenekler):

let getLineFromStream (stream: System.IO.StreamReader) =
    stream.ReadLine() |> Option.ofObj

Null ile ilgili özel durumlar oluşturmak için özel nullArgCheck ve nonNull işlevleri kullanabilirsiniz. Ayrıca, değer null olmadığında, bağımsız değişkeni temizlenmiş değeriyle örtmeleri nedeniyle de kullanışlıdırlar; böylece, daha sonraki kod artık olası null işaretçilere erişemez.

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

Nesne programlama

F# nesneleri ve nesne odaklı (OO) kavramları için tam desteğe sahiptir. Birçok OO kavramı güçlü ve kullanışlı olsa da, bunların tümü kullanmak için ideal değildir. Aşağıdaki listelerde, yüksek düzeyde OO özellikleri kategorileri hakkında rehberlik sunulmaktadır.

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

  • Nokta gösterimi (x.Length)
  • Örnek üyeleri
  • İmplicit oluşturucular
  • Statik üyeler
  • Dizin gösterimi (arr[x]), bir Item özelliği tanımlayarak
  • arr[x..y] dilimleme gösterimi (arr[x..], arr[..y], GetSlice), üyeleri tanımlayarak
  • Adlandırılmış ve İsteğe Bağlı argümanlar
  • Arabirimler ve arabirim uygulamaları

Önce bu özelliklere erişmeyin, ancak bir sorunu çözmek için uygun olduklarında bunları mantıklı bir şekilde uygulayın:

  • Yöntem aşırı yükleme
  • Kapsüllenmiş değiştirilebilir veriler
  • Türlerdeki işleçler
  • Otomatik özellikler
  • uygulama IDisposable ve IEnumerable
  • Tür uzantıları
  • Ekinlikler
  • Yapılar
  • Temsilciler
  • Numaralandırmalar

Bu özellikleri kullanmanız gerekmediği sürece genellikle bu özelliklerden kaçının:

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

Birleştirmeyi devralma yerine tercih et

Kompozisyon, kalıtıma tercih edilir, iyi F# kodunun bağlı kalabileceği uzun süreli bir yazılım geliştirme prensibidir. Temel ilke, bir temel sınıfı kullanıma sunmamalı ve işlev elde etmek için çağıranları bu temel sınıftan devralmaya zorlamamalısınız.

Sınıfa ihtiyacınız yoksa arabirimleri uygulamak için nesne ifadelerini kullanma

Nesne İfadeleri , bir sınıfın içinde bunu yapmanıza gerek kalmadan uygulanan arabirimi bir değere bağlayarak arabirimleri anında uygulamanıza olanak sağlar. Bu, özellikle yalnızca arabirimi uygulamanız gerekiyorsa ve tam bir sınıfa ihtiyacınız yoksa kullanışlıdır.

Örneğin, bir simge eklediyseniz ve bunun için bir open deyiminiz yoksa, kod düzeltme eylemi sağlamak amacıyla Ionide içinde çalıştırılan kod aşağıda verilmiştir:

    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 API'siyle etkileşim kurarken sınıfa gerek olmadığından, Nesne İfadeleri bunun için ideal bir araçtır. Ayrıca, test yordamlarına sahip bir arabirimi doğaçlama bir şekilde saptamak istediğinizde birim testi için de değerlidir.

İmzaları kısaltmak için Tür Kısaltmalarını göz önünde bulundurun

Tür Kısaltmaları , işlev imzası veya daha karmaşık bir tür gibi başka bir türe etiket atamanın kullanışlı bir yoludur. Örneğin, aşağıdaki takma ad, bir derin öğrenme kitaplığı olan CNTK ile bir hesabı tanımlamak için gerekenlere bir etiket atar.

open CNTK

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

Computation adı, diğer adla eşleşen işlevlerin imzasını belirtmek için kullanışlı bir yöntemdir. Bunun gibi Tür Kısaltmaları kullanmak kullanışlıdır ve daha kısa kod sağlar.

Etki alanınızı temsil etmek için Tür Kısaltmaları kullanmaktan kaçının

Tür Kısaltmaları işlev imzalarına ad vermek için kullanışlı olsa da, diğer türleri kısaltırken kafa karıştırıcı olabilir. Şu kısaltmayı göz önünde bulundurun:

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

Bu, çeşitli yollarla kafa karıştırıcı olabilir:

  • BufferSize soyutlama değildir; bir tamsayı için yalnızca başka bir addır.
  • Genel API'de kullanıma sunulursa BufferSize , yalnızca int'den daha fazlasını ifade etmek için kolayca yanlış yorumlanabilir. Genel olarak, etki alanı türlerinin birçok özniteliği bulunur ve int gibi ilkel türler değildir. Bu kısaltma bu varsayımı ihlal eder.
  • PascalCase yazım kuralı, bu türün daha fazla veri barındırdığını gösterir.
  • Bu diğer ad, işleve adlandırılmış bağımsız değişken sağlamaya kıyasla daha fazla netlik sunmaz.
  • Kısaltma derlenmiş IL'de gösterilmez; yalnızca bir tamsayıdır ve bu takma ad derleme zamanında oluşturulan bir yapıdır.
module Networking =
    ...
    let send data (bufferSize: int) = ...

Özetle, Tür Kısaltmaları ile ilgili tuzak, kısalttıkları türlerin soyutlaması olmamalarıdır. Önceki örnekte, BufferSize, gizli bir int'dir, ek veri içermez ve int'nin zaten sahip oldukları dışında tür sisteminden herhangi bir fayda sağlamaz.

Etki alanını temsil etmek için tür kısaltmaları kullanmanın alternatif bir yaklaşımı, tek seçenekli ayrık birleşimler kullanmaktır. Önceki örnek aşağıdaki gibi modellenebilir:

type BufferSize = BufferSize of int

Kodun BufferSize ve onun temel değeri ile çalışmasını sağlamak istiyorsanız, rastgele bir tamsayı geçirmek yerine bir örnek oluşturmanız gerekmektedir.

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

Çağıranın işlevi çağırmadan önce bir değeri sarmalayan bir send tür oluşturması gerektiğinden, bu işleve yanlışlıkla rastgele bir tamsayı BufferSize geçirme olasılığını azaltır.