25

私は 3 年以上関数型プログラミングに没頭しており、関数型プログラミングの多くの記事や側面を読んで理解してきました。

しかし、私はしばしば、副作用の計算における「世界」に関する記事や、IO モナドのサンプルにおける「世界」の実行とコピーに関する多くの記事に出くわしました。この文脈での「世界」とは何を意味するのでしょうか。これはすべての副作用計算コンテキストで同じ「世界」ですか、それとも IO モナドにのみ適用されますか?

また、Haskell に関するドキュメントやその他の記事では、「世界」について何度も言及されています。

この「世界」に関する参考文献: http://channel9.msdn.com/Shows/Going+Deep/Erik-Meijer-Functional-Programming

そしてこれ: http://www.infoq.com/presentations/Taming-Effect-Simon-Peyton-Jones

ワールドコンセプトの説明だけでなく、サンプルを期待しています。Haskell、F#、Scala、Scheme のサンプル コードを歓迎します。

4

7 に答える 7

50

「世界」とは、「世界の状態」、つまり現在の計算の外側にあるすべての状態を捉えた単なる抽象的な概念です。

たとえば、次の I/O 関数を使用します。

write : Filename -> String -> ()

これは、副作用によってファイル (コンテンツが世界の状態の一部である) を変更するため、機能しません。ただし、世界を明示的なオブジェクトとしてモデル化した場合は、次の関数を提供できます。

write : World -> Filename -> String -> World

これは現在の世界を取り、ファイルが変更された「新しい」世界を機能的に生成し、それを連続した呼び出しに渡すことができます。World 自体は単なる抽象型であり、 のような対応する関数を使用しない限り、それを直接覗く方法はありませんread

ここで、上記のインターフェースには 1 つの問題があります。それ以上の制限がなければ、プログラムは世界を「複製」することができます。例えば:

w1 = write w "file" "yes"
w2 = write w "file" "no"

同じ世界wを 2 回使用して、2 つの異なる未来の世界を作成しました。明らかに、これは物理 I/O のモデルとして意味がありません。そのような例を防ぐには、世界が線形に処理される、つまり、2 回使用されないようにする、より手の込んだ型システムが必要です。言語 Clean は、このアイデアのバリエーションに基づいています。

または、明示的にならないように世界をカプセル化して、構築によって複製できないようにすることもできます。それが I/O モナドが達成することです。これは、状態が世界である状態モナドと考えることができ、モナドアクションを暗黙的に通します。

于 2012-11-12T09:56:07.973 に答える
12

「世界」は、命令型プログラミングを純粋関数型言語に埋め込む一種の概念です。

ご存じのとおり、純粋な関数型プログラミングでは、関数の結果が引数の値だけに依存する必要があります。では、典型的なgetLine操作を純粋な関数として表現したいとします。明らかな問題が 2 つあります。

  1. getLine同じ引数 (この場合は引数なし) で呼び出されるたびに異なる結果を生成できます。
  2. getLineストリームの一部を消費するという副作用があります。プログラムで を使用する場合getLine、(a) の各呼び出しは入力の異なる部分を消費する必要があり、(b) プログラムの入力の各部分は何らかの呼び出しによって消費される必要があります。getLine(その行が入力で 2 回出現しない限り、同じ入力行を 2 回読み取る呼び出しを2 回行うことはできません。また、プログラムに入力行をランダムにスキップさせることもできません。)

だからgetLine、関数にすることはできませんよね?まあ、それほど速くはありませんが、私たちができるいくつかのトリックがあります:

  1. を複数回呼び出すと、getLine異なる結果が返される場合があります。純粋に関数的な動作と互換性を持たせるために、これは、純粋に関数的な動作がgetLine引数を取ることができることを意味します: getLine :: W -> String. W次に、引数に異なる値を使用して各呼び出しを行う必要があることを規定することにより、呼び出しごとに異なる結果が生じるという考えを調整できます。W入力ストリームの状態を表していると想像できます。
  2. への複数の呼び出しはgetLine、明確な順序で実行する必要があり、それぞれが前の呼び出しから残った入力を消費する必要があります。変更: getLinetypeを指定し、プログラムが値を複数回W -> (String, W)使用することを禁止します (コンパイル時にチェックできるもの)。プログラムで複数回W使用するには、前の呼び出しの結果を後続の呼び出しに渡すように注意する必要があります。getLineW

s が再利用されないことを保証できる限り、Wこの種の手法を使用して、(シングルスレッドの) 命令型プログラムを純粋に機能するプログラムに変換できます。その型の実際のメモリ内オブジェクトを用意する必要さえありませんW。プログラムを型チェックして分析し、それぞれWが 1 回しか使用されていないことを証明してから、そのようなものを何も参照しないコードを発行するだけです。 .

したがって、「世界」はこのアイデアにすぎませんが、getLine.


以上のことをすべて説明したので、これを知っておいたほうがよいかどうか疑問に思うかもしれません。私の意見はノーです、あなたはそうではありません。ほら、IMO、「世界を渡す」というアイデア全体は、モナドチュートリアルのようなものの1つであり、実際にはそうではない方法で「役立つ」ことを選択したHaskellプログラマーが多すぎます。

初心者が Haskell IO を理解するのを助けるために、「世界を回る」という言葉が日常的に「説明」として提供されます。しかし、問題は、(a) 多くの人が頭を包み込むのは本当に風変わりな概念であることです (「私が全世界の状態を渡すつもりだとはどういう意味ですか?」)、(b) 非常に抽象的です (ほとんどすべての関数に、ソース コードにもオブジェクト コードにも表示されない未使用のダミー パラメータがあるという考えに、多くの人が頭を悩ませることはできません)。 .

Haskell I/O の最も簡単で実用的な説明である IMHO は次のようになります。

  1. Haskell は純粋に関数型なので、 のようなものは関数にgetLineはなりません。
  2. しかし、Haskell には次のようなものがありgetLineます。これは、それらが関数ではない別のものであることを意味します。それらをアクションと呼びます。
  3. Haskell では、アクションを値として扱うことができます。アクションを生成する関数 (例: putStrLn :: String -> IO ())、アクションを引数として受け入れる関数(例: など) を使用できます(>>) :: IO a -> IO b -> IO b)
  4. ただし、Haskell にはアクションを実行する関数はありません。execute :: IO a -> a真の機能ではないため、存在することはできません。
  5. Haskell には、アクションを構成する組み込み関数があります。単純なアクションから複合アクションを作成します。基本的なアクションとアクション コンビネータを使用すると、命令型プログラムをアクションとして記述できます。
  6. Haskell コンパイラは、アクションを実行可能なネイティブ コードに変換する方法を知っています。main :: IO ()したがって、サブアクションの観点からアクションを作成することにより、実行可能な Haskell プログラムを作成します。
于 2012-11-12T18:30:48.393 に答える
7

「世界」を表す値を渡すことは、純粋な宣言型プログラミングで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に戻します。は端末に印刷されており、渡されてからその間に起こったことも組み込まれています。次に、に渡されます。これにより、返されます(非常に似ていますが、改行の印刷に加えて、その間に発生したその他の効果が組み込まれています)。オペレーティングシステムに戻された世界の最終状態です。TmpWorldInitialWorldInitialWorldmainTmpWorldnlFinalWorldTmpWorldFinalWorldmain

もちろん、私たちはプログラムの価値として宇宙全体を実際に回しているわけではありません。基礎となる実装では、実際に渡すのに役立つ情報がないため、通常、型の値はまったくありません。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。ここで、XYの部分はコンパイラによって自動的に入力され、状態変数は、記述された順序でゴールに「スレッド化」されます。これは、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ランタイムがプログラムを実行することが操作上わかっています。IOputStrLn "Hello world!"putStrLn "Hello World!"IOmainIO () mainmain

これらの純粋なIOモデルを外界との実際の相互作用に接続しているため、少し注意する必要があります。宇宙全体が他の値と同じように渡すことができる値であるのようにプログラミングしています。ただし、他の値を複数の異なる呼び出しに渡したり、ポリモーフィックコンテナーに格納したり、実際のユニバースでは意味をなさない他の多くの値を渡すことができます。したがって、実際に現実の世界で実行できることには対応しないモデルの「世界」で何かを実行できないようにするいくつかの制限が必要です。

Mercuryで採用されているアプローチは、一意のモードを使用して、io値が一意のままであることを強制することです。そのため、入力と出力の世界はそれぞれととして宣言されio::diましio::uoた。ioこれは、最初のパラメーターのタイプがであり、そのモードが( di「破壊的入力」の略)であり、2番目のパラメーターのタイプがでありio、そのモードがuo(「一意の出力」の略)であることを宣言するための省略形です。は抽象型であるためio、新しいものを作成する方法はありません。したがって、一意性の要件を満たす唯一の方法は、常にio最大で1つの呼び出しに値を渡すことです。これにより、一意のio値が返され、出力されます。最後ioに呼び出したものからの最終値。

Haskellで採用されているアプローチは、モナドインターフェイスを使用して、モナドの値IOを純粋なデータや他の値から構築できるようにすることですが、モナドから純粋なデータを「抽出」できる値IOの関数は公開しません。これは、組み込まれた値のみが何も実行しないことを意味し、これらのアクションは正しく順序付けられている必要があります。IOIOIOmain

IO純粋な言語で作業しているプログラマーは、依然としてほとんどのIOについて運用上考える傾向があることを前に述べました。では、命令型プログラマーと同じように考えるだけなら、なぜこのような問題を抱えてIOの純粋なモデルを考え出すのでしょうか。大きな利点は、すべての言語に適用されるすべての理論/コード/すべてがIOコードにも適用されることです。

たとえば、Mercuryでは、fold要素ごとにリストを処理してアキュムレータ値を作成します。これはfold、任意のタイプの変数の入力/出力ペアをアキュムレータとして使用することを意味します(これはMercuryで非常に一般的なパターンです)。標準ライブラリであり、状態変数構文がIO以外のコンテキストで非常に便利であることがよくあると私が言った理由です)。「世界」はマーキュリー計画で型の値として明示的に表示されるため、値をアキュムレータとしてio使用することができます。ioMercuryで文字列のリストを印刷するのは、と同じくらい簡単foldl(print, MyStrings, !IO)です。同様にHaskellでは、ジェネリックモナド/ファンクターコードはIO値。完全に特殊なメカニズムでIOを処理する言語で、IOに特化して新たに実装する必要のある「高次」IO操作が多数発生します。

また、純粋なモデルをIOで壊すことを避けるため、計算モデルに当てはまる理論は、IOが存在する場合でも当てはまります。これにより、プログラマーおよびプログラム分析ツールによる推論では、IOが関与する可能性があるかどうかを考慮する必要がなくなります。たとえば、Scalaのような言語では、多くの「通常の」コードは実際には純粋ですが、コンパイラーはすべての呼び出しにIOまたはその他の効果が含まれる可能性があると想定する必要があるため、純粋なコードで機能する最適化と実装手法は一般に適用できません。


1プログラムを操作上考えるということは、プログラムを実行するときにコンピュータが実行する操作の観点からプログラムを理解することを意味します。

于 2012-11-13T07:12:21.320 に答える
4

このテーマについて最初に読むべきことは、Tackling the Awkward Squadだと思います。(私はそうしませんでした。後悔しています。) 著者は、GHC の内部表現を「ちょっとしたハック」と実際に説明していIOますworld -> (a,world)。この「ハック」は、一種の無邪気な嘘だと思います。ここには 2 種類の嘘があると思います。

  1. GHCは、「世界」が何らかの変数で表現できるふりをしています。
  2. この型world -> (a,world) は基本的に、何らかの方法で世界をインスタンス化できる場合、世界の「次の状態」は、コンピューター上で実行される小さなプログラムによって機能的に決定されることを示しています。これは明らかに実現不可能であるため、他のほとんどの言語と同様に、プリミティブは (もちろん) 副作用のある関数として実装され、無意味な "world" パラメータは無視されます。

著者は、この「ハッキング」を次の 2 つの根拠で弁護します。

  1. IO をタイプ の薄いラッパーとして扱うことによりworld -> (a,world)、GHC は IO コードの多くの最適化を再利用できるため、この設計は非常に実用的で経済的です。
  2. 上記のように実装された IO 計算の操作セマンティクスは、コンパイラが特定のプロパティを満たしていれば、健全であることが証明できます。この論文は、これを証明するために引用されています。

問題 (ここで聞きたかったのですが、最初に質問されたのでここに書いて許してください) は、標準の「遅延 IO」関数の存在下で、GHC の操作セマンティクスが健全なままであるかどうか確信が持てないことです。 .

hGetContents内部呼び出しなどの標準の「遅延 IO」関数は、シングル スレッド プログラムunsafeInterleaveIOと同等 です。unsafeDupableInterleaveIO

unsafeDupableInterleaveIO :: IO a -> IO a
unsafeDupableInterleaveIO (IO m)
     = IO ( \ s -> let  r = case m s of (# _, res #) -> res
                   in  (# s, r #))

等式推論がこの種のプログラムでも機能するふりをして ( m は不純な関数であることに注意してください)、コンストラクターを無視すると、 unsafeDupableInterleaveIO m >>= f==>\world -> f (snd (m world)) worldが得られます。これは意味的に、アンドレアス・ロスバーグが上で説明したのと同じ効果があります。世界。私たちの世界はこのように複製することはできず、Haskell プログラムの正確な評価順序は事実上予測不可能であるため、ファイル ハンドルなどの貴重なシステム リソースに対して、ほとんど制約がなく非同期の同時実行性が競合します。もちろん、この種の操作はAriola&Sabryではまったく考慮されていません。したがって、私はこの点で Andreas に同意しません -- IO モナドはそうではありません'(これが、遅延IOが悪いと言う人がいる理由です)。

于 2012-11-13T13:54:22.970 に答える
2

世界とはまさにそれを意味します - 物理的な、現実の世界です。(1つしかありません、念のため。)

CPU とメモリに限定された物理プロセスを無視することで、すべての機能を分類できます。

  1. 物理的な世界に影響を与えないもの (CPU と RAM での一時的な、ほとんど観察できない影響を除く)
  2. 目に見える効果があるもの。たとえば、プリンターで何かを印刷したり、ネットワーク ケーブルを介して電子を送信したり、ロケットを発射したり、ディスク ヘッドを動かしたりします。

実際には最も純粋な Haskell プログラムを実行しても、 CPU が熱くなり、ファンがオンになるなどの目に見える効果がある限り、この区別は少し人工的です。

于 2012-11-12T10:09:58.353 に答える
1

基本的に、作成するすべてのプログラムは2つの部分に分けることができます(FPワードでは、命令型/ OOの世界ではそのような区別はありません)。

  1. コア/純粋な部分:これは、アプリケーションを構築した問題を解決するために使用される、アプリケーションの実際のロジック/アルゴリズムです。(今日のアプリケーションの95%は、if / elseが散りばめられたAPI呼び出しの混乱であり、人々は自分自身をプログラマーと呼び始めているため、この部分が欠けています)例:画像操作ツールでは、画像にさまざまな効果を適用するアルゴリズムはに属しますこのコア部分。したがって、FPでは、純度などのFPの概念を使用してこのコア部分を構築します。入力を受け取り、結果を返す関数を構築します。アプリケーションのこの部分には何の変化もありません。

  2. 外層部分:画像操作ツールのコア部分を完了し、さまざまな入力で関数を呼び出して出力を確認することでアルゴリズムをテストしたとしましょう。ただし、これは出荷できるものではなく、ユーザーがどのように使用するかを示しています。このコア部分は、その顔はなく、単なる機能の集まりです。エンドユーザーの観点からこのコアをusable作成するには、ある種のUIを構築する必要があります。ディスクからファイルを読み取る方法は、組み込みデータベースを使用してユーザー設定を保存することで、リストは続きます。アプリケーションのコアコンセプトではありませんが、それを使用可能にするために必要な他のさまざまなものとのこの相互作用はworld、FPではと呼ばれます。

演習:以前に作成したアプリケーションについて考え、それを上記の2つの部分に分割してみてください。そうすれば、状況がより明確になります。

于 2012-11-12T09:16:11.093 に答える
1

世界とは、現実世界との相互作用を指します / 副作用があります - たとえば

fprintf file "hello world"

これには副作用があります-ファイルが"hello world"追加されました。

これは、次のような純粋に機能的なコードとは対照的です。

let add a b = a + b

副作用のないもの

于 2012-11-12T09:20:12.533 に答える