Haskellの興味深い概念の1つは、純粋さです。しかし、これの背後にある実際的な理由は何なのかと思っています。私の質問を拒否する前に、もう少し説明しましょう。
私の主なポイントは、これらをどのように言語にカプセル化しても、副作用があるということです。そのため、Haskellのようなプログラミング言語が純粋であるとしても、副作用を「延期」するだけです。
純粋であることでプログラムについての推論が容易になるという考えを理解しています(参照の透明性など)。 Haskellでは、すべての副作用を型システムにカプセル化します(IOモナド)。したがって、IO =いわば、不純な関数を呼び出すすべての関数のタイプをモナドで説明します。
そのため、型システムで純粋な関数を不純な関数から分離するという考えに非常に熱心です。これは、そのような影響を制限するように強制するための良い方法のようです。
それでも、Haskellはその純粋な性質のためにしばしば打ちのめされています-人々はそれが実用的でも実用的でもないと言います。これが本当かどうかは別問題ですが、多くの人がこの理由でHaskellを拒否しています。
私にとって、主な勝利点は純粋そのものではなく、純粋なコードと不純なコードの分離にあるようです。モナドでIOをカプセル化するのが非現実的である場合は、もっと簡単な方法を試してみませんか?
代わりに、システム内のすべての戻り値の型と引数の型に純粋または不純のラベルを付けたとしたらどうでしょう。 D言語には明示的な「純粋な」キーワードがありますが、デフォルトでは関数を純粋にして、純粋でない関数のみをマークすると考えています。
MLのような言語では、これは次のようになります。
let square x = x * x
let impure getDbData connectionString = ...
let impure getMappedDbData mapping =
let dbData = getDbData "<some connection info>"
in map mapping dbData
let main = // Compiler error, as it is not marked as impure
let mappedData = getMappedData square
in ...
さらに、不純な関数を呼び出すと、呼び出し側の関数の戻り値の型が「汚染」され、不純になります。そのようにラベル付けされていない場合、コンパイルエラーが発生します。
(もちろん、引数として関数を渡すことは、期待される純度に対してもチェックする必要がありますが、引数の期待される「純度」を推測することは簡単であるべきです)
(「不純」なラベルも推測できますが、読みやすくするために、このラベルを明示的にしておくことをお勧めします)
一方、純粋な関数は、期待される純度に関係なく、どこにでも渡すことができます。純粋でない関数を受け入れる関数への入力として、純粋な関数を与えることができます。したがって、それは対称的な関係ではありません-したがって「汚染」という言葉。
全体として、これはコードベースを純粋な部分と純粋でない部分に明示的に(そして静的にチェックして)分離します。多くの点で、これはIOモナドの機能に近いですが(非対称性によりおそらく2つの間の1対1のマッピングは除外されます)、暗黙のアンラップが自動的に行われます。最も重要なこととして、多くのプログラマーは、明示的なIOモナドまたは類似の何かよりも直感的で実用的であると感じます。
そして、完全に不純なソリューションとは異なり、これは純粋な部品について推論するためのより多くの安全性を提供します(不純なものを定義するときに、私たちの邪魔をしすぎないでください)。そして全体として、それはほとんどの場合、ほとんどの関数型プログラマーがとにかくベストプラクティス(分離)と見なすものの静的に強制されたバージョンです。
上記のアプローチのようなものが試されたことはありますか?そうでない場合、そうでない理由はありますか(技術的、実用的、理論的、および哲学的な理由の両方が興味深い)?
そして最後に、そのような純度チェックは、Haskellが非現実的であると主張する人々にとって受け入れやすいでしょうか?
私は個人的にHaskellへの恨みを抱いたり、IOにモナドを使用したりする選択をしていません。実際、Haskellは使用されている最もエレガントな言語の1つであり、おそらく私が最も気に入っている言語の1つだと思います。
さらに、上記のアプローチがIOモナドが提供しないものを提供していないことは明らかです-唯一のことは、それがOCamlやF#などの不純な言語にもっと密接に(視覚的に)似ているということです実際には、既存のF#コードを取得して、いくつかの「不純な」ラベルを追加することが、コードの唯一の視覚的な違いになります。
したがって、これは、Haskellが持っているモナドを取り巻く生態系と同じエコシステムを持たない言語の純粋さへの簡単なステップになる可能性があります。
エフェクトシステム について説明しました。モナド以外にもエフェクトシステムがあることは事実ですが、実際には、モナドは、考案する可能性のある実際的なエフェクトシステムで再発明する必要がある多くの表現力を提供します。
例えば:
io
エフェクトをタグ付けします。io
ではないため、exn
エフェクトを追加します。次に、エフェクトを組み合わせることができるようにする必要があります。h
で内部的にミューテーションを使用する非I/O関数を使用したいのですが、コンパイラーに変更可能な参照がスコープをエスケープしないようにしたいので、_st<h>
_効果。次に、パラメーター化されたエフェクトが必要です。map
と不純な関数を使用するmap
などです。今、私は効果多型が必要です。モナドはこれらすべてのユースケースをサポートします:
Either
モナドを使用できます。EitherT IO
_モナドを使用できます。ST h
_モナドを使用すると、ヒープSTRef
のスコープ内のh
sでローカル変更を行うことができます。Monad
タイプクラスのため、操作をオーバーロードしてあらゆる種類の効果を処理できます。たとえば、mapM :: (Monad m) => (a -> m b) -> [a] -> m [b]
はIO
で機能しますが、Either
やST
やその他のモナドでも機能します。IO
やST
などを除いて、すべてのモナドはユーザー定義型として実装されます。もちろん、Haskellの副作用のためのモナドの使用には、いくつかの重要ないぼがあります。
1つには、モナド変換子は一般に可換ではないため、_StateT Maybe
_は_MaybeT State
_とは異なり、実際に意味する組み合わせを選択するように注意する必要があります。
より大きな問題は、モナドが使用されている関数は多態性である可能性がありますが、かどうかモナドが使用されている場合は多態性である可能性がないことです。したがって、重複したコードのロードを取得します:map
対mapM
; zipWith
対zipWithM
、&c。
さて、あなたの実際の質問に答えて…
Koka マイクロソフトリサーチの言語は、これらの問題に悩まされない効果システムを備えた言語です。たとえば、Kokaのmap
関数のタイプには、エフェクト用のタイプパラメータe
が含まれています。
_map : (xs : list<a>, f : (a) -> e b) -> e list<b>
_
また、このパラメーターをnull効果にインスタンス化して、map
を純粋な関数と不純な関数の両方で機能させることができます。さらに、効果が指定される順序は重要ではありません:_<exn,io>
_は_<io,exn>
_と同じです。
Javaのチェック例外メカニズムは、人々が広く受け入れているエフェクトシステムの例であることにも言及する価値がありますが、それは一般的ではないため、あなたの質問に適切に答えるとは思いません。
あなたはモナドを再発明したと思います!ここにあるものを見てみましょう。純粋な計算を暗黙的に「ダーティ」にして不純な計算で使用し、不純な関数から不純な関数を呼び出すことができます。
モナドのように聞こえますが、return
を使用して純粋な値をダーティにし、不純な関数から別の関数を呼び出すことができます。>>=
apply :: (a -> IO b) -> IO a -> IO b
apply f a = a >>= f
さて、あなたのアプローチは他の言語の人々にとってもう少し慣れ親しんでいます。私たちは皆、何が起こっているのかを理解するために頭を駆け巡る何らかの純度チェックを持っています。
不利な点は、これらを言語レベルの概念にすることで、構築を非常に難しくしたことです。 Haskellには、すべてのモナドで機能する通常のユーザー定義関数のライブラリ全体があります。 IO
を言語に焼き込んだ場合、これらの関数をすべてプリミティブにする必要があります。
純粋さがどのように処理されるかについてのダークマジックの量を最小限に抑えることで、ユーザーランドでの作業を実際に簡単にすることができます。