純粋に関数型の言語では、「i <- 3」などのコマンドが不変変数 i を直接代入する代わりに、「<-」などの「代入」演算子を定義することはできませんでした。新しいコール スタックで i を 3 に置き換え、その時点から新しいコール スタックを実行することを除いて、現在のコール スタック全体のコピー? 実際に変更されたデータがないことを考えると、それは定義上「純粋に機能的」と見なされませんか? もちろん、コンパイラは i に 3 を代入するだけの最適化を行います。この場合、命令型と純粋関数型の違いは何でしょう?
3 に答える
Haskell などの純粋関数型言語には、命令型言語をモデル化する方法があり、それを認めることをためらいません。:)
http://www.haskell.org/tutorial/io.html、特に 7.5を参照してください。
結局、Haskell は単に命令型車輪を再発明しただけなのでしょうか?
ある意味、そうです。I/O モナドは、Haskell 内の小さな命令型サブ言語を構成するため、プログラムの I/O コンポーネントは、通常の命令型コードと同様に見える場合があります。ただし、重要な違いが 1 つあります。ユーザーが対処する必要がある特別なセマンティクスはありません。特に、Haskell の等式推論は損なわれていません。プログラム内のモナド コードの命令的な感覚は、Haskell の機能面を損なうものではありません。経験豊富な関数型プログラマーは、プログラムの命令型コンポーネントを最小限に抑え、トップレベルのシーケンシングを最小限に抑えるために I/O モナドのみを使用できるはずです。モナドは、機能的なプログラム コンポーネントと命令的なプログラム コンポーネントを明確に分離します。対照的に、
したがって、関数型言語の価値は、状態の変更を不可能にすることではなく、プログラムの純粋に機能的な部分を状態を変更する部分から分離できるようにする方法を提供することです。
もちろん、これを無視してプログラム全体を命令型スタイルで書くこともできますが、そうすると言語の機能を利用できなくなります。
アップデート
あなたの考えはあなたが思っているほど間違っていません。まず、命令型言語しか知らない人が整数の範囲をループしたい場合、カウンターをインクリメントする方法なしでどのようにこれを達成できるのか不思議に思うかもしれません。
しかしもちろん、代わりにループの本体として機能する関数を作成し、それ自体を呼び出すようにします。関数の各呼び出しは「反復ステップ」に対応します。また、各呼び出しのスコープでは、パラメーターは異なる値を持ち、インクリメント変数のように機能します。最後に、ランタイムは、再帰呼び出しが呼び出しの最後に表示されることを認識することができるため、関数呼び出しスタックを成長させる代わりに、その一番上を再利用できます (末尾呼び出し)。この単純なパターンでさえ、コンパイラ/ランタイムが静かに介入し、実際に突然変異を発生させる (スタックのトップを上書きする) など、アイデアのフレーバーのほとんどすべてを備えています。これは、変化するカウンターを使用したループと論理的に同等であるだけでなく、実際には、CPU とメモリが物理的に同じことを行うようにします。
GetStack
現在のスタックをデータ構造として返すa について言及しています。呼び出されるたびに (引数なしで) 必ず異なるものを返すことを考えると、これは確かに関数の純粋性に違反します。しかしCallWithStack
、独自の関数を渡す function はどうですか?関数はコールバックし、現在のスタックをパラメーターとして渡しますか? それはまったく問題ありません。CallCCはそのように機能します。
Haskell はコール スタックをイントロスペクトまたは「実行」する方法をすぐには提供しないので、その特定の奇妙なスキームについてあまり心配する必要はありません。ただし、一般に、などの安全でない「関数」を使用して型システムを覆すことができるのはunsafePerformIO :: IO a -> a
事実です。アイデアは、純度を侵害することを不可能ではなく困難にすることです.
実際、C ライブラリの Haskell バインディングを作成する場合など、多くの状況で、これらのメカニズムは非常に必要です。これらのメカニズムを使用することで、コンパイラから純粋性を証明する負担を取り除き、自分でそれを引き受けることになります。
型システムのそのようなサブバージョンを非合法化することにより、実際に安全性を保証するという提案があります。私はあまり詳しくありませんが、ここで読むことができます。
不変性は言語の特性であり、実装の特性ではありません。
データをコピーする操作は、その場所を参照する値がプログラマーの観点から変更されたように見えるa <- expr
場合でも、必須の操作です。a
同様に、純粋な関数型言語の実装は、各変更がプログラマーに見えない限り、変数を心ゆくまで上書きして再利用することができます。たとえば、map
関数は原則として、新しいリストを作成する代わりにリストを上書きできます。これは、言語実装が古いリストがどこにも必要ないと推測できる場合はいつでも可能です。