6

だから私はFPを学ぼうとしていて、参照透過性と副作用について頭を悩ませようとしています。

型システムですべての効果を明示的にすることが、参照透過性を保証する唯一の方法であることを学びました。

「ほぼ関数型プログラミング」という考えは実現不可能です。暗黙的な副作用を部分的に取り除くだけでは、命令型プログラミング言語をより安全にすることは不可能です。多くの場合、1 種類のエフェクトを残すだけで、削除しようとしたエフェクトそのものをシミュレートするのに十分です。一方、純粋な言語で効果を「忘れる」ことを許可すると、それ自体が混乱を引き起こします。

残念ながら、ゴールデン ミドルは存在せず、古典的な二分法に直面しています。排除されたミドルの呪いは、次のいずれかの選択を提示します。依然として根本的に効果的です。または (b) 型システムですべての効果を明示し、実用的にすることで純粋性を完全に受け入れる -ソース

また、Scala や F# などの非純粋な FP 言語では参照透過性を保証できないことも学びました。

参照透過性を強制する機能は、Java と相互運用可能なクラス/オブジェクト システムを持つという Scala の目標とほとんど相容れません。-ソース

そして、非純粋な FP では、参照透過性を確保するのはプログラマ次第です。

ML、Scala、F# などの非純粋な言語では、プログラマーが参照透過性を確保する必要があります。もちろん、Clojure や Scheme などの動的型付け言語では、参照透過性を強制する静的型システムはありません。-ソース

.Net の経験があるので F# に興味があります。次の質問は次のとおりです。

F# コンパイラによって適用されない場合、F# アプリケーションで参照透過性を保証するにはどうすればよいですか?

4

3 に答える 3

7

この質問に対する簡単な答えは、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はモナドのバインド関数であり、効果をシーケンス化できます。

runIO を実行して、含まれているすべての効果を実行します。これはunsafePerformIOHaskell のようなものです。

次に、これらのプリミティブ関数を使用して計算式ビルダーを定義し、多くの素晴らしい構文糖衣を自分自身に与えることができます。


もう 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# に対する私の実践的で経験豊富な見解は、実際には、「ほぼ関数型」のプログラミングは、ほぼ常に競合他社よりも大幅に改善されていることを示しています。

于 2016-08-19T17:14:42.020 に答える
0

Mark Seemann がコメントで確認したように、「F# では参照透過性を保証できるものはありません。これについて考えるのはプログラマ次第です。」

私はオンラインでいくつかの検索を行っており、「規律はあなたの親友」であり、F# アプリケーションの参照透過性のレベルを可能な限り高く保つためのいくつかの推奨事項を見つけました。

  • ミュータブル、for または while ループ、ref キーワードなどは使用しないでください。
  • 純粋に不変のデータ構造 (判別共用体、リスト、タプル、マップなど) に固執します。
  • ある時点で IO を実行する必要がある場合は、純粋に機能的なコードから分離されるようにプログラムを設計してください。関数型プログラミングは、副作用の制限と分離がすべてであることを忘れないでください。
  • 代数データ型 (ADT) は、オブジェクトの代わりに「識別共用体」として知られています。
  • 怠惰を愛することを学ぶ。
  • モナドを抱きしめる。
于 2016-08-19T16:29:14.487 に答える