「世界」を表す値を渡すことは、純粋な宣言型プログラミングでIO(およびその他の副作用)を実行するための純粋なモデルを作成する1つの方法です。
純粋な宣言型(関数型だけでなく)プログラミングの「問題」は明らかです。純粋な宣言型プログラミングは、計算モデルを提供します。これらのモデルは可能な計算を表現できますが、現実の世界では、プログラムを使用して、理論的な意味での計算ではないことをコンピューターに実行させます。入力の取得、ディスプレイへのレンダリング、ストレージの読み取りと書き込み、ネットワークの使用、ロボットの制御などです。 、など。計算などのほとんどすべてのプログラムを直接モデル化できます(たとえば、この入力が計算である場合にファイルに書き込む出力)が、プログラム外のものとの実際の相互作用は純粋なモデルの一部ではありません。 。
これは、命令型プログラミングにも当てはまります。Cプログラミング言語である計算の「モデル」は、ファイルへの書き込み、キーボードからの読み取りなどの方法を提供しません。しかし、命令型プログラミングの解決策は簡単です。命令型モデルで計算を実行することは、一連の命令を実行することであり、各命令が実際に実行することは、実行時のプログラムの環境全体によって異なります。したがって、実行時にIOアクションを実行する「魔法の」命令を提供するだけです。そして、命令型プログラマーは自分たちのプログラムを運用上考えることに慣れているので1、これは彼らがすでに行っていることに非常に自然に適合します。
しかし、すべての純粋な計算モデルでは、特定の計算単位(関数、述語など)が行うことは、その入力にのみ依存する必要があり、毎回異なる可能性のある任意の環境には依存しません。したがって、IOアクションを実行するだけでなく、プログラムの外部のユニバースに依存する計算を実装することも不可能です。
ただし、ソリューションのアイデアはかなり単純です。純粋な計算モデル全体の中でIOアクションがどのように機能するかについてのモデルを構築します。次に、一般に純粋なモデルに適用されるすべての原則と理論は、IOをモデル化するモデルの一部にも適用されます。次に、言語またはライブラリの実装内で(言語自体では表現できないため)、IOモデルの操作を実際のIOアクションに接続します。
これにより、世界を表す値を渡すことができます。たとえば、Mercuryの「helloworld」プログラムは次のようになります。
:- pred main(io::di, io::uo) is det.
main(InitialWorld, FinalWorld) :-
print("Hello world!", InitialWorld, TmpWorld),
nl(TmpWorld, FinalWorld).
プログラムには、プログラム外のユニバース全体を表すInitialWorld
タイプの値が与えられます。io
この世界を、 「Hello world!」のような世界print
に戻します。は端末に印刷されており、渡されてからその間に起こったことも組み込まれています。次に、に渡されます。これにより、返されます(非常に似ていますが、改行の印刷に加えて、その間に発生したその他の効果が組み込まれています)。オペレーティングシステムに戻された世界の最終状態です。TmpWorld
InitialWorld
InitialWorld
main
TmpWorld
nl
FinalWorld
TmpWorld
FinalWorld
main
もちろん、私たちはプログラムの価値として宇宙全体を実際に回しているわけではありません。基礎となる実装では、実際に渡すのに役立つ情報がないため、通常、型の値はまったくありません。io
それはすべてプログラムの外に存在します。しかし、値を渡すモデルを使用すると、宇宙全体が影響を受けるすべての操作の入力と出力であるかio
のようにプログラムできます(したがって、入力と出力の引数をとらない操作はすべて可能であることがわかります。外界の影響を受けない)。io
実際、通常、IOを実行するプログラムを、あたかも宇宙を通過しているように考えることすらありません。実際のMercuryコードでは、「状態変数」構文糖衣を使用し、上記のプログラムを次のように記述します。
:- pred main(io::di, io::uo) is det.
main(!IO) :-
print("Hello world!", !IO),
nl(!IO).
感嘆符の構文は、!IO
実際には2つの引数とをIO_X
表すことを意味しますIO_Y
。ここで、X
とY
の部分はコンパイラによって自動的に入力され、状態変数は、記述された順序でゴールに「スレッド化」されます。これは、IOのコンテキストで役立つだけでなく、状態変数はMercuryにある非常に便利な構文糖衣です。
したがって、プログラマーは実際には、これを、書き込まれた順序で実行される一連のステップ(外部状態に依存し、外部状態に影響を与える)と考える傾向があります。!IO
これが適用される呼び出しをマークするだけの魔法のタグになります。
Haskellでは、IOの純粋なモデルはモナドであり、「helloworld」プログラムは次のようになります。
main :: IO ()
main = putStrLn "Hello world!"
モナドを解釈する1つの方法は、IO
モナドと同様State
です。状態値を自動的に通過させ、モナド内のすべての値はこの状態に依存するか、この状態に影響を与える可能性があります。マーキュリー計画のように、スレッド化されている状態の場合にのみ、IO
宇宙全体が存在します。Mercuryの状態変数とHaskellの表記法を使用すると、2つのアプローチは非常によく似たものになり、「世界」はソースコードで呼び出された順序を尊重する方法で自動的にスレッド化されますが、IO
明示的にアクションが実行されます。マークされた。
sacundim
の回答で非常によく説明されているように、 HaskellのモナドをIO-y計算のモデルとして解釈する別の方法は、実際には「宇宙」をスレッド化する必要のある計算ではなく、それ自体であると想像することです。実行可能なIOアクションを説明するデータ構造。この理解に関して、モナド内のプログラムが実行していることは、実行時に命令型プログラムを生成するために純粋なHaskellプログラムを使用することです。純粋なHaskellでは、そのプログラムを実際に実行する方法はありませんが、isタイプ自体がそのようなプログラムに評価されるため、Haskellランタイムがプログラムを実行することが操作上わかっています。IO
putStrLn "Hello world!"
putStrLn "Hello World!"
IO
main
IO ()
main
main
これらの純粋なIOモデルを外界との実際の相互作用に接続しているため、少し注意する必要があります。宇宙全体が他の値と同じように渡すことができる値であるかのようにプログラミングしています。ただし、他の値を複数の異なる呼び出しに渡したり、ポリモーフィックコンテナーに格納したり、実際のユニバースでは意味をなさない他の多くの値を渡すことができます。したがって、実際に現実の世界で実行できることには対応しないモデルの「世界」で何かを実行できないようにするいくつかの制限が必要です。
Mercuryで採用されているアプローチは、一意のモードを使用して、io
値が一意のままであることを強制することです。そのため、入力と出力の世界はそれぞれととして宣言されio::di
ましio::uo
た。io
これは、最初のパラメーターのタイプがであり、そのモードが( di
「破壊的入力」の略)であり、2番目のパラメーターのタイプがでありio
、そのモードがuo
(「一意の出力」の略)であることを宣言するための省略形です。は抽象型であるためio
、新しいものを作成する方法はありません。したがって、一意性の要件を満たす唯一の方法は、常にio
最大で1つの呼び出しに値を渡すことです。これにより、一意のio
値が返され、出力されます。最後io
に呼び出したものからの最終値。
Haskellで採用されているアプローチは、モナドインターフェイスを使用して、モナドの値IO
を純粋なデータや他の値から構築できるようにすることですが、モナドから純粋なデータを「抽出」できる値IO
の関数は公開しません。これは、組み込まれた値のみが何も実行しないことを意味し、これらのアクションは正しく順序付けられている必要があります。IO
IO
IO
main
IO
純粋な言語で作業しているプログラマーは、依然としてほとんどのIOについて運用上考える傾向があることを前に述べました。では、命令型プログラマーと同じように考えるだけなら、なぜこのような問題を抱えてIOの純粋なモデルを考え出すのでしょうか。大きな利点は、すべての言語に適用されるすべての理論/コード/すべてがIOコードにも適用されることです。
たとえば、Mercuryでは、fold
要素ごとにリストを処理してアキュムレータ値を作成します。これはfold
、任意のタイプの変数の入力/出力ペアをアキュムレータとして使用することを意味します(これはMercuryで非常に一般的なパターンです)。標準ライブラリであり、状態変数構文がIO以外のコンテキストで非常に便利であることがよくあると私が言った理由です)。「世界」はマーキュリー計画で型の値として明示的に表示されるため、値をアキュムレータとしてio
使用することができます。io
Mercuryで文字列のリストを印刷するのは、と同じくらい簡単foldl(print, MyStrings, !IO)
です。同様にHaskellでは、ジェネリックモナド/ファンクターコードはIO
値。完全に特殊なメカニズムでIOを処理する言語で、IOに特化して新たに実装する必要のある「高次」IO操作が多数発生します。
また、純粋なモデルをIOで壊すことを避けるため、計算モデルに当てはまる理論は、IOが存在する場合でも当てはまります。これにより、プログラマーおよびプログラム分析ツールによる推論では、IOが関与する可能性があるかどうかを考慮する必要がなくなります。たとえば、Scalaのような言語では、多くの「通常の」コードは実際には純粋ですが、コンパイラーはすべての呼び出しにIOまたはその他の効果が含まれる可能性があると想定する必要があるため、純粋なコードで機能する最適化と実装手法は一般に適用できません。
1プログラムを操作上考えるということは、プログラムを実行するときにコンピュータが実行する操作の観点からプログラムを理解することを意味します。