F# kodlama kurallarıF# coding conventions

Aşağıdaki kurallar, büyük F # codetabanlarla çalışma deneyiminden alınmıştır.The following conventions are formulated from experience working with large F# codebases. Her bir önerinin temelini, Iyi F # kodu olan beş ilkeden oluşur.The Five principles of good F# code are the foundation of each recommendation. Bunlar f # bileşen tasarım yönergeleriyleilgilidir, ancak yalnızca kitaplıklar gibi bileşenler değil herhangi bir f # kodu için geçerlidir.They are related to the F# component design guidelines, but are applicable for any F# code, not just components such as libraries.

Kodu düzenlemeOrganizing code

F # kodu düzenlemenin iki birincil yolunu sunar: modüller ve ad alanları.F# features two primary ways to organize code: modules and namespaces. Bunlar benzerdir, ancak aşağıdaki farklılıklara sahiptir:These are similar, but do have the following differences:

  • Ad alanları .NET ad alanları olarak derlenir.Namespaces are compiled as .NET namespaces. Modüller statik sınıflar olarak derlenir.Modules are compiled as static classes.
  • Ad alanları her zaman en üst düzeydir.Namespaces are always top level. Modüller en üst düzey ve diğer modüller içinde iç içe olabilir.Modules can be top-level and nested within other modules.
  • Ad alanları birden çok dosyaya yayılabilir.Namespaces can span multiple files. Modüller.Modules cannot.
  • Modüller ve ile donatılmış olabilir [<RequireQualifiedAccess>] [<AutoOpen>] .Modules can be decorated with [<RequireQualifiedAccess>] and [<AutoOpen>].

Aşağıdaki yönergeler kodunuzu düzenlemek için bunları kullanmanıza yardımcı olur.The following guidelines will help you use these to organize your code.

En üst düzeyde ad alanlarını tercih etPrefer namespaces at the top level

Genel kullanım düzeylerine sahip herhangi bir kod için, ad alanları en üst düzeydeki modüllerle tercihe göre yapılır.For any publicly consumable code, namespaces are preferential to modules at the top level. .NET ad alanları olarak derlendiğinden, hiçbir sorun olmadan C# ' den tüketilebilir.Because they are compiled as .NET namespaces, they are consumable from C# with no issue.

// Good!
namespace MyCode

type MyClass() =
    ...

En üst düzey modülün kullanılması yalnızca F # ' dan çağrıldığında farklı görünmeyebilir, ancak C# tüketicileri için, bu, modüle uygun hale getirmek zorunda kalmadan çağıranlar olabilir MyClass MyCode .Using a top-level module may not appear different when called only from F#, but for C# consumers, callers may be surprised by having to qualify MyClass with the MyCode module.

// Bad!
module MyCode

type MyClass() =
    ...

Dikkatle Uygula[<AutoOpen>]Carefully apply [<AutoOpen>]

[<AutoOpen>]Yapı, çağıranlar için kullanılabilir olan kapsamı ve bir şeyin geldiği yanıtın "Magic" olduğunu pollute olabilir.The [<AutoOpen>] construct can pollute the scope of what is available to callers, and the answer to where something comes from is "magic". Bu iyi bir şey değildir.This is not a good thing. Bu kural için bir özel durum, F # Çekirdek kitaplığının kendisidir (Bu olgu aynı zamanda bir bit controversıal).An exception to this rule is the F# Core Library itself (though this fact is also a bit controversial).

Ancak, bu genel API 'den ayrı olarak düzenlemek istediğiniz ortak API için yardımcı işlevselliğe sahipseniz kolaylık vardır.However, it is a convenience if you have helper functionality for a public API that you wish to organize separately from that public API.

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ıyı tam olarak nitelendirmek zorunda kalmadan, bir işlevin ortak API 'sinden uygulama ayrıntılarını düzgün bir şekilde ayırmanızı sağlar.This lets you cleanly separate implementation details from the public API of a function without having to fully qualify a helper each time you call it.

Ek olarak, ad alanı düzeyinde uzantı yöntemlerinin ve ifade oluşturucuların kullanıma sunulması ile birlikte ifade edilebilir [<AutoOpen>] .Additionally, exposing extension methods and expression builders at the namespace level can be neatly expressed with [<AutoOpen>].

[<RequireQualifiedAccess>]Adların ne zaman çakıştığını veya okunabilirlik konusunda yardımcı olduğunu düşünüyorsanız kullanınUse [<RequireQualifiedAccess>] whenever names could conflict or you feel it helps with readability

[<RequireQualifiedAccess>]Özniteliği bir modüle eklemek, modülün açılamayabilir ve modülün öğelerine yapılan başvuruların açık nitelikli erişim gerektirdiğini gösterir.Adding the [<RequireQualifiedAccess>] attribute to a module indicates that the module may not be opened and that references to the elements of the module require explicit qualified access. Örneğin, Microsoft.FSharp.Collections.List modülün bu özniteliği vardır.For example, the Microsoft.FSharp.Collections.List module has this attribute.

Bu, modüldeki işlevlerin ve değerlerin diğer modüllerdeki adlarla çakışabilecek adlara sahip olduğu durumlarda faydalıdır.This is useful when functions and values in the module have names that are likely to conflict with names in other modules. Tam erişim gerektirmek, bir kitaplığın uzun süreli bakım ve gelişmesini büyük ölçüde artırabilir.Requiring qualified access can greatly increase the long-term maintainability and evolvability of a library.

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

...

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

Sort open deyimleri topologicallySort open statements topologically

F # ' da, deyimler dahil olmak üzere bildirimlerin sırası önemlidir open .In F#, the order of declarations matters, including with open statements. Bu, using ve efektinin bir dosyadaki deyimlerin sıralarından bağımsız olduğu C# ' nin aksine using static .This is unlike C#, where the effect of using and using static is independent of the ordering of those statements in a file.

F # ' da, bir kapsamda açılan öğeler başkalarının daha önceden var olan öğelerini gölgelendirebilir.In F#, elements opened into a scope can shadow others already present. Bu, yeniden sıralama open deyimlerinin kodun anlamını değiştirebilecek anlamına gelir.This means that reordering open statements could alter the meaning of code. Sonuç olarak, tüm open deyimlerin (örneğin, alfasayısal) herhangi bir rastgele sıralanması önerilmez, en uzun işlem, bekleneceğiniz farklı davranışlar oluşturur.As a result, any arbitrary sorting of all open statements (for example, alphanumerically) is not recommended, lest you generate different behavior that you might expect.

Bunun yerine, topologically; diğer bir deyişle, open deyimlerinizi sisteminizin katmanlarının tanımlandığı sırada sıralayın.Instead, we recommend that you sort them topologically; that is, order your open statements in the order in which layers of your system are defined. Farklı topolojik katmanlarda Alfasayısal sıralama yapmak de göz önünde bulundurulmayabilir.Doing alphanumeric sorting within different topological layers may also be considered.

Örnek olarak, F # derleyicisi hizmeti ortak API dosyası için topik sıralama aşağıda verilmiştir:As an example, here is the topological sorting for the F# compiler service public API file:

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 Microsoft.FSharp.Compiler
open Microsoft.FSharp.Compiler.AbstractIL
open Microsoft.FSharp.Compiler.AbstractIL.Diagnostics
open Microsoft.FSharp.Compiler.AbstractIL.IL
open Microsoft.FSharp.Compiler.AbstractIL.ILBinaryReader
open Microsoft.FSharp.Compiler.AbstractIL.Internal
open Microsoft.FSharp.Compiler.AbstractIL.Internal.Library

open Microsoft.FSharp.Compiler.AccessibilityLogic
open Microsoft.FSharp.Compiler.Ast
open Microsoft.FSharp.Compiler.CompileOps
open Microsoft.FSharp.Compiler.CompileOptions
open Microsoft.FSharp.Compiler.Driver
open Microsoft.FSharp.Compiler.ErrorLogger
open Microsoft.FSharp.Compiler.Infos
open Microsoft.FSharp.Compiler.InfoReader
open Microsoft.FSharp.Compiler.Lexhelp
open Microsoft.FSharp.Compiler.Layout
open Microsoft.FSharp.Compiler.Lib
open Microsoft.FSharp.Compiler.NameResolution
open Microsoft.FSharp.Compiler.PrettyNaming
open Microsoft.FSharp.Compiler.Parser
open Microsoft.FSharp.Compiler.Range
open Microsoft.FSharp.Compiler.Tast
open Microsoft.FSharp.Compiler.Tastops
open Microsoft.FSharp.Compiler.TcGlobals
open Microsoft.FSharp.Compiler.TypeChecker
open Microsoft.FSharp.Compiler.SourceCodeServices.SymbolHelpers

open Internal.Utilities
open Internal.Utilities.Collections

Bir satır sonu, her katmana daha sonra alfasayısal olarak sıralanmakta olan topik katmanları ayırır.Note that a line break separates topological layers, with each layer being sorted alphanumerically afterwards. Bu, yanlışlıkla gölgeleme değerleri olmadan kodu düzgün bir şekilde düzenler.This cleanly organizes code without accidentally shadowing values.

Yan etkileri olan değerleri içermesi için sınıfları kullanmaUse classes to contain values that have side effects

Bir değerin başlatılması, bir veritabanı veya başka bir uzak kaynak için bir bağlamı örneklendirirken birçok zaman olabilir.There are many times when initializing a value can have side effects, such as instantiating a context to a database or other remote resource. Bu tür şeyleri bir modülde başlatmak ve sonraki işlevlerde kullanmak önemlidir:It is tempting to initialize such things in a module and use it in subsequent functions:

// This is bad!
module MyApi =
    let dep1 = File.ReadAllText "/Users/{your 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, birkaç nedenden dolayı çoğunlukla kötü bir fikir olabilir:This is frequently a bad idea for a few reasons:

İlk olarak, uygulama yapılandırması, ve ile kod tabanına dep1 gönderilir dep2 .First, application configuration is pushed into the codebase with dep1 and dep2. Daha büyük kod tabanlarında bakım yapmak zordur.This is difficult to maintain in larger codebases.

İkinci olarak, bileşen birden çok iş parçacığı kullanıyorsa, statik olarak başlatılan veriler iş parçacığı güvenli olmayan değerler içermemelidir.Second, statically initialized data should not include values that are not thread safe if your component will itself use multiple threads. Bu, tarafından açıkça ihlal edilir dep3 .This is clearly violated by dep3.

Son olarak, modül başlatması tüm derleme birimi için statik bir oluşturucuya derlenir.Finally, module initialization compiles into a static constructor for the entire compilation unit. Bu modülde izin-bağlandeğer başlatma bölümünde herhangi bir hata oluşursa, bu, TypeInitializationException daha sonra uygulamanın kullanım ömrü boyunca önbelleğe alınmış bir olarak bildirim oluşturulur.If any error occurs in let-bound value initialization in that module, it manifests as a TypeInitializationException that is then cached for the entire lifetime of the application. Bunu tanılamak zor olabilir.This can be difficult to diagnose. Genellikle bunun nedenini deneyebilmeniz için bir iç özel durum vardır, ancak yoksa, kök nedenin ne olduğunu söylemez.There is usually an inner exception that you can attempt to reason about, but if there is not, then there is no telling what the root cause is.

Bunun yerine, bağımlılıkları tutmak için basit bir sınıf kullanmanız yeterlidir:Instead, just use a simple class to hold dependencies:

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

Bu, şunları sunar:This enables the following:

  1. Herhangi bir bağımlı durumu API 'nin dışında iletme.Pushing any dependent state outside of the API itself.
  2. Yapılandırma artık API dışında yapılabilir.Configuration can now be done outside of the API.
  3. Bağımlı değerler için başlatma hatası, bir olarak bildirimde bulunma olasılığı yoktur TypeInitializationException .Errors in initialization for dependent values are not likely to manifest as a TypeInitializationException.
  4. API artık test etmek daha kolay.The API is now easier to test.

Hata yönetimiError management

Büyük sistemlerde hata yönetimi karmaşık ve anormal bir Endeavor ve sistemlerinizin hataya dayanıklı ve iyi davranmasını sağlamaya yönelik gümüş bir madde işareti yoktur.Error management in large systems is a complex and nuanced endeavor, and there are no silver bullets in ensuring your systems are fault-tolerant and behave well. Aşağıdaki kılavuzlar, bu zor alanda gezinmek için rehberlik sunmalıdır.The following guidelines should offer guidance in navigating this difficult space.

Etki alanınız için iç türlerde hata durumlarını ve geçersiz durumu temsil ederRepresent error cases and illegal state in types intrinsic to your domain

Ayrılmış birleşimlerile, F #, tür sisteminizde hatalı program durumunu temsil etme olanağı sunar.With Discriminated Unions, F# gives you the ability to represent faulty program state in your type system. Örneğin:For example:

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

Bu durumda, bir banka hesabından paranın bir şekilde çizmesinin başarısız olması için üç bilinen yol vardır.In this case, there are three known ways that withdrawing money from a bank account can fail. Her bir hata durumu, türünde temsil edilir ve bu nedenle program genelinde güvenle ele alınabilir.Each error case is represented in the type, and can thus be dealt with safely throughout the program.

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 %O" expiredDate
    | UndisclosedFailure -> printfn "Failed: unknown"

Genel olarak, etki alanındaki bir şeyin başarısız olmasına yönelik farklı yollar modelleyebilir, daha sonra hata işleme kodu artık normal program akışına ek olarak uğraşmanız gereken bir şey olarak değerlendirilmez.In general, if you can model the different ways that something can fail in your domain, then error handling code is no longer treated as something you must deal with in addition to regular program flow. Yalnızca normal program akışının bir parçasıdır ve olağanüstüolarak kabul edilmez.It is simply a part of normal program flow, and not considered exceptional. Bunun başlıca iki avantajı vardır:There are two primary benefits to this:

  1. Etki alanınız zaman içinde değiştikçe bakım daha kolay olur.It is easier to maintain as your domain changes over time.
  2. Hata durumlarının birim testi daha kolay.Error cases are easier to unit test.

Hatalar türlerle gösterilemeyeceği durumlarda özel durumlar kullanınUse exceptions when errors cannot be represented with types

Hatalar bir sorun etki alanında gösterilemez.Not all errors can be represented in a problem domain. Bu tür hatalar doğası gereği , bu nedenle F # içinde özel durum tetikleyebilme ve yakalayamaz.These kinds of faults are exceptional in nature, hence the ability to raise and catch exceptions in F#.

İlk olarak, özel durum tasarım kılavuzunuokumanız önerilir.First, it is recommended that you read the Exception Design Guidelines. Bunlar F # için de geçerlidir.These are also applicable to F#.

Özel durumları tetikme amaçları için F # ' da kullanılabilen ana yapılar, aşağıdaki tercih sırasına göre düşünülmelidir:The main constructs available in F# for the purposes of raising exceptions should be considered in the following order of preference:

İşlevFunction SözdizimiSyntax AmaçPurpose
nullArg nullArg "argumentName" System.ArgumentNullExceptionBelirtilen bağımsız değişken adına sahip bir oluşturur.Raises a System.ArgumentNullException with the specified argument name.
invalidArg invalidArg "argumentName" "message" System.ArgumentExceptionBelirtilen bağımsız değişken adı ve iletisiyle bir ile başlatır.Raises a System.ArgumentException with a specified argument name and message.
invalidOp invalidOp "message" System.InvalidOperationExceptionBelirtilen iletiyle bir oluşturur.Raises a System.InvalidOperationException with the specified message.
raise raise (ExceptionType("message")) Özel durum oluşturmak için genel amaçlı mekanizma.General-purpose mechanism for throwing exceptions.
failwith failwith "message" System.ExceptionBelirtilen iletiyle bir oluşturur.Raises a System.Exception with the specified message.
failwithf failwithf "format string" argForFormatString , System.Exception Biçim dizesi ve girişleri tarafından belirlenen bir ileti ile başlatır.Raises a System.Exception with a message determined by the format string and its inputs.

nullArg' İ invalidArg ve ' yi invalidOp oluşturmak için gereken mekanizmayı ve ArgumentNullException ArgumentException InvalidOperationException uygun olduğunda kullanın.Use nullArg, invalidArg and invalidOp as the mechanism to throw ArgumentNullException, ArgumentException and InvalidOperationException when appropriate.

failwithVe failwithf işlevlerinin genellikle kaçınılması gerekir Exception , çünkü belirli bir özel durum değil temel türü yükseltir.The failwith and failwithf functions should generally be avoided because they raise the base Exception type, not a specific exception. Özel durum tasarım yönergelerinegöre,, ' yi kullanırken daha özel özel durumlar da yapmak istersiniz.As per the Exception Design Guidelines, you want to raise more specific exceptions when you can.

Özel durum işleme söz dizimini kullanmaUsing exception-handling syntax

F #, sözdizimi aracılığıyla özel durum düzenlerini destekler try...with :F# supports exception patterns via the try...with syntax:

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

Bir özel durumun açık olması için işlevselliği karşılaştırma, kod temizlemeyi sağlamak istiyorsanız biraz karmaşık olabilir.Reconciling functionality to perform in the face of an exception with pattern matching can be a bit tricky if you wish to keep the code clean. Bunu işlemenin bir yolu, etkin desenleri bir özel durum ile bir hata durumu çevreleyen işlevselliği gruplandırmak için bir yol olarak kullanmaktır.One such way to handle this is to use active patterns as a means to group functionality surrounding an error case with an exception itself. Örneğin, bir özel durum oluşturduğunda, önemli bilgileri özel durum meta verilerinde kapsayan bir API kullanıyor olabilirsiniz.For example, you may be consuming an API that, when it throws an exception, encloses valuable information in the exception metadata. Etkin düzenin içindeki yakalanan özel durum gövdesinde yararlı bir değeri sarmalama ve bu değeri döndürme bazı durumlarda yararlı olabilir.Unwrapping a useful value in the body of the captured exception inside the Active Pattern and returning that value can be helpful in some situations.

Özel durumları değiştirmek için monadıc hata işleme kullanmayınDo not use monadic error handling to replace exceptions

Özel durumlar fonksiyonel programlamada biraz Taboo olarak görülür.Exceptions are seen as somewhat taboo in functional programming. Aslında özel durumlar ihlal ediyor, bu nedenle bunları çok işlevli olarak düşünmek güvenlidir.Indeed, exceptions violate purity, so it's safe to consider them not-quite functional. Ancak, bu durum kodun çalışması gereken gerçekliği yoksayar ve çalışma zamanı hataları oluşabilir.However, this ignores the reality of where code must run, and that runtime errors can occur. Genel olarak, önemli sürprleri en aza indirmek için çoğu şeyin saf veya toplam olmadığı varsayımına göre kod yazın.In general, write code on the assumption that most things are neither pure nor total, to minimize unpleasant surprises.

.NET çalışma zamanı ve çapraz dil ekosisteminde bir bütün olarak ilgi ve uygunluk açısından, özel durumların önem derecesine/yönlerini göz önünde bulundurmanız önemlidir.It is important to consider the following core strengths/aspects of Exceptions with respect to their relevance and appropriateness in the .NET runtime and cross-language ecosystem as a whole:

  1. Bu kişiler, bir sorun ayıklanırken çok yararlı olan ayrıntılı tanılama bilgilerini içerirler.They contain detailed diagnostic information, which is very helpful when debugging an issue.
  2. Çalışma zamanı ve diğer .NET dilleri tarafından iyi anlaşılabilirler.They are well-understood by the runtime and other .NET languages.
  3. Bu kişiler, semantiğinin bazı alt kümelerini geçici olarak uygulayarak özel durumların önüne geçen kodla karşılaştırıldığında önemli ortak bir şekilde azalabilir.They can reduce significant boilerplate when compared with code that goes out of its way to avoid exceptions by implementing some subset of their semantics on an ad-hoc basis.

Bu üçüncü nokta kritiktir.This third point is critical. Önemsiz olmayan karmaşık işlemler için, özel durumları kullanmayan durumlar, şunun gibi yapılarla ilgileniyor:For nontrivial complex operations, failing to use exceptions can result in dealing with structures like this:

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

Bu, "stringly-yazılan" hatalarda model eşleştirme gibi kırılmamış koda kolayca yol açabilir:Which can easily lead to fragile code like pattern matching on "stringly-typed" errors:

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, "Nicer" türü döndüren bir "basit" işlevi için istenen özel duruma izin verebilir.Additionally, it can be tempting to swallow any exception in the desire for a "simple" function that returns a "nicer" type:

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

Ne yazık ki, tryReadAllText bir dosya sisteminde gerçekleşebilecek şeyler hakkında çok sayıda özel durum oluşturabilir ve bu kod ortamınızda gerçekten yanlış duruma gelmiş olabilecek bilgileri atar.Unfortunately, tryReadAllText can throw numerous exceptions based on the myriad of things that can happen on a file system, and this code discards away any information about what might actually be going wrong in your environment. Bu kodu bir sonuç türüyle değiştirirseniz, "stringly-Typed" hata iletisi ayrıştırma ' ya geri dönebilirsiniz:If you replace this code with a result type, then you're back to "stringly-typed" error message parsing:

// 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ştirilmesi, Error işlev yerine çağrı sitesindeki özel durum türüyle düzgün bir şekilde uğraşmaya zorlar.And placing the exception object itself in the Error constructor just forces you to properly deal with the exception type at the call site rather than in the function. Bunu etkin hale getirmek, bir API çağıranı olarak ele alınması için önemli ölçüde eğlenceli olmayan, denetlenen özel durumlar oluşturur.Doing this effectively creates checked exceptions, which are notoriously unfun to deal with as a caller of an API.

Yukarıdaki örneklere iyi bir alternatif, belirli özel durumları yakalamak ve bu özel durumun bağlamına anlamlı bir değer döndürmemelidir.A good alternative to the above examples is to catch specific exceptions and return a meaningful value in the context of that exception. tryReadAllTextİşlevi aşağıdaki gibi değiştirirseniz, None daha fazla anlamı vardır:If you modify the tryReadAllText function as follows, None has more meaning:

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

Catch-all olarak çalışmak yerine, bu işlev artık bir dosya bulunamadığı ve bu anlamı bir return öğesine atayan durumu doğru şekilde işleymeyecektir.Instead of functioning as a catch-all, this function will now properly handle the case when a file was not found and assign that meaning to a return. Bu dönüş değeri bu hata durumuyla eşleşirken, herhangi bir bağlamsal bilgileri atmakla veya çağıranların koddaki bu noktada ilgisi olmayan bir servis talebiyle uğraşmak üzere zorlanmamalıdır.This return value can map to that error case, while not discarding any contextual information or forcing callers to deal with a case that may not be relevant at that point in the code.

Gibi türler Result<'Success, 'Error> , iç içe olmadıkları durumlarda temel işlemler için uygundur ve F # isteğe bağlı türleri bir şeyi veya hiçbirini döndürene zaman dönebileceği temsil etmek için mükemmeldir.Types such as Result<'Success, 'Error> are appropriate for basic operations where they aren't nested, and F# optional types are perfect for representing when something could either return something or nothing. Özel durumların yerini almaz, ancak özel durumları değiştirme girişiminde kullanılmamalıdır.They are not a replacement for exceptions, though, and should not be used in an attempt to replace exceptions. Bunun yerine, özel durum ve hata yönetimi ilkesinin hedeflenen yollarla belirli yönlerini karşılamak için bozacağından uygulanmalıdır.Rather, they should be applied judiciously to address specific aspects of exception and error management policy in targeted ways.

Kısmi uygulama ve noktadan ücretsiz programlamaPartial application and point-free programming

F # kısmi uygulamayı destekler ve bu nedenle, noktadan farklı stilde programlama için çeşitli yollar sunar.F# supports partial application, and thus, various ways to program in a point-free style. Bu, bir modül içindeki kod yeniden kullanımı veya bir şeyin uygulanması için faydalı olabilir, ancak genel kullanıma sunulmayı bir şey değildir.This can be beneficial for code reuse within a module or the implementation of something, but it is not something to expose publicly. Genel olarak, ücretsiz programlama, ve içinde bir virtuale değildir ve stilde sarmalanmış olmayan kişiler için önemli bir bilişsel engel ekleyebilir.In general, point-free programming is not a virtue in and of itself, and can add a significant cognitive barrier for people who are not immersed in the style.

Kısmi uygulama kullanmayın ve ortak API 'lerde currying kullanınDo not use partial application and currying in public APIs

Küçük bir özel durumla, genel API 'lerde kısmi uygulama kullanımı, tüketiciler için kafa karıştırıcı olabilir.With little exception, the use of partial application in public APIs can be confusing for consumers. Genellikle, let F # kodundaki bağlantılı değerler, işlev değerlerideğil değerlerdir.Usually, let-bound values in F# code are values, not function values. Değerleri ve işlev değerlerini birlikte karıştırmak, özellikle de işlevleri oluşturmak için gibi işleçlerle birleştirildiğinde, çok sayıda bilişsel ek yük için Exchange 'de küçük miktarda kod satırı kaydedilmesine neden olabilir >> .Mixing together values and function values can result in saving a small number of lines of code in exchange for quite a bit of cognitive overhead, especially if combined with operators such as >> to compose functions.

Noktadan noktaya ücretsiz programlama için araç etkilerini göz önünde bulundurunConsider the tooling implications for point-free programming

Curried işlevleri bağımsız değişkenlerini etiketetmez.Curried functions do not label their arguments. Bu, etkilerini olumsuz etkiler.This has tooling implications. Aşağıdaki iki işlevi göz önünde bulundurun:Consider the following two functions:

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

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

Her ikisi de geçerli işlevlerdir, ancak funcWithApplication curried bir işlevdir.Both are valid functions, but funcWithApplication is a curried function. Bir düzenleyicide türlerin üzerine geldiğinizde şunu görürsünüz:When you hover over their types in an editor, you see this:

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

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

Çağrı sitesinde, Visual Studio gibi araç ipuçları, string ve int Giriş türlerinin gerçekten temsil ettiği şekilde size anlamlı bilgiler vermeyecektir.At the call site, tooltips in tooling such as Visual Studio will not give you meaningful information as to what the string and int input types actually represent.

Herkese açık şekilde tüketilebilir gibi nokta içermeyen funcWithApplication bir kodla karşılaşırsanız, araç, bağımsız değişkenler için anlamlı adlara sahip olması için tam η genişletmesi yapmanız önerilir.If you encounter point-free code like funcWithApplication that is publicly consumable, it is recommended to do a full η-expansion so that tooling can pick up on meaningful names for arguments.

Ayrıca, hata ayıklama noktası ücretsiz kod, mümkün değilse zor olabilir.Furthermore, debugging point-free code can be challenging, if not impossible. Hata ayıklama araçları, adlara bağlı değerleri kullanır (örneğin, let bağlamalar) ve böylece ara değerleri, yürütme aracılığıyla bir şekilde inceleyebilirsiniz.Debugging tools rely on values bound to names (for example, let bindings) so that you can inspect intermediate values midway through execution. Kodunuzun incelenecek bir değeri olmadığında hata ayıklamanın bir şey yoktur.When your code has no values to inspect, there is nothing to debug. Gelecekte, hata ayıklama araçları, daha önce yürütülen yollara göre bu değerleri sentezleştirmek üzere gelişiyor, ancak sonuçlarınızı olası hata ayıklama işlevselliğine göre eklemek iyi bir fikir değildir.In the future, debugging tools may evolve to synthesize these values based on previously executed paths, but it's not a good idea to hedge your bets on potential debugging functionality.

İç ortak uygulamayı azaltmak için kısmi uygulamayı bir teknik olarak düşününConsider partial application as a technique to reduce internal boilerplate

Önceki noktanın aksine kısmi uygulama, bir uygulamanın içindeki demirbaş veya bir API 'nin daha derin iç içe geçmiş olması için harika bir araçtır.In contrast to the previous point, partial application is a wonderful tool for reducing boilerplate inside of an application or the deeper internals of an API. Daha karmaşık API 'lerin uygulanması yararlı olabilir; burada ortak, genellikle ilgileniyor bir sorun.It can be helpful for unit testing the implementation of more complicated APIs, where boilerplate is often a pain to deal with. Örneğin, aşağıdaki kod, en fazla bir çerçeve için bir dış bağımlılık almadan ve ilgili bir beste API öğrenmeye gerek kalmadan size en fazla ne kadar çok şey sağladığını nasıl gerçekleştirebileceğinizi göstermektedir.For example, the following code shows how you can accomplish what most mocking frameworks give you without taking an external dependency on such a framework and having to learn a related bespoke API.

Örneğin, aşağıdaki çözüm topografisini göz önünde bulundurun:For example, consider the following solution topography:

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

ImplementationLogic.fsprojŞu gibi bir kod açığa çıkabilir:ImplementationLogic.fsproj might expose code such as:

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

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

Transactions.doTransaction' De birim testi ImplementationLogic.Tests.fsproj kolaydır:Unit testing Transactions.doTransaction in ImplementationLogic.Tests.fsproj is easy:

namespace TransactionsTestingUtil

open Transactions

module TransactionsTestable =
    let getTestableTransactionRoutine mockContext = Transactions.doTransaction mockContext

doTransactionBir sahte işlem bağlam nesnesiyle kısmen uygulamak, her seferinde bir mocıng bağlamı oluşturmaya gerek kalmadan, işlevi tüm birim testlerinizde çağırmanıza olanak sağlar:Partially applying doTransaction with a mocking context object lets you call the function in all of your unit tests without needing to construct a mocked context each time:

namespace 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 teknik, tüm kod tabanınıza evrensel olarak uygulanmamalıdır, ancak karmaşık iç yapılar ve bu iç yapıları için ortak bir yoldur.This technique should not be universally applied to your entire codebase, but it is a good way to reduce boilerplate for complicated internals and unit testing those internals.

Erişim denetimiAccess control

F #, erişim denetimiiçin birden fazla seçeneğe sahiptir ve .NET çalışma zamanında kullanılabilir olan öğeden devralınır.F# has multiple options for Access control, inherited from what is available in the .NET runtime. Bunlar yalnızca türler için kullanılamaz. bunları işlevler için de kullanabilirsiniz.These are not just usable for types - you can use them for functions, too.

  • publicTürleri ve üyeleri, herkese açık bir şekilde tüketilene kadar tercih edin.Prefer non-public types and members until you need them to be publicly consumable. Bu Ayrıca, hangi tüketicilerin için de en aza indirir.This also minimizes what consumers couple to.
  • Tüm yardımcı işlevleri tutmak için çaba harcar private .Strive to keep all helper functionality private.
  • [<AutoOpen>]Çok sayıda hale gelirse, yardımcı işlevlerin özel modülünde kullanımını göz önünde bulundurun.Consider the use of [<AutoOpen>] on a private module of helper functions if they become numerous.

Tür çıkarımı ve genel türlerType inference and generics

Tür çıkarımı, çok sayıda demirbaş yazmanız için sizi kaydedebilir.Type inference can save you from typing a lot of boilerplate. F # derleyicisinde otomatik Genelleştirme, sizin bölüminizdeki neredeyse hiç fazla çaba olmadan daha fazla genel kod yazmanıza yardımcı olabilir.And automatic generalization in the F# compiler can help you write more generic code with almost no extra effort on your part. Ancak, bu özellikler evrensel olarak iyi değildir.However, these features are not universally good.

  • Bağımsız değişken adlarını ortak API 'lerde açık türlerle etiketlemeyi düşünün ve bunun için tür çıkarımı kullanmayın.Consider labeling argument names with explicit types in public APIs and do not rely on type inference for this.

    Bunun nedeni, derleyicinin değil, API 'nizin şeklinin denetiminde olması gerekir.The reason for this is that you should be in control of the shape of your API, not the compiler. Derleyici, sizin için türler üzerinde ince bir iş yapabilse de, bağımlı olduğu iç işlem tür türleri değiştiyse API 'nizin şeklinin değiştirilmesi mümkündür.Although the compiler can do a fine job at inferring types for you, it is possible to have the shape of your API change if the internals it relies on have changed types. Bu, istediğiniz gibi olabilir, ancak aşağı akış tüketicilerinin daha sonra uğraşmak zorunda olacağı bir API değişikliğine neredeyse tamamen neden olur.This may be what you want, but it will almost certainly result in a breaking API change that downstream consumers will then have to deal with. Bunun yerine, ortak API 'nizin şeklini açıkça kontrol ediyorsanız, bu son değişiklikleri denetleyebilirsiniz.Instead, if you explicitly control the shape of your public API, then you can control these breaking changes. DDD terimlerinde, bu, bozulma önleyici bir katman olarak düşünülebilir.In DDD terms, this can be thought of as an Anti-corruption layer.

  • Genel bağımsız değişkenlerinize anlamlı bir ad vermeyi düşünün.Consider giving a meaningful name to your generic arguments.

    Belirli bir etki alanına özgü olmayan gerçekten genel kod yazmadığınız takdirde anlamlı bir ad, diğer programcıların çalıştıkları etki alanını anlamasına yardımcı olabilir.Unless you are writing truly generic code that is not specific to a particular domain, a meaningful name can help other programmers understanding the domain they're working in. Örneğin, 'Document bir belge veritabanıyla etkileşim bağlamında adlı bir tür parametresi, genel belge türlerinin, çalıştığınız işlev veya üye tarafından kabul edilebilir olmasını sağlar.For example, a type parameter named 'Document in the context of interacting with a document database makes it clearer that generic document types can be accepted by the function or member you are working with.

  • Genel tür parametrelerini PascalCase ile adlandırmayı düşünün.Consider naming generic type parameters with PascalCase.

    Bu, .NET ' te şeyler yapmanın genel yoludur. bu nedenle, snake_case veya camelCase yerine PascalCase kullanmanız önerilir.This is the general way to do things in .NET, so it's recommended that you use PascalCase rather than snake_case or camelCase.

Son olarak, otomatik Genelleştirme, F # veya büyük bir kod temeli için yeni olan kişiler için her zaman bir Boon değildir.Finally, automatic generalization is not always a boon for people who are new to F# or a large codebase. Genel olan bileşenleri kullanırken bilişsel ek yükü vardır.There is cognitive overhead in using components that are generic. Ayrıca, otomatik olarak Genelleştirilmiş işlevler farklı giriş türleriyle kullanılmazsa (Bu şekilde kullanılması amaçlanıyorsa tek başına izin ver), bu noktada bu noktada genel olan gerçek bir avantaj yoktur.Furthermore, if automatically generalized functions are not used with different input types (let alone if they are intended to be used as such), then there is no real benefit to them being generic at that point in time. Yazmakta olduğunuz kodun gerçekten genel olmasının yararlı olacağını her zaman göz önünde bulundurun.Always consider if the code you are writing will actually benefit from being generic.

PerformansPerformance

Küçük veri türleri için yapıları tercih etPrefer structs for small data types

Yapıları (değer türleri olarak da bilinir) kullanmak, genellikle nesne ayırmayı önlediği için bazı kodlar için daha yüksek performans oluşmasına neden olabilir.Using structs (also called Value Types) can often result in higher performance for some code because it typically avoids allocating objects. Ancak, yapılar her zaman bir "daha hızlı git" düğmesi değildir: bir yapı içindeki verilerin boyutu 16 baytı aşarsa, verilerin kopyalanması genellikle başvuru türü kullanmaktan daha fazla CPU süresi harcanmasına neden olabilir.However, structs are not always a "go faster" button: if the size of the data in a struct exceeds 16 bytes, copying the data can often result in more CPU time spend than using a reference type.

Yapısını kullanıp kullanmadığını öğrenmek için aşağıdaki koşulları göz önünde bulundurun:To determine if you should use a struct, consider the following conditions:

  • Verilerinizin boyutu 16 bayt veya daha küçükse.If the size of your data is 16 bytes or smaller.
  • Büyük olasılıkla, çalışan bir programda bellekte yerleşik olarak bulunan bu veri türlerinden birçoğuna sahip olabilirsiniz.If you're likely to have many of these data types resident in memory in a running program.

İlk koşul geçerliyse, genellikle bir struct kullanmanız gerekir.If the first condition applies, you should generally use a struct. Her ikisi de varsa, neredeyse her zaman bir struct kullanmanız gerekir.If both apply, you should almost always use a struct. Önceki koşulların geçerli olduğu bazı durumlar olabilir, ancak bir yapının kullanılması bir başvuru türü kullanmaktan daha iyi veya daha kötütür değildir ancak nadir olabilir.There may be some cases where the previous conditions apply, but using a struct is no better or worse than using a reference type, but they are likely to be rare. Bu, ancak bu gibi değişiklikler yaparken her zaman ölçülmek önemlidir, ancak varsayım veya ıntukon üzerinde çalışmaz.It's important to always measure when making changes like this, though, and not operate on assumption or intuition.

Küçük değer türlerini gruplarken yapı tanımlama gruplarını tercih etPrefer struct tuples when grouping small value types

Aşağıdaki iki işlevi göz önünde bulundurun:Consider the following two functions:

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 değerlendirme aracı ile kıyasladığınızda, runWithStructTuple Yapı tanımlama gruplarını kullanan işlevin %40 daha hızlı çalıştığını ve bellek ayırdığını fark edeceksiniz.When you benchmark these functions with a statistical benchmarking tool like BenchmarkDotNet, you'll find that the runWithStructTuple function that uses struct tuples runs 40% faster and allocates no memory.

Ancak, bu sonuçlar her zaman kendi kodunuzda durum değildir.However, these results won't always be the case in your own code. Bir işlevi olarak işaretlerseniz inline , başvuru tanımlama gruplarını kullanan kod bazı ek iyileştirmeler alabilir veya ayrılacak kod yalnızca en iyi duruma getirilebilir.If you mark a function as inline, code that uses reference tuples may get some additional optimizations, or code that would allocate could simply be optimized away. Performans açısından her zaman sonuçları ölçmelisiniz ve varsayım ya da ıntuksiz göre hiçbir zaman çalışmaz.You should always measure results whenever performance is concerned, and never operate based on assumption or intuition.

Veri türü küçük olduğunda yapı kayıtlarını tercih etPrefer struct records when the data type is small

Daha önce açıklanan Thumb kuralı F # kayıt türleriiçin de geçerlidir.The rule of thumb described earlier also holds for F# record types. Aşağıdaki veri türlerini ve bunları işleyen işlevleri göz önünde bulundurun:Consider the following data types and functions that process them:

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.This is similar to the previous tuple code, but this time the example uses records and an inlined inner function.

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.When you benchmark these functions with a statistical benchmarking tool like BenchmarkDotNet, you'll find that processStructPoint runs nearly 60% faster and allocates nothing on the managed heap.

Veri türü küçük olduğunda struct ayrılmış birleşimler tercih etPrefer struct discriminated unions when the data type is small

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.The previous observations about performance with struct tuples and records also holds for F# Discriminated Unions. Aşağıdaki kodu inceleyin:Consider the following code:

    type Name = Name of string

    [<Struct>]
    type SName = SName of string

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

    let structReverseName (SName s) =
        s.ToCharArray()
        |> Array.rev
        |> 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.It's common to define single-case Discriminated Unions like this for domain modeling. 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.When you benchmark these functions with a statistical benchmarking tool like BenchmarkDotNet, you'll find that structReverseName runs about 25% faster than reverseName for small strings. Büyük dizeler için her ikisi de aynı şekilde gerçekleştirilir.For large strings, both perform about the same. Bu nedenle, bu durumda her zaman bir struct kullanılması tercih edilir.So, in this case, it's always preferable to use a struct. Daha önce belirtildiği gibi, varsayımlar veya ıntuklarda her zaman ölçüm ve işlem kullanmayın.As previously mentioned, always measure and do not operate on assumptions or intuition.

Ö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.Although the previous example showed that a struct Discriminated Union yielded better performance, it is common to have larger Discriminated Unions when modeling a domain. 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.Larger data types like that may not perform as well if they are structs depending on the operations on them, since more copying could be involved.

Fonksiyonel programlama ve mutasyonFunctional programming and mutation

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).F# values are immutable by default, which allows you to avoid certain classes of bugs (especially those involving concurrency and parallelism). 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.However, in certain cases, in order to achieve optimal (or even reasonable) efficiency of execution time or memory allocations, a span of work may best be implemented by using in-place mutation of state. Bu, anahtar sözcüğü ile F # ile bir katılım temelinde mümkündür mutable .This is possible in an opt-in basis with F# with the mutable keyword.

mutableF # içinde öğesinin kullanımı, gürültü 'yi işlevsel bir şekilde kullanabilir.Use of mutable in F# may feel at odds with functional purity. Bu, anlaşılır değildir ancak her yerde işlevsel anlaya, performans hedeflerine sahip gürültü 'de bulunabilir.This is understandable, but functional purity everywhere can be at odds with performance goals. 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.A compromise is to encapsulate mutation such that callers need not care about what happens when they call a function. Bu, performans açısından kritik kod için bir mutasyon tabanlı uygulama üzerinde işlevsel bir arabirim yazmanızı sağlar.This allows you to write a functional interface over a mutation-based implementation for performance-critical code.

Değişmez arabirimlerde kesilebilir kodu sarınWrap mutable code in immutable interfaces

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.With referential transparency as a goal, it is critical to write code that does not expose the mutable underbelly of performance-critical functions. Örneğin, aşağıdaki kod Array.contains Işlevi F # Çekirdek Kitaplığı 'nda uygular:For example, the following code implements the Array.contains function in the F# core library:

[<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.Calling this function multiple times does not change the underlying array, nor does it require you to maintain any mutable state in consuming it. Neredeyse her bir kod satırı mutation kullandığından bile, bu değer saydam bir şekilde görünür.It is referentially transparent, even though almost every line of code within it uses mutation.

Sınıflarda kesilebilir verileri kapsüllemek için kullanınConsider encapsulating mutable data in classes

Önceki örnekte, değiştirilebilir verileri kullanarak işlemleri kapsüllemek için tek bir işlev kullanılmıştır.The previous example used a single function to encapsulate operations using mutable data. Bu, daha karmaşık veri kümeleri için her zaman yeterli değildir.This is not always sufficient for more complex sets of data. Aşağıdaki işlev kümelerini göz önünde bulundurun:Consider the following sets of functions:

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.This code is performant, but it exposes the mutation-based data structure that callers are responsible for maintaining. Bu, değiştireyebilecek temel üye olmadan bir sınıfın içine sarmalanabilir:This can be wrapped inside of a class with no underlying members that can change:

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

Closure1Tabletemel 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.Closure1Table encapsulates the underlying mutation-based data structure, thereby not forcing callers to maintain the underlying data structure. 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.Classes are a powerful way to encapsulate data and routines that are mutation-based without exposing the details to callers.

Hücrelere başvuru yapmayı tercih et let mutablePrefer let mutable to reference cells

Başvuru hücreleri değerin kendisi yerine bir değere başvuruyu temsil etmenin bir yoludur.Reference cells are a way to represent the reference to a value rather than the value itself. Performans açısından kritik kod için kullanılabilmesine rağmen, bunlar önerilmez.Although they can be used for performance-critical code, they are not recommended. Aşağıdaki örneği inceleyin:Consider the following example:

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.The use of a reference cell now "pollutes" all subsequent code with having to dereference and re-reference the underlying data. Bunun yerine şunları göz önünde bulundurun let mutable :Instead, consider 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.Aside from the single point of mutation in the middle of the lambda expression, all other code that touches acc can do so in a manner that is no different to the usage of a normal let-bound immutable value. Bu, zaman içinde değişiklik yapmayı kolaylaştırır.This will make it easier to change over time.

Nesne programlamaObject programming

F #, nesneler ve nesne yönelimli (OO) kavramları için tam desteğe sahiptir.F# has full support for objects and object-oriented (OO) concepts. Birçok OO kavramı güçlü ve yararlı olsa da, bunların hepsi kullanım için idealdir.Although many OO concepts are powerful and useful, not all of them are ideal to use. Aşağıdaki listeler, en yüksek düzeyde, OO özelliklerinin kategorileri üzerinde rehberlik sunar.The following lists offer guidance on categories of OO features at a high level.

Bu özellikleri birçok durumda kullanmayı göz önünde bulundurun:Consider using these features in many situations:

  • Nokta gösterimi ( x.Length )Dot notation (x.Length)
  • Örnek üyeleriInstance members
  • Örtük oluşturucularImplicit constructors
  • Statik üyelerStatic members
  • Dizin Oluşturucu gösterimi ( arr.[x] )Indexer notation (arr.[x])
  • Adlandırılmış ve Isteğe bağlı bağımsız değişkenlerNamed and Optional arguments
  • Arabirimler ve arabirim uygulamalarıInterfaces and interface implementations

Önce bu özelliklere ulaşmayın, ancak bir sorunu çözmek için uygun olmaları durumunda bozacağından uygulayın:Don't reach for these features first, but do judiciously apply them when they are convenient to solve a problem:

  • Yöntem aşırı yüklemesiMethod overloading
  • Encapsulated kesilebilir verilerEncapsulated mutable data
  • Türlerde işleçlerOperators on types
  • Otomatik ÖzelliklerAuto properties
  • Uygulama IDisposable veIEnumerableImplementing IDisposable and IEnumerable
  • Tür uzantılarıType extensions
  • OlaylarEvents
  • YapılarStructs
  • TemsilcilerDelegates
  • NumaralandırmalarEnums

Bunları kullanmanız gerekmedikçe bu özelliklerden genellikle kaçının:Generally avoid these features unless you must use them:

  • Devralma tabanlı tür hiyerarşileri ve uygulama devralmaInheritance-based type hierarchies and implementation inheritance
  • Null değerleri veUnchecked.defaultof<_>Nulls and Unchecked.defaultof<_>

Devralma üzerine oluşturmayı tercih etPrefer composition over inheritance

Devralma üzerinden bileşim , Iyi bir F # kodunun bağlı kalacağının uzun sürme deyimidir.Composition over inheritance is a long-standing idiom that good F# code can adhere to. 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.The fundamental principle is that you should not expose a base class and force callers to inherit from that base class to get functionality.

Sınıf gerekmiyorsa arabirim uygulamak için nesne ifadelerini kullanınUse object expressions to implement interfaces if you don't need a class

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.Object Expressions allow you to implement interfaces on the fly, binding the implemented interface to a value without needing to do so inside of a class. Bu, özellikle de yalnızca arabirimini uygulamanız gerekiyorsa ve tam sınıfa gerek duygerekmiyorsa kullanışlı bir yöntemdir.This is convenient, especially if you only need to implement the interface and have no need for a full class.

Ö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 :For example, here is the code that is run in Ionide to provide a code fix action if you've added a symbol that you don't have an open statement for:

    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 ile etkileşim kurarken bir sınıfa gerek olmadığından, nesne Ifadeleri bunun için ideal bir araçtır.Because there is no need for a class when interacting with the Visual Studio Code API, Object Expressions are an ideal tool for this. Ayrıca, test yordamlarına sahip bir arabirimi geçici olarak bir şekilde sağlamak istediğinizde birim testi için de değerlidir.They are also valuable for unit testing, when you want to stub out an interface with test routines in an ad hoc manner.

İmzaları kısaltmak için kısaltmalar türlerini göz önünde bulundurunConsider Type Abbreviations to shorten signatures

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.Type Abbreviations are a convenient way to assign a label to another type, such as a function signature or a more complex type. Örneğin, aşağıdaki diğer ad, derin bir öğrenme kitaplığı olan Cntkile bir hesaplama tanımlamak için gereken bir etiketi atar:For example, the following alias assigns a label to what's needed to define a computation with CNTK, a deep learning library:

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.The Computation name is a convenient way to denote any function that matches the signature it is aliasing. Bu gibi tür kısaltmalarının kullanılması kullanışlıdır ve daha fazla kısa kodu sağlar.Using Type Abbreviations like this is convenient and allows for more succinct code.

Etki alanınızı temsil etmek için tür kısaltmalarının kullanmaktan kaçınınAvoid using Type Abbreviations to represent your domain

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.Although Type Abbreviations are convenient for giving a name to function signatures, they can be confusing when abbreviating other types. Bu kısaltmayı göz önünde bulundurun:Consider this abbreviation:

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

Bu, birden çok şekilde kafa karıştırıcı olabilir:This can be confusing in multiple ways:

  • BufferSizebir soyutlama değil; tamsayı için yalnızca başka bir addır.BufferSize is not an abstraction; it is just another name for an integer.
  • BufferSizeOrtak BIR API 'de açığa çıkarılacası, kolayca yanlışlıkla büyük bir şekilde yorumlanabilmektedir int .If BufferSize is exposed in a public API, it can easily be misinterpreted to mean more than just int. Genellikle, etki alanı türlerinin kendileri için birden çok özniteliği vardır ve gibi basit türler değildir int .Generally, domain types have multiple attributes to them and are not primitive types like int. Bu kısaltma Bu varsayımını ihlal ediyor.This abbreviation violates that assumption.
  • Büyük/küçük harf BufferSize (PascalCase), bu türün daha fazla veri bulundurduğunu gösterir.The casing of BufferSize (PascalCase) implies that this type holds more data.
  • 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.This alias does not offer increased clarity compared with providing a named argument to a function.
  • 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.The abbreviation will not manifest in compiled IL; it is just an integer and this alias is a compile-time construct.
module Networking =
    ...
    let send data (bufferSize: int) = ...

Özet olarak, tür kısaltmalarıyla birlikte, abbreviating oldukları türler üzerinde soyutlamalar değildir .In summary, the pitfall with Type Abbreviations is that they are not abstractions over the types they are abbreviating. Önceki örnekte, daha BufferSize int fazla veri olmadan ve tür sisteminden zaten sahip olduğu gibi herhangi bir avantajın altında yer alır int .In the previous example, BufferSize is just an int under the covers, with no additional data, nor any benefits from the type system besides what int already has.

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.An alternative approach to using type abbreviations to represent a domain is to use single-case discriminated unions. Önceki örnek aşağıdaki gibi modellenebilir:The previous sample can be modeled as follows:

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:If you write code that operates in terms of BufferSize and its underlying value, you need to construct one rather than pass in any arbitrary integer:

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.This reduces the likelihood of mistakenly passing an arbitrary integer into the send function, because the caller must construct a BufferSize type to wrap a value before calling the function.