12

最近、関数型プログラミングについて質問したところ、 (良い!) 回答が返ってきて、さらに多くの質問が寄せられました (学習の場合もそうであるように思われます)。以下にいくつかの例を示します。

  1. 1 つの回答は、不変データ構造の利点に言及しています。各スレッドは独自のコピーを持つことができます。私には、これはどちらかというとバージョン管理システムのように思えますが (類推すると)、誰かがチェックアウトしたコードをロックして他の人が変更できないようにするのではなく、誰もが自分のコピーをチェックアウトできます。いいですね。ただし、VCS では、2 人が同じものを変更した場合に備えて、変更を「マージ」するという概念があります。この問題は確かにマルチスレッドのシナリオで発生する可能性があるようです...では、スレッドが最新のデータを見ることが重要な場合、「マージ」はどのように行われるのでしょうか?

  2. この回答では、オブジェクトのループで操作が実行されている場合と、古いオブジェクトを更新する代わりに毎回新しいオブジェクトを使用する方法について説明しました。ただし、bankAccountたとえば、GUI バンキング システムなど、非ループ シナリオで が更新されているとします。オペレーターが [金利の変更] ボタンをクリックすると、(C# などで) のようなイベントが発生しますbankAccount.InterestRate = newRateFromUser。私はここで密集しているように感じますが、うまくいけば私の例は理にかなっています: オブジェクトが更新される何らかの方法が必要ですよね? 他にもいくつかのことが新しいデータに依存する場合があります。

とにかく、パラダイム シフトについて頭を悩ませるのを手伝ってくれたら、ありがたいです。コーディングへの単純な手続き型の命令型アプローチの背景を理解した後、OOP を学習するときに、私の脳が同様の「愚かな段階」を経験したことを覚えています。

4

6 に答える 6

7

.Net の String クラス (不変オブジェクト) について考えてみてください。文字列に対してメソッドを呼び出すと、新しいコピーが取得されます。

String s1 = "there";
String s2 = s1.Insert(0, "hello ");

Console.Writeline("string 1: " + s1);
Console.Writeline("string 2: " + s2);

これは出力されます:

文字列 1: そこ

文字列 2: こんにちは

この動作を、基本的に同じメソッド シグネチャを持つ StringBuilder と比較します。

StringBuilder sb  = new StringBuilder("there");
StringBuilder sb2 = sb.Insert(0, "hi ");

Console.WriteLine("sb 1: " + sb.ToString());
Console.WriteLine("sb 2: " + sb2.ToString());

StringBuilder は変更可能であるため、両方の変数が同じオブジェクトを指しています。出力は次のようになります。

sb 1: こんにちは

sb 2: こんにちは

したがって、作成した文字列を変更することは絶対にできません。s1 は、時間の終わりまで (またはガベージが収集されるまで) 常に「存在」します。これはスレッド化において重要です。なぜなら、いつでも各文字をステップ実行して、その値が常に「そこに」出力されることがわかっているため、その値を出力できるからです。作成後に StringBuilder の出力を開始した場合、そこの最初の 2 文字を出力して「th」を取得することがあります。ここで、別のスレッドが「hi」という広告を挿入したとします。価値が違う!3 番目の文字を印刷すると、「hi」のスペースになります。したがって、「th there」と出力します。

于 2008-12-11T23:14:19.403 に答える
6

パート 1 への回答: 不変オブジェクト自体は、2 つのスレッドの更新の結果を結合できるようにする「マージ」などをサポートしていません。そのためには、悲観的な戦略と楽観的な戦略の 2 つがあります。悲観的な人は、2 つのスレッドが同じデータを同時に更新しようとする可能性が非常に高いと考えます。したがって、最初のスレッドが終了したと言うまで 2 番目のスレッドがフリーズするように、ロックを採用します。これがめったに起こらないと楽観的に考えている場合は、両方のスレッドがデータの独自の論理コピーで動作するようにします。最初に終了したスレッドは新しいバージョンを提供し、もう一方は最初からやり直す必要があります。最初のスレッドの変更の結果から開始するだけです。ただし、この高価な再起動はたまにしか発生しませんが、

パート 2: 純粋な関数型ステートレス言語は、その問題を実際に排除するわけではありません。純粋な Haskell プログラムでさえ、それに関連付けられた状態を持つことができます。違いは、ステートフル コードの戻り値の型が異なることです。状態を操作する関数は、その状態を表すオブジェクトを操作する一連の操作として表現されます。ばかげた例として、コンピューターのファイル システムを考えてみましょう。プログラムがファイルの内容を (たとえ 1 バイトでも) 変更するたびに、ファイル システム全体の新しい「バージョン」が作成されます。そしてひいては、宇宙全体の新しいバージョンです。ただし、ここではファイル システムに注目しましょう。ファイルシステムを検査するプログラムの他の部分は、その変更されたバイトの影響を受ける可能性があります。そのため、Haskell は、ファイル システム上で動作する関数は、ファイル システムのバージョンを表すオブジェクトを効果的に渡す必要があると述べています。次に、これを手動で処理するのは面倒なので、要件を裏返しにして、関数が IO を実行できるようにしたい場合は、一種のコンテナー オブジェクトを返す必要があると述べています。コンテナー内には、関数が返したい値があります。しかし、コンテナは、関数にも副作用があるか、副作用が見える可能性があるという証拠として機能します。これは、Haskell の型システムが、副作用のある関数と「純粋な」関数を区別できることを意味します。そのため、実際にはコードのステートフル性を排除することなく、コードのステートフル性を保持および管理するのに役立ちます。要件を裏返しにして、関数が IO を実行できるようにしたい場合は、一種のコンテナ オブジェクトを返す必要があると述べています。コンテナー内には、関数が返したい値があります。しかし、コンテナは、関数にも副作用があるか、副作用が見える可能性があるという証拠として機能します。これは、Haskell の型システムが、副作用のある関数と「純粋な」関数を区別できることを意味します。そのため、実際にはコードのステートフル性を排除することなく、コードのステートフル性を保持および管理するのに役立ちます。要件を裏返しにして、関数が IO を実行できるようにしたい場合は、一種のコンテナ オブジェクトを返す必要があると述べています。コンテナー内には、関数が返したい値があります。しかし、コンテナは、関数にも副作用があるか、副作用が見える可能性があるという証拠として機能します。これは、Haskell の型システムが、副作用のある関数と「純粋な」関数を区別できることを意味します。そのため、実際にはコードのステートフル性を排除することなく、コードのステートフル性を保持および管理するのに役立ちます。s 型システムは、副作用のある関数と「純粋な」関数を区別できます。そのため、実際にはコードのステートフル性を排除することなく、コードのステートフル性を保持および管理するのに役立ちます。s 型システムは、副作用のある関数と「純粋な」関数を区別できます。そのため、実際にはコードのステートフル性を排除することなく、コードのステートフル性を保持および管理するのに役立ちます。

于 2008-12-11T23:29:43.367 に答える
4

#2に関して...

他のいくつかのことが新しいデータに依存する可能性があります。

これは純粋主義者が「効果」と呼ぶものです。同じ可変オブジェクトへの複数のオブジェクト参照の概念は、可変状態の本質であり、問​​題の核心です。OOPでは、タイプBankAccountのオブジェクト「a」がある場合があります。a.Balanceなどを異なる時間に読み取ると、異なる値が表示される場合があります。対照的に、純粋FPでは、「a」のタイプがBankAccountの場合、不変であり、時間に関係なく同じ値になります。

ただし、BankAccountはおそらくモデル化したいオブジェクトであり、その状態は時間とともに変化するため、FPではその情報をタイプにエンコードします。したがって、「a」は「IO BankAccount」タイプ、または本質的に「a」を実際に「世界の以前の状態」(または銀行の以前の金利の状態)を入力として受け取る関数にする他のモナディックタイプを持つ可能性があります。 、または何でも)、そして世界の新しい状態を返します。金利の更新は、効果を表すタイプの別の操作(たとえば、別のIO操作)であり、したがって、新しい「ワールド」を返します。金利に依存する可能性のあるすべてのものは、その世界を入力として受け取る必要があることを知っているタイプ。

その結果、「a.Balance」などを呼び出す唯一の可能な方法は、静的型のおかげで、「これまでの世界の歴史」が適切に計画されていることを強制するコードを使用することです。呼び出し、および入力であるワールドヒストリーは、a.Balanceから取得する結果に影響します。

State Monadを読むと、「共有された可変状態」を純粋にモデル化する方法を理解するのに役立つ場合があります。

于 2008-12-12T00:39:36.707 に答える
3

MVCC (マルチバージョン同時実行制御)

あなたが参照している問題の解決策は、Rich Hickey のビデオ プレゼンテーションで説明されています。

要するに、クライアントに直接参照によってデータを渡す代わりに、間接的にもう 1 つのレベルを追加し、データへの参照への参照を渡します。(実際には、少なくとももう 1 つのレベルの間接化が必要です。しかし、データ構造が「配列」のように非常に単純であると仮定しましょう。 )
データは不変であるため、データを変更する必要があるたびに、変更された部分のコピーを作成します (配列の場合は、別の配列を作成する必要があります! ) さらに、すべての「変更された」データへの別の参照を作成します。
したがって、配列の最初のバージョンを使用したすべてのクライアントでは、最初のバージョンへの参照が使用されます。2 番目のバージョンにアクセスしようとするすべてのクライアントは、2 番目の参照を使用します。
「配列」データ構造は、データを分割できず、すべてをコピーする必要があるため、この方法ではあまり興味深いものではありません。しかし、ツリーのようなより洗練されたデータ構造の場合、データ構造の一部を「共有」できるため、毎回すべてをコピーする必要はありません。

詳細については、Chris Okasaki による「Purely Functional Data Structures」という論文をご覧ください。

于 2010-02-17T08:51:02.617 に答える
3
  1. 不変データ構造は VCS とは異なります。不変データ構造を読み取り専用ファイルと考えてください。読み取り専用の場合、いつでも誰がファイルのどの部分を読んでいても、誰もが正しい情報を読むことができます。

  2. その答えはhttp://en.wikipedia.org/wiki/Monad_(functional_programming)について話している

于 2008-12-11T21:54:53.590 に答える
1

「不変」とは、まさにそれを意味します。変更されません。

関数型プログラムが更新を行う方法は、新しいものを渡すことです。既存の値が変更されることはありません。新しい値を作成して代わりに渡すだけです。多くの場合、新しい価値の共有は古い価値の共有と同じです。この手法の良い例は、コンス セルで構成されたリストとジッパーです。

于 2008-12-11T23:21:27.423 に答える