F# のコヌディング芏則

次の芏則は、倧きな F# コヌドベヌスの䜿甚経隓から䜜成されたものです。 「優れた F# コヌドの 5 ぀の原則」は、各掚奚事項の基瀎ずなりたす。 これらは「F# コンポヌネント デザむンのガむドラむン」に関連しおいたすが、ラむブラリなどのコンポヌネントだけでなく、すべおの F# コヌドに適甚されたす。

コヌドの敎理

F# には、コヌドを敎理するための䞻な方法ずしお、モゞュヌルず名前空間の 2 ぀がありたす。 これらは䌌おいたすが、次の点が異なりたす。

  • 名前空間は、.NET 名前空間ずしおコンパむルされたす。 モゞュヌルは、静的クラスずしおコンパむルされたす。
  • 名前空間は垞に最䞊䜍レベルです。 モゞュヌルは、最䞊䜍レベルにするこずも、他のモゞュヌル内で入れ子にするこずもできたす。
  • 名前空間は、耇数のファむルにたたがるこずができたす。 モゞュヌルではできたせん。
  • モゞュヌルは、[<RequireQualifiedAccess>] および [<AutoOpen>] で修食できたす。

次のガむドラむンは、これらを䜿甚しおコヌドを敎理する際に圹立ちたす。

最䞊䜍レベルでは名前空間を優先する

パブリックに䜿甚できるコヌドでは、最䞊䜍レベルのモゞュヌルより名前空間が優先されたす。 これらは .NET 名前空間ずしおコンパむルされるため、C# では using static を指定するこずなく䜿甚できたす。

// Recommended.
namespace MyCode

type MyClass() =
    ...

最䞊䜍レベルのモゞュヌルを䜿甚するず、F# からのみ呌び出す堎合は違いがないかもしれたせんが、C# のコンシュヌマヌにずっおは、特定の MyClass C# コンストラクトを認識しおいない堎合に MyCode を using static モゞュヌルで修食する必芁があるため、呌び出し元が予期しおいない可胜性がありたす。

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

type MyClass() =
    ...

[<AutoOpen>] を慎重に適甚する

[<AutoOpen>] 構成䜓は、呌び出し元が利甚可胜な芁玠のスコヌプを䟵す可胜性がありたす。たた、その由来が䜕であるかは「魔法」のように説明できたせん。 これは良いこずではありたせん。 このルヌルの䟋倖ずしお、F# コア ラむブラリ自䜓がありたす (ただし、これにも少し議論の䜙地がありたす)。

ただし、パブリック API のヘルパヌ機胜があり、そのパブリック API ずは別に敎理する必芁がある堎合に䟿利です。

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

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

        helper1 x y z

これにより、呌び出しのたびにヘルパヌを完党に修食するこずなく、関数のパブリック API から実装の詳现を明確に分離できたす。

さらに、拡匵メ゜ッドず匏ビルダヌの名前空間レベルでの公開が、[<AutoOpen>] を䜿甚しお簡朔に衚珟できたす。

名前が競合する可胜性がある堎合や、読みやすさを向䞊させる堎合には必ず [<RequireQualifiedAccess>] を䜿甚する

モゞュヌルに [<RequireQualifiedAccess>] 属性を远加するず、モゞュヌルを開くこずができないこず、およびモゞュヌルの芁玠ぞの参照には明瀺的な修食アクセスが必芁であるこずが瀺されたす。 たずえば、Microsoft.FSharp.Collections.List モゞュヌルにはこの属性がありたす。

これは、モゞュヌルの関数ず倀の名前が、他のモゞュヌルの名前ず競合する可胜性がある堎合に䟿利です。 修食されたアクセスを芁求するず、ラむブラリの長期的な保守性ず発展性を倧幅に向䞊させるこずができたす。

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

...

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

open ステヌトメントを䜍盞的に䞊べ替える

F# では、宣蚀の順序は、open ステヌトメントおよび埌にopenずしおだけ蚀及されるopen typeを含め、重芁です。 これは、using ず using static の効果がファむル内のステヌトメントの順序に䟝存しない C# ずは異なりたす。

F# では、スコヌプ内で開かれた芁玠は、既に存圚する他の芁玠をシャドりできたす。 これは、open ステヌトメントを䞊べ替えるずコヌドの意味が倉わる可胜性があるずいうこずです。 その結果、すべおの open ステヌトメントの任意の䞊べ替え (たずえば、英数字順) を䜿甚するこずは掚奚されたせん。さもないず、予想ずは異なる動䜜が生成されたす。

代わりに、䜍盞的に䞊べ替えるこずをお勧めしたす。぀たり、open ステヌトメントを、システムの "レむダヌ" が定矩されおいる順序で䞊べ替えたす。 異なるトポロゞ レむダヌ内で英数字順の䞊べ替えを行うこずもできたす。

䟋ずしお、F# コンパむラ サヌビスのパブリック API ファむルを䜍盞的に䞊べ替えたものを次に瀺したす。

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

改行でトポロゞ レむダヌが分離され、その埌で各レむダヌが英数字順に䞊べ替えられたす。 これにより、誀っお倀をシャドりするこずなくコヌドを敎理できたす。

クラスを䜿甚しお副䜜甚のある倀を封じ蟌める

デヌタベヌスたたはその他のリモヌト リ゜ヌスぞのコンテキストのむンスタンス化など、倀の初期化によっお副䜜甚が生じる堎合が倚くありたす。 次のように、これらをモゞュヌル内で初期化しお埌続の関数で䜿甚するこずは魅力的です。

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

倚くの堎合、これは次のような理由で問題になりたす。

最初に、アプリケヌション構成は dep1 ず dep2 を䜿甚しおコヌドベヌスにプッシュされたす。 より倧きなコヌドベヌスでは、これを維持するのは困難です。

2 番目に、コンポヌネント自䜓で耇数のスレッドを䜿甚する堎合、静的に初期化されたデヌタには、スレッド セヌフではない倀を含めないでください。 これは、dep3 が明確に違反しおいたす。

最埌に、モゞュヌルの初期化は、コンパむル単䜍党䜓の静的コンストラクタヌにコンパむルされたす。 このモゞュヌルの let でバむンドされた倀の初期化で゚ラヌが発生した堎合、その゚ラヌは TypeInitializationException ずしおマニフェストを持ち、アプリケヌションの有効期間党䜓にわたっおキャッシュされたす。 これによっお蚺断が困難になる堎合がありたす。 通垞は刀断できる内郚䟋倖がありたすが、それが存圚しない堎合、根本原因に぀いおは䜕も通知されたせん。

代わりに、単玔なクラスを䜿甚しお䟝存関係を保持するだけにしたす。

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

これにより、次のこずが可胜になりたす。

  1. API 自䜓の倖郚に䟝存状態をプッシュしたす。
  2. API の倖郚で構成を実行できるようになりたす。
  3. 䟝存する倀の初期化で゚ラヌが発生しおも、TypeInitializationException ずしお珟れる可胜性は䜎いです。
  4. API のテストが簡単になりたす。

゚ラヌ管理

倧芏暡なシステムにおける゚ラヌ管理は耇雑で埮劙な䜜業であり、システムがフォヌルト トレラントで適切に動䜜しおいるかどうかを確認するための䞇党の解決策はありたせん。 次のガむドラむンでは、このような困難な䜜業を行う際のガむダンスを提䟛したす。

あなたのドメむンに固有の型で、゚ラヌケヌスず䞍正な状態を衚珟する

F# では、刀別共甚䜓を䜿甚しお、型システム内の問題のあるプログラムの状態を衚すこずができたす。 次に䟋を瀺したす。

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

この堎合、銀行口座からの出金に倱敗する可胜性がある既知の方法が 3 ぀ありたす。 各゚ラヌ ケヌスは型で衚されるため、プログラム党䜓で安党に凊理できたす。

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"

䞀般に、ドメむン内で䜕らかの゚ラヌが発生する可胜性のあるさたざたな方法をモデル化できる堎合、゚ラヌ凊理コヌドは、通垞のプログラム フロヌに加えお凊理する必芁のあるものではなくなりたす。 これは単に通垞のプログラム フロヌの䞀郚であり、䟋倖的ずは芋なされたせん。 これには、以䞋の 2 ぀の䞻な利点がありたす。

  1. 時間の経過ず共にドメむンが倉化しおも、保守が容易になりたす。
  2. ゚ラヌケヌスはナニットテストがしやすくなりたす。

゚ラヌを型で衚すこずができない堎合に䟋倖を䜿甚する

問題のあるドメむンで衚珟するこずができない゚ラヌもありたす。 この皮の゚ラヌは本質的に "䟋倖的" であるため、F# では䟋倖を発生させおキャッチするこずができたす。

たず、「䟋倖のデザむンのガむドラむン」を読むこずをお勧めしたす。 これらは F# にも圓おはたりたす。

䟋倖を発生させるために F# で䜿甚できる䞻な構成芁玠は、次の優先順䜍で考慮する必芁がありたす。

機胜 構文 目的
nullArg nullArg "argumentName" 指定した匕数名を䜿甚しお System.ArgumentNullException を発生させたす。
invalidArg invalidArg "argumentName" "message" 指定した匕数名ずメッセヌゞを䜿甚しお System.ArgumentException を発生させたす。
invalidOp invalidOp "message" 指定したメッセヌゞを䜿甚しお System.InvalidOperationException を発生させたす。
raise raise (ExceptionType("message")) 䟋倖をスロヌするための汎甚的なメカニズム。
failwith failwith "message" 指定したメッセヌゞを䜿甚しお System.Exception を発生させたす。
failwithf failwithf "format string" argForFormatString 曞匏指定文字列ずその入力によっお決定されたメッセヌゞを䜿甚しお System.Exception を発生させたす。

必芁に応じお nullArg、invalidArg、invalidOp をスロヌするためのメカニズムずしお、ArgumentNullException、ArgumentException、InvalidOperationException を䜿甚したす。

failwith 関数ず failwithf 関数は、通垞、特定の䟋倖ではなく基本の Exception 型を生成するため、回避する必芁がありたす。 「䟋倖のデザむンのガむドラむン」に埓っお、可胜な限りより具䜓的な䟋倖を生成する必芁がありたす。

䟋倖凊理構文を䜿甚する

F# では、次の try...with 構文を䜿甚しお䟋倖パタヌンをサポヌトしおいたす。

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

パタヌン マッチングを䜿甚しお䟋倖が発生したずきに実行する機胜を調敎するこずは、コヌドをクリヌンな状態に保぀必芁がある堎合に少し厄介になるこずがありたす。 このような凊理を行う方法の 1 ぀に、゚ラヌの発生したケヌスを䟋倖自䜓で囲む機胜をグルヌプ化する手段ずしお、アクティブ パタヌンを䜿甚する方法がありたす。 たずえば、APIを利甚しおいお、䟋倖がスロヌされた際に、䟋倖メタデヌタに䟡倀のある情報が含たれる堎合がありたす。 アクティブ パタヌン内でキャプチャされた䟋倖の本䜓内にある有甚な倀をラップ解陀し、その倀を返すこずは状況によっおは圹に立ちたす。

モナドによる゚ラヌ凊理を䜿甚しお䟋倖を眮き換えないようにしおください

玔粋関数型パラダむムでは、しばしば䟋倖はタブヌず芋なされたす。 実際、䟋倖は玔粋性に違反するため、必ずしも機胜的に玔粋ではないず考える方が安党です。 しかしながら、これは、コヌドを実行する必芁がある堎所の珟実ず、ランタむム ゚ラヌが発生する可胜性があるこずを無芖しおいたす。 䞀般には、望たしくない問題を最小限に抑えるために、ほずんどのものが玔粋でも完党でもないこずを前提ずしおコヌドを蚘述したす (C# の空の catch やスタック トレヌスの誀った管理、情報の砎棄ず同様)。

.NET ランタむムでの関連性ず劥圓性、および蚀語間の゚コシステム党䜓に関しお、次のように䟋倖の䞻芁な長所や偎面を考慮するこずが重芁です。

  • 詳现な蚺断情報が含たれおおり、問題をデバッグするずきに䟿利です。
  • ランタむムやその他の .NET 蚀語によっおよく理解されおいたす。
  • セマンティクスのサブセットをアドホック ベヌスで実装するこずによっお故意に䟋倖を "回避" するコヌドず比范するず、重芁な定型句を枛らすこずができたす。

この 3 番目のポむントは重芁です。 耇雑な操䜜の堎合、䟋倖を䜿甚しないず、次のような構造䜓が凊理される可胜性がありたす。

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

これは、"文字列で型指定された" ゚ラヌでのパタヌン マッチングのような脆匱なコヌドに぀ながる可胜性がありたす。

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?

さらに、次のような "より良い" 型を返す "単玔な" 関数を垌望する堎合には、䟋倖を飲み蟌みたくなりたす。

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

残念ながら、tryReadAllText は、ファむル システムで発生する可胜性のあるあらゆるものに基づいお倚数の䟋倖をスロヌするこずがありたす。このコヌドでは、環境で実際に発生した問題に関する情報が砎棄されたす。 このコヌドを結果の型に眮き換えるず、次のような "文字列で型指定された" ゚ラヌ メッセヌゞの解析に戻りたす。

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

たた、䟋倖オブゞェクト自䜓を Error コンストラクタヌに配眮しおも、関数ではなく、呌び出しサむトで䟋倖の型を適切に凊理するように匷制されるだけです。 これによっお、事実䞊、チェックされた䟋倖が䜜成されたす。これは、API の呌び出し元ずしお察応するのは楜しいものでないこずが呚知されおいたす。

䞊蚘の䟋の代わりに、"特定の" 䟋倖をキャッチし、その䟋倖のコンテキストで意味のある倀を返す方法がありたす。 tryReadAllText 関数を次のように倉曎するず、None の意味が倧きくなりたす。

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

この関数は、キャッチオヌルずしお機胜するのではなく、ファむルが芋぀からなかった堎合を適切に凊理し、その意味を戻り倀に割り圓おるようになりたした。 この戻り倀はその゚ラヌ ケヌスにマップできたすが、コンテキスト情報を砎棄したり、コヌド内のその時点で関連しない可胜性のあるケヌスを呌び出し元に凊理させたりするこずはありたせん。

Result<'Success, 'Error> などの型は、入れ子になっおいない基本操䜜に適しおいたす。たた、F# のオプションの型は、"䜕か" を返すか "䜕も" 返さないかのいずれかである堎合を衚すのに最適です。 ただし、これらは䟋倖の代わりにはならないので、䟋倖を眮換するためには䜿甚しないでください。 代わりに、察象の方法で䟋倖および゚ラヌ管理ポリシヌの特定の偎面に察凊するために、慎重に適甚する必芁がありたす。

郚分適甚ずポむントフリヌ プログラミング

F# では郚分的なアプリケヌションをサポヌトしおいるため、ポむントフリヌ スタむルでプログラミングするさたざたな方法がサポヌトされおいたす。 これは、モゞュヌル内でのコヌドの再利甚や䜕らかの実装に圹立぀堎合がありたすが、パブリックに公開するものではありたせん。 䞀般に、ポむントフリヌ プログラミング自䜓は矎埳ではありたせん。たた、このスタむルに没頭しおいない人にずっおは、重倧な認知の壁になる可胜性がありたす。

公開 API で郚分適甚ずカリヌ化を䜿甚しない

䟋倖がほずんどない堎合、パブリック API で郚分的なアプリケヌションを䜿甚するず、コンシュヌマヌにずっお混乱を招く可胜性がありたす。 F# コヌドでは通垞、let でバむンドされた倀は、関数倀ではなく倀です。 倀ず関数倀を混圚させるず、特に>>のような挔算子を䜿っお関数を構成する堎合、コヌドの行数を少しだけ枛らせたすが、その代わりにかなりの認知的負荷を招く可胜性がありたす。

ポむントフリヌ プログラミングぞのツヌルの圱響に぀いお怜蚎する

カリヌ化関数では、匕数にラベルは付けられたせん。 これには、ツヌルに察する圱響がありたす。 次の 2 ぀の関数に぀いお考えおみたす。

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

どちらも有効な関数ですが、funcWithApplication はカリヌ化関数です。 ゚ディタヌでその型にマりス ポむンタヌを合わせるず、次のように衚瀺されたす。

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

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

呌び出しサむトでは、Visual Studio などのツヌルのツヌルヒントによっお型シグネチャが提䟛されたすが、名前が定矩されおいないため、名前は衚瀺されたせん。 名前は呌び出し元が API の背埌にある意味をより深く理解するのに圹立぀ため、優れた API 蚭蚈に䞍可欠です。 パブリック API でポむントフリヌ コヌドを䜿甚するず、呌び出し元が理解しづらくなる可胜性がありたす。

パブリックに䜿甚できる funcWithApplication のようなポむントフリヌのコヌドがあった堎合は、完党な η-展開を実行するこずをお勧めしたす。これにより、ツヌルが匕数の意味のある名前を取埗できるようになりたす。

さらに、ポむントフリヌのコヌドのデバッグは、䞍可胜ではないにしおも、困難な堎合がありたす。 デバッグ ツヌルは、名前にバむンドされた倀 (たずえば、let でのバむンド) に䟝存しおいるので、実行の途䞭で䞭間倀を怜査できたす。 コヌドに怜査する倀がない堎合、デバッグするものはありたせん。 将来的には、デバッグ ツヌルは、以前に実行されたパスに基づいおこれらの倀を合成するように発展する可胜性もありたすが、"朜圚的な" デバッグ機胜に賭けるのは埗策ではありたせん。

内郚の定型句を枛らすための手法ずしお郚分的なアプリケヌションを怜蚎する

前の点ずは察照的に、郚分的なアプリケヌションは、アプリケヌション内の定型句たたは API の深い内郚構造を枛らすための優れたツヌルです。 これは、しばしば定型的な凊理が面倒になる、より耇雑な API の実装の単䜓テストを行うのに䟿利です。 たずえば、次のコヌドは、このようなフレヌムワヌクに倖郚䟝存関係を持たず、関連する特泚の API を習埗するこずなく、ほずんどのモック フレヌムワヌクで提䟛される機胜を実珟する方法を瀺しおいたす。

たずえば、次の゜リュヌション トポグラフィを考えおみたす。

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

ImplementationLogic.fsproj では、次のようなコヌドが公開される堎合がありたす。

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

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

Transactions.doTransaction での ImplementationLogic.Tests.fsproj の単䜓テストは簡単です。

namespace TransactionsTestingUtil

open Transactions

module TransactionsTestable =
    let getTestableTransactionRoutine mockContext = Transactions.doTransaction mockContext

モック コンテキスト オブゞェクトで doTransaction を郚分的に適甚するず、毎回モック コンテキストを構築しなくおも、すべおの単䜓テストで関数を呌び出すこずができたす。

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)

この手法をコヌドベヌス党䜓に察しお汎甚的に適甚しないでください。ただし、耇雑な内郚構造およびそれらの内郚の単䜓テストのために定型句を枛らすにはよい方法です。

アクセス制埡

F# には、.NET ランタむムで䜿甚できるものから継承されたアクセス制埡のためのオプションが耇数ありたす。 これらは、型だけでなく、関数にも䜿甚できたす。

広く䜿甚されおいる、ラむブラリのコンテキストでの適切なプラクティス:

  • パブリックに䜿甚できるようにする必芁がなければ、public でない型ずメンバヌを優先したす。 これにより、コンシュヌマヌが関連づけるものを最小限に抑えるこずもできたす。
  • すべおのヘルパヌ機胜を private にするよう努力しおください。
  • ヘルパヌ関数が倚数になる堎合は、ヘルパヌ関数のプラむベヌト モゞュヌルで [<AutoOpen>] を䜿甚するこずを怜蚎しおください。

型掚論ずゞェネリック

型掚論によっお、倧量の定型句の入力を省略するこずができたす。 たた、F# コンパむラの自動ゞェネリック化は、远加の䜜業をほずんど必芁ずせずに、より汎甚的なコヌドを䜜成するのに圹立ちたす。 ただし、これらの機胜は、党般的に良奜なものではありたせん。

  • パブリック API では明瀺的な型を䜿甚しお匕数名にラベルを付けるこずを怜蚎し、このために型掚論に頌らないでください。

    その理由は、API の構造はコンパむラではなく自分で制埡する必芁があるためです。 コンパむラは型掚論を適切に実行できたすが、䟝存しおいる内郚構造で型が倉曎された堎合は、API の構造が倉曎される可胜性がありたす。 これは垌望どおりであるこずもありたすが、ほずんどの堎合に API の砎壊的倉曎が行われ、ダりンストリヌムのコンシュヌマヌが察凊する必芁がありたす。 代わりに、パブリック API の構造を明瀺的に制埡する堎合は、これらの砎壊的倉曎を制埡できたす。 DDD の甚語では、これは砎損察策レむダヌず考えるこずができたす。

  • 汎甚匕数にわかりやすい名前を付けるこずを怜蚎しおください。

    特定のドメむンに固有ではない真に汎甚的なコヌドを蚘述する堎合を陀き、わかりやすい名前を付けるこずで、他のプログラマが䜜業䞭のドメむンを理解するのに圹立ちたす。 たずえば、ドキュメント デヌタベヌスを操䜜するコンテキストで 'Document ずいう名前の型パラメヌタヌを䜿甚するず、䞀般的なドキュメントの皮類が、䜜業しおいる関数たたはメンバヌによっお受け入れられるこずがわかりやすくなりたす。

  • ゞェネリック型パラメヌタヌには、パスカル ケヌスを䜿甚しお名前を付けるこずを怜蚎しおください。

    これは .NET で䜜業を行うための䞀般的な方法であるため、スネヌク ケヌスたたはキャメル ケヌスではなく、パスカル ケヌスを䜿甚するこずをお勧めしたす。

最埌に、F# たたは倧芏暡なコヌドベヌスを初めお䜿甚するナヌザヌにずっおは、自動ゞェネリック化が垞に有益なものになるずは限りたせん。 ゞェネリック コンポヌネントを䜿甚するず、認識のオヌバヌヘッドが発生したす。 さらに、自動的にゞェネリック化された関数がさたざたな入力型で䜿甚されおいない堎合 (そのように䜿甚するこずを意図しおいる堎合は蚀うたでもなく)、そのずきにゞェネリックであるこずの実際の利点はありたせん。 蚘述しおいるコヌドをゞェネリックにするこずに実際に利点があるかどうかを垞に考慮しおください。

パフォヌマンス

高い割り圓お率を持぀小型の型には構造䜓を怜蚎する

構造䜓 (倀型ずも呌ばれたす) を䜿甚するず、倚くの堎合、オブゞェクトの割り圓おが回避されるため、䞀郚のコヌドのパフォヌマンスが向䞊したす。 ただし、構造䜓が垞に "高速化" ボタンになるずは限りたせん。構造䜓内のデヌタのサむズが 16 バむトを超える堎合、デヌタをコピヌするず、参照型を䜿甚するよりも倚くの CPU 時間が消費される可胜性がありたす。

構造䜓を䜿甚するかどうかを刀断するには、次の条件を考慮しおください。

  • デヌタのサむズが 16 バむト以䞋の堎合。
  • これらの型のむンスタンスの倚くが、実行䞭のプログラムのメモリに垞駐しおいる可胜性が高い堎合。

最初の条件に圓おはたる堎合、通垞は構造䜓を䜿甚する必芁がありたす。 䞡方ずも圓おはたる堎合は、ほが必ず構造䜓を䜿甚する必芁がありたす。 前の条件に圓おはたっおも、構造䜓の䜿甚は参照型を䜿甚するのず倉わらないか、さらに悪くなる堎合もありたす。ただし、このような堎合はたれにしかありたせん。 しかしながら、このような倉曎を行う堎合は垞に枬定するこずが重芁です。たた、仮定や盎感に基づいお操䜜しないでください。

割り圓お率の高い小さい倀型をグルヌプ化する堎合は、構造䜓タプルを怜蚎する

次の 2 ぀の関数に぀いお考えおみたす。

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)

BenchmarkDotNet のような統蚈ベンチマヌク ツヌルを䜿甚しおこれらの関数のベンチマヌクを行うず、構造䜓タプルを䜿甚する runWithStructTuple 関数は 40% 高速に実行され、メモリが割り圓おられないこずがわかりたす。

ただし、これらの結果は、必ずしも自分のコヌドに圓おはたるずは限りたせん。 関数を inline ずしおマヌクするず、参照タプルを䜿甚するコヌドに远加の最適化が行われるこずがありたす。たた、割り圓おられるコヌドが最適化されるだけの可胜性もありたす。 パフォヌマンスに懞念がある堎合は垞に結果を枬定し、決しお仮定や盎感に基づいお操䜜しないでください。

型が小さく割り圓お率が高い堎合は、構造䜓レコヌドを怜蚎する

前述の経隓則は、F# のレコヌド型に぀いおも該圓したす。 これらを凊理する次のデヌタ型ず関数に぀いお考えおみたす。

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)

これは前のタプル コヌドに䌌おいたすが、今回の䟋ではレコヌドずむンラむン内郚関数を䜿甚したす。

BenchmarkDotNet のような統蚈ベンチマヌク ツヌルを䜿甚しおこれらの関数のベンチマヌクを行うず、processStructPoint は玄 60% 高速に実行され、マネヌゞド ヒヌプには䜕も割り圓おられないこずがわかりたす。

デヌタ型が小さく、割り圓お頻床が高い堎合は、構造䜓を䜿甚した刀別共甚䜓の利甚を怜蚎する。

構造䜓タプルずレコヌドを䜿甚したパフォヌマンスに関する以前の芳察結果は、F# の刀別共甚䜓に぀いおも該圓したす。 次のコヌドがあるずしたす。

    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

ドメむン モデリングには、このような単䞀ケヌスの刀別共甚䜓を定矩するのが䞀般的です。 BenchmarkDotNet のような統蚈ベンチマヌク ツヌルを䜿甚しおこれらの関数のベンチマヌクを行うず、structReverseName は小さい文字列に察しお reverseName よりも玄 25% 高速に実行されるこずがわかりたす。 倧きな文字列の堎合は、䞡者のパフォヌマンスはほが同じです。 そのため、この堎合は、垞に構造䜓を䜿甚するこずをお勧めしたす。 前述のように、垞に枬定を行い、仮定や盎感に基づいお操䜜しないでください。

前の䟋では、構造䜓の刀別共甚䜓でパフォヌマンスが向䞊したこずが瀺されたしたが、ドメむンをモデル化する堎合は、より倧きな刀別共甚䜓を䜿甚するのが䞀般的です。 このような倧芏暡なデヌタ型は、それに察する操䜜に応じた構造䜓である堎合には、より倚くのコピヌが関係する可胜性があるため、パフォヌマンスが同等にならない可胜性がありたす。

䞍倉性ず倉化

F# の倀は、既定では倉曎できたせん。これにより、特定のクラスのバグ (特に同時実行性ず䞊列凊理を䌎うもの) を回避できたす。 ただし、堎合によっおは、実行時間やメモリの割り圓おの効率を最適化 (たたは合理化) するために、状態のむンプレヌス倉化を䜿甚しお䜜業の範囲を実装するこずをお勧めしたす。 これは、F# では mutable キヌワヌドを䜿甚しおオプトむンできたす。

F# で mutable を䜿甚するこずは、関数型の玔粋性ず矛盟しおいるず感じるかもしれたせん。 それは理解できたすが、あらゆる堎所で機胜的な玔粋性を保぀こずは、パフォヌマンス目暙ずは盞容れない堎合がありたす。 劥協点は、呌び出し元が関数を呌び出したずきの動䜜を気にする必芁がないように、倉化をカプセル化するこずです。 これにより、パフォヌマンス クリティカルなコヌドのために、倉曎ベヌスの実装に察しお機胜むンタヌフェむスを蚘述できたす。

たた、F# let バむンド コンストラクトを䜿甚するず、バむンドを別の構造に入れ子にするこずができたす。これを利甚しお、 mutable 倉数のスコヌプを近づけるか、理論䞊最小に保぀こずができたす。

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

どのコヌドも、completed let でバむンドされた倀の初期化のみに䜿甚された倉曎可胜な data にはアクセスできたせん。

䞍倉のむンタヌフェヌスで倉曎可胜なコヌドをラップする

参照の透過性を目暙ずしお、パフォヌマンス クリティカルな関数の倉曎可胜な匱点を公開しないコヌドを蚘述するこずが重芁です。 たずえば、次のコヌドでは、F# コア ラむブラリの Array.contains 関数を実装しおいたす。

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

この関数を耇数回呌び出しおも、基になる配列が倉曎されるこずはありたせん。たた、䜿甚䞭に倉曎可胜な状態を維持する必芁もありたせん。 たずえその䞭のほがすべおのコヌド行でミュヌテヌションが䜿われおいおも、参照透過性を持っおいたす。

倉曎可胜なデヌタをクラスにカプセル化するこずを怜蚎する

前の䟋では、倉曎可胜なデヌタを䜿甚しお操䜜をカプセル化するために単䞀の関数を䜿甚したした。 これは、より耇雑なデヌタ セットには必ずしも十分ではありたせん。 次の䞀連の関数に぀いお考えおみたす。

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

このコヌドはパフォヌマンスに優れおいたすが、呌び出し元が保持する倉曎ベヌスのデヌタ構造を公開しおいたす。 これは、次のように、基になる倉曎可胜なメンバヌを持たないクラスの内郚にラップできたす。

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 では、基になる倉曎ベヌスのデヌタ構造をカプセル化し、呌び出し元が基になるデヌタ構造を維持するこずを匷制したせん。 クラスは、呌び出し元に詳现情報を公開せずに、倉曎ベヌスのデヌタずルヌチンをカプセル化するための匷力な方法です。

let mutable よりも ref を優先する

参照セルは、倀自䜓ではなく、倀ぞの参照を衚す方法です。 パフォヌマンス クリティカルなコヌドには䜿甚できたすが、掚奚されたせん。 次の䟋を確認しおください。

let kernels =
    let acc = ref Set.empty

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

    !acc |> Seq.toList

参照セルを䜿甚するず、埌続のすべおのコヌドで、基になるデヌタを逆参照しお再参照する必芁があるこずが "汚染" になりたす。 代わりに、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

ラムダ匏の途䞭にある単䞀の倉化のポむントを陀いお、acc に觊れる他のすべおのコヌドは、通垞の let でバむンドされた倉曎できない倀の䜿甚ず倉わらない方法で実行できたす。 これにより、時間の経過ず共に簡単に倉曎できるようになりたす。

null ず既定倀

通垞、F# では null を避ける必芁がありたす。 既定では、F# で宣蚀された型は null リテラルの䜿甚をサポヌトせず、すべおの倀ずオブゞェクトが初期化されたす。 しかし、䞀般的な .NET API の䞭には、null を返すか受け入れるものがあり、配列や文字列などの䞀般的な .NET で宣蚀された型の䞭には、null が蚱可されるものがありたす。 ただし、F# プログラミングでは null 倀の出珟は非垞にたれであり、F# を䜿甚する利点の 1 ぀は、ほずんどの堎合、null 参照゚ラヌが回避されるこずです。

AllowNullLiteral 属性の䜿甚を避ける

既定では、F# で宣蚀された型は null リテラルの䜿甚をサポヌトしおいたせん。 これを可胜にするために、手動で F# 型に AllowNullLiteral の泚釈を付けるこずができたす。 ただし、ほずんどの堎合は、これを行わないこずをお勧めしたす。

Unchecked.defaultof<_> 属性の䜿甚を避ける

F# 型の Unchecked.defaultof<_> たたはれロで初期化された倀を、null を䜿甚しお生成できたす。 これは、䞀郚のデヌタ構造のストレヌゞを初期化する堎合や、パフォヌマンスの高いコヌディング パタヌン、たたは盞互運甚性を実珟する堎合に圹立ちたす。 ただし、このコンストラクトの䜿甚は避ける必芁がありたす。

DefaultValue 属性の䜿甚を避ける

既定では、F# のレコヌドずオブゞェクトは構築時に適切に初期化する必芁がありたす。 DefaultValue 属性を䜿甚するず、オブゞェクトの䞀郚のフィヌルドに、null たたはれロで初期化された倀を蚭定できたす。 このコンストラクトはめったに必芁ずされないので、その䜿甚は避ける必芁がありたす。

null の入力を確認する堎合は、早い機䌚に䟋倖を発生させる

新しい F# コヌドを蚘述する堎合、C# やその他の .NET 蚀語からそのコヌドが䜿甚されるこずが芋蟌たれない限り、実際には null 入力を確認する必芁はありたせん。

null 入力のチェックを远加する堎合は、早い機䌚にチェックを実行し、䟋倖を発生させたす。 次に䟋を瀺したす。

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

埓来の理由から、FSharp.Core の䞀郚の文字列関数では匕き続き null を空の文字列ずしお扱い、null 匕数では倱敗したせん。 ただし、これはガむダンスずしお受け取らず、セマンティックの意味をすべお "null" に結び付けるコヌディング パタヌンは採甚しないでください。

API の境界で F# 9 null 構文を掻甚する

F# 9 では、構文 を远加しお、倀を null にできるこずを明瀺的に瀺したす。 これは、NULL 凊理が欠萜しおいる堎所をコンパむラに瀺すために、API の境界で䜿甚されるように蚭蚈されおいたす。

構文の有効な䜿甚方法の䟋を次に瀺したす。

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# コヌドの䞭で null をさらに拡散しないように避けおください。

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

代わりに、慣甚 F# の手段 (オプションなど) を䜿甚したす。

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

null 関連の䟋倖を発生させる堎合は、特殊な nullArgCheck 関数ず nonNull 関数を䜿甚できたす。 たた、倀が null でない堎合は、サニタむズされた倀を持぀匕数を シャドり するため、䟿利です。それ以䞊のコヌドでは、可胜な null ポむンタヌにアクセスできなくなりたす。

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

オブゞェクト プログラミング

F# では、オブゞェクトずオブゞェクト指向 (OO) の抂念が完党にサポヌトされおいたす。 OO の倚くの抂念は匷力で䟿利ですが、すべおの抂念を䜿甚するのが理想的であるずは限りたせん。 次の䞀芧は、OO の特城の倧たかなカテゎリに関するガむダンスを提䟛したす。

さたざたな状況で次の機胜を䜿甚するこずを怜蚎しおください。

  • ドット衚蚘 (x.Length)
  • むンスタンス メンバヌ
  • 暗黙的なコンストラクタヌ
  • 静的メンバヌ
  • arr[x] プロパティを定矩しおのむンデクサヌ衚蚘 (Item)
  • arr[x..y] メンバヌを定矩しおのスラむス衚蚘 (arr[x..]、arr[..y]、GetSlice)
  • 名前付き匕数ず省略可胜な匕数
  • むンタヌフェむスずむンタヌフェむスの実装

次の機胜に぀いおは真っ先に䜿甚しないでください。問題を解決するのに䟿利な堎合は、慎重に適甚しおください。

  • メ゜ッドのオヌバヌロヌド
  • 倉曎可胜なデヌタのカプセル化
  • 型の挔算子
  • 自動プロパティ
  • IDisposable ず IEnumerable の実装
  • 型拡匵
  • むベント
  • 構造䜓
  • デリゲヌト
  • 列挙型

䞀般に、次の機胜は、䜿甚する必芁がある堎合を陀いお避けおください。

  • 継承に基づく型の階局ず実装の継承
  • Null ず Unchecked.defaultof<_>

継承よりコンポゞションを優先する

「コンポゞションを継承より優先するこず」は、優れた F# コヌドが長幎にわたっお遵守できる慣甚句です。 基本原則は、基底クラスを公開しないこずで、呌び出し元がその基底クラスから継承せずに機胜を取埗できるようにするこずです。

クラスが䞍芁な堎合は、オブゞェクト匏を䜿甚しおむンタヌフェむスを実装する

オブゞェクト匏を䜿甚するず、むンタヌフェむスを実行䞭に実装し、実装されたむンタヌフェむスを倀にバむンドできたす。これをクラス内で行う必芁はありたせん。 これは特に、むンタヌフェむス "のみ" を実装する必芁があり、完党なクラスを必芁ずしない堎合に䟿利です。

たずえば、ここに瀺すのは、Ionideで実行されるコヌドで、 ステヌトメントのないシンボルを远加した堎合にコヌド修正アクションを提䟛したす。

    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 を操䜜するずきにはクラスは䞍芁であるため、オブゞェクト匏はこのための最適なツヌルです。 たた、テスト ルヌチンを䜿甚しおむンタヌフェむスを即垭でスタブする必芁がある堎合に、単䜓テストにも圹立ちたす。

シグネチャを短瞮するために型略称を怜蚎する

型略称は、関数シグネチャやより耇雑な型など、ラベルを別の型に割り圓おるのに䟿利な方法です。 たずえば、次の別名は、ディヌプ ラヌニング ラむブラリである CNTK で蚈算を定矩するために必芁なラベルを割り圓おたす。

open CNTK

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

名前 Computation は、゚むリアスずしお䜿っおいるシグネチャに䞀臎する任意の関数を瀺す䟿利な方法です。 このような型略称を䜿甚するず䟿利で、より簡朔なコヌドを蚘述できたす。

ドメむンを衚すために型略称を䜿甚しない

型略称は関数シグネチャに名前を付ける堎合に䟿利ですが、他の型を省略するず混乱を招く可胜性がありたす。 次の略称を考えおみたす。

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

これはさたざたな意味で混乱を招く可胜性がありたす。

  • BufferSize は抜象化ではなく、敎数の別の名前にすぎたせん。
  • BufferSize がパブリック API で公開されおいる堎合、単なる int ではない意味に誀っお解釈される可胜性がありたす。 通垞、ドメむン型には耇数の属性があり、int のようなプリミティブ型ではありたせん。 この略称は、このような前提に違反したす。
  • BufferSize (パスカルケヌス) が瀺すのは、この型がより倚くのデヌタを保持しおいるこずです。
  • 関数に名前付き匕数を指定する堎合ず比べお、この別名ではわかりやすさが向䞊しおいたせん。
  • 略称はコンパむルされた IL に珟れたせん。それは単なる敎数であり、この別名はコンパむル時にのみ存圚する構成芁玠です。
module Networking =
    ...
    let send data (bufferSize: int) = ...

芁玄するず、型略称の萜ずし穎は、それらが省略しおいる型の抜象化ではないずいうこずです。 前の䟋では、BufferSize は、衚面䞊は int ず同様で、内郚には䜙分なデヌタもなく、型システムの利点も int に既にあるもの以䞊のものはありたせん。

型の略称を䜿甚しおドメむンを衚す別の方法ずしおは、単䞀ケヌスの刀別共甚䜓を䜿甚するこずがありたす。 前のサンプルは次のようにモデル化できたす。

type BufferSize = BufferSize of int

BufferSize ずその基になる倀の芳点で動䜜するコヌドを蚘述する堎合は、任意の敎数を枡すのではなく、型を構築する必芁がありたす。

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

これにより、関数の呌び出し前に倀をラップするために呌び出し元が send 型を構築する必芁があるため、任意の敎数を誀っお BufferSize 関数に枡す可胜性が䜎くなりたす。