この質問に対する簡単な答えは、F# で参照透過性を保証する方法がないということです。F# の大きな利点の 1 つは、他の .NET 言語との優れた相互運用性があることですが、Haskell のようなより孤立した言語と比較して、副作用があり、それに対処しなければならないという欠点があります。
F# で副作用を実際にどのように処理するかは、まったく別の問題です。
実際には、Haskell の場合とほぼ同じ方法で F# の型システムに効果を持ち込むことを妨げるものは何もありませんが、実際には、強制されるのではなく、このアプローチに「オプトイン」しています。
本当に必要なのは、次のようなインフラストラクチャだけです。
/// A value of type IO<'a> represents an action which, when performed (e.g. by calling the IO.run function), does some I/O which results in a value of type 'a.
type IO<'a> =
private
|Return of 'a
|Delay of (unit -> 'a)
/// Pure IO Functions
module IO =
/// Runs the IO actions and evaluates the result
let run io =
match io with
|Return a -> a
|Delay (a) -> a()
/// Return a value as an IO action
let return' x = Return x
/// Creates an IO action from an effectful computation, this simply takes a side effecting function and brings it into IO
let fromEffectful f = Delay (f)
/// Monadic bind for IO action, this is used to combine and sequence IO actions
let bind x f =
match x with
|Return a -> f a
|Delay (g) -> Delay (fun _ -> run << f <| g())
return
内に値をもたらしますIO
。
fromEffectful
副作用関数を取り、unit -> 'a
それを 内に持ってきますIO
。
bind
はモナドのバインド関数であり、効果をシーケンス化できます。
run
IO を実行して、含まれているすべての効果を実行します。これはunsafePerformIO
Haskell のようなものです。
次に、これらのプリミティブ関数を使用して計算式ビルダーを定義し、多くの素晴らしい構文糖衣を自分自身に与えることができます。
もう 1 つの価値のある質問は、これは F# で役立つかということです。
F# と Haskell の基本的な違いは、F# はデフォルトで熱心な言語であるのに対し、Haskell はデフォルトで遅延言語であることです。Haskell コミュニティ (および .NET コミュニティも、程度は低いと思います) は、遅延評価と副作用/IO を組み合わせると、非常に悪いことが起こる可能性があることを学びました。
Haskell の IO モナドで作業する場合、(一般的に) IO のシーケンシャルな性質について何かを保証し、IO の 1 つの部分が別の部分の前に確実に行われるようにします。また、影響が発生する頻度と時期についても保証しています。
私が F# でポーズをとるのが好きな 1 つの例は、次のとおりです。
let randomSeq = Seq.init 4 (fun _ -> rnd.Next())
let sortedSeq = Seq.sort randomSeq
printfn "Sorted: %A" sortedSeq
printfn "Random: %A" randomSeq
一見すると、このコードはシーケンスを生成し、同じシーケンスをソートしてから、ソートされたバージョンとソートされていないバージョンを出力するように見えるかもしれません。
そうではありません。2 つのシーケンスが生成され、1 つはソートされ、もう 1 つはソートされていません。それらは完全に異なる値を持つことができ、ほぼ確実にそうします。
これは、副作用と参照透過性なしの遅延評価を組み合わせた直接的な結果です。which を使用Seq.cache
することで、繰り返し評価を防ぐことができますが、効果がいつ、どのような順序で発生するかを制御することはできません。
対照的に、熱心に評価されたデータ構造を操作している場合、結果は一般的にそれほど目立たないため、F# での明示的な効果の要件は Haskell に比べて大幅に削減されていると思います。
とはいえ、型システム内ですべての効果を明示的にすることの大きな利点は、優れた設計を強制するのに役立つことです。Mark Seemann のような人は、オブジェクト指向であろうと関数型であろうと、堅牢なシステムを設計するための最良の戦略には、システムのエッジで副作用を分離し、参照透過性と高度に単体テスト可能なコアに依存することが含まれると教えてくれます。
明示的な効果とIO
型システムで作業していて、すべての関数が最終的に で記述されているIO
場合、それは強くて明白な設計臭です。
これが F# で価値があるかどうかという最初の質問に戻ると、私はまだ「わからない」と答えなければなりません。私は、この可能性を自分で探るために、F# の参照透過効果のライブラリに取り組んできました。IO
興味があれば、このテーマに関するより多くの資料と、より完全な実装があります。
最後に、排除された中間の呪いはおそらく、典型的な開発者よりもプログラミング言語の設計者をターゲットにしていることを覚えておく価値があると思います。
不純な言語で作業している場合は、副作用に対処して飼いならす方法を見つける必要があります。これを行うために従う正確な戦略は、解釈の余地があり、あなた自身および/またはあなたのニーズに最も適したものです。チームですが、F# はこれを行うためのツールをたくさん提供してくれると思います。
最後に、F# に対する私の実践的で経験豊富な見解は、実際には、「ほぼ関数型」のプログラミングは、ほぼ常に競合他社よりも大幅に改善されていることを示しています。