関数型プログラミング:並行性と状態に関する正しいアイデア?


21

FP支持者は、パラダイムが可変状態を回避するため、並行性は簡単であると主張しています。わかりません。

FPを使用して、純粋な機能と不変のデータ構造を強調するマルチプレイヤーダンジョンクロール(ローグライクゲーム)を作成していると想像してください。部屋、廊下、ヒーロー、モンスター、戦利品で構成されるダンジョンを生成します。私たちの世界は、事実上、構造とそれらの関係のオブジェクトグラフです。物事が変化すると、世界の表現はそれらの変化を反映するように修正されます。私たちのヒーローはネズミを殺したり、短剣を拾ったりします。

私にとって、世界(現在の現実)はこの状態の考え方を持ち、FPがこれをどのように克服するのか見逃しています。ヒーローが行動を起こすと、機能が世界の状態を修正します。すべての決定(AIまたは人間)は、現在の世界の状態に基づいて行う必要があるようです。並行性はどこで許可しますか?複数のプロセスが並行して世界の状態を修正することはできません。1つのプロセスの結果が期限切れの状態に基づいていることはありません。世界の現在のオブジェクトグラフで表される現在の状態を常に処理しているように、すべての制御は単一の制御ループ内で行われるべきだと感じています。

明らかに、並行性に完全に適した状況があります(つまり、状態が互いに独立している孤立したタスクを処理する場合)。

私の例では並行性がどのように役立つかを確認できず、それが問題になる可能性があります。私は何らかの形で主張を誤って伝えているかもしれません。

誰かがこの主張をよりよく表現できますか?


1
共有状態を参照しています。共有状態は常にそれであり、常に何らかの形式の同期を必要とします。純粋なFPの人々の間でしばしば好まれる形式はSTMです。条件は自動的に処理されます。共有メモリのための別の技術ではなく、共有メモリを有していると、あなたは自分のローカルメモリを依頼するためのローカルメモリや他の俳優の知識を持っているメッセージパッシングである
ジミー・ホッファ

1
だから...あなたは、共有状態の同時実行が簡単であることが、シングルスレッドアプリケーションの状態管理にどのように役立つかを尋ねていますか?一方、あなたの例は、そのように実装されているかどうかに関係なく、概念的に並行性(AIが制御する各エンティティのスレッド)に適しています。ここであなたが何を求めているのか混乱しています。
CAマッキャン

1
言葉では、ジッパー
JK。

2
すべてのオブジェクトには、独自の世界観があります。最終的な一貫性があります。それはおそらく、波動関数が崩壊する「現実の世界」で物事がどのように機能するかです。
ヘルツマイスター

1
:あなたは「純粋に機能retrogames」面白いかもしれませんprog21.dadgum.com/23.html
user802500

回答:


15

その答えをほのめかします。これはありません答えで、単なる紹介図です。@jkの答えは、本物のジッパーを指しています。

不変のツリー構造があると想像してください。子を挿入して1つのノードを変更します。その結果、まったく新しいツリーが作成されます。

しかし、新しいツリーのほとんどは古いツリーとまったく同じです。巧妙な実装では、ほとんどのツリーフラグメントを再利用して、変更されたノードの周囲にポインターをルーティングします。

ウィキペディアから

岡崎の本はこのような例がます。

したがって、ゲームワールドの小さな部分を移動するたびに合理的に変更し(コインを拾う)、実際にワールドデータ構造の小さな部分(コインが拾われたセル)だけを変更できると思います。過去の状態にのみ属するパーツは、時間内にガベージコレクションされます。

これはおそらく、データゲームの世界構造を適切な方法で設計する際に、ある程度の考慮が必要です。残念ながら、私はこれらの問題の専門家ではありません。間違いなく、可変データ構造として使用するNxM行列以外のものでなければなりません。おそらく、ツリーノードのように、互いに指し示すより小さな部分(廊下?個々のセル?)で構成される必要があります。


3
+1:岡崎の本を指すため。まだ読んでいませんが、To Doリストに載っています。あなたが描いたのは正しい解決策だと思います。別の方法として、一意性タイプ(Clean、en.wikipedia.org/wiki/Uniqueness_type)を検討できます。この種のタイプを使用すると、参照の透過性を保持しながら、データオブジェクトを破壊的に更新できます。
ジョルジオ

キーまたはIDを介した間接参照を介して関係を定義する利点はありますか?つまり、ある構造体から別の構造体への実際のタッチが少なくなれば、変化が起こったときに世界構造体の修正が少なくて済むと思っていました。または、この手法はFPで実際に使用されていませんか?
マリオT.ランザ

9

9000の答えは半分です。永続的なデータ構造により、変更されていない部分を再利用できます。

ただし、「ツリーのルートを変更したい場合はどうすればよいか」とすでに考えているかもしれません。現在の例では、すべてのノードを変更することを意味しています。これはジッパーですの助けとなります。O(1)でフォーカスの要素を変更でき、フォーカスは構造内のどこにでも移動できます。

ジッパーのもう1つのポイントは、必要なほとんどすべてのデータ型に対してジッパーが存在することです。


FPを探検しているだけなので、「ジッパー」を掘り下げるには時間がかかると思います。Haskellの経験はありません。
マリオT.ランザ

今日はサンプルを追加しようと思います
jk。

4

関数型プログラムは、並行性を使用するような多くの機会を作成します。コレクションを変換、フィルタリング、または集約し、すべてが純粋または不変である場合は常に、同時実行によって操作が高速化される可能性があります。

たとえば、AIの決定を互いに独立して、特定の順序で実行しないと仮定します。彼らは交代しません、彼らはすべて同時に決定をします、そして、世界は進歩します。コードは次のようになります。

func MakeMonsterDecision curWorldState monster =
    ...
    ...
    return monsterDecision

func NextWorldState curWorldState =
    ...
    let monsterMakeDecisionForCurrentState = MakeMonsterDecision curWorldState
    let monsterDecisions = List.map monsterMakeDecisionForCurrentState activeMonsters
    ...
    return newWorldState

ワールドステートで与えられるモンスターの動作を計算し、次のワールドステートの計算の一部としてすべてのモンスターに適用する機能があります。これは関数型言語で行うのが自然なことであり、コンパイラは「すべてのモンスターに適用する」ステップを並行して自由に実行できます。

命令型言語では、すべてのモンスターを繰り返し処理し、その効果を世界に適用する可能性が高くなります。クローン作成や複雑なエイリアシングに対処したくないので、そのようにするのは簡単です。その場合、コンパイラはモンスターの計算を並行して実行できません。これは、早期のモンスターの決定が後のモンスターの決定に影響するためです。


それはかなり役立ちます。モンスターが次に何をするかを同時に決定することで、ゲームでどのように大きな利益が得られるかがわかります。
マリオT.ランザ14

4

リッチヒッキーのいくつかの講演、特にこの講演を聞いて、混乱を和らげました。ある人は、並行プロセスが最新の状態になっていないかもしれないと言った。私はそれを聞く必要がありました。私が問題を消化できなかったのは、プログラムが実際には新しいものに取って代わられた世界のスナップショットに基づいて決定を行うことで問題ないということでした。私は、コンカレントFPが古い状態に基づいて決定を下す問題をどのように回避したのかと考え続けました。

銀行のアプリケーションでは、新しい状態に取って代わられた状態のスナップショットに基づいて決定を下したくないでしょう(撤回が発生しました)。

FPパラダイムは可変状態を回避するため、その並行性は簡単です。これは、潜在的に古い状態に基づいて決定を下すことの論理的なメリットについては何も言わない技術的な主張であるためです。FPは依然として最終的に状態の変化をモデル化します。これを回避する方法はありません。


0

FP支持者は、パラダイムが可変状態を回避するため、並行性は簡単であると主張しています。わかりません。

私はこの一般的な質問について、機能的な初心者であるが、長年にわたって副作用が眼球にかかっていて、より簡単な(または特に「安全な」、エラーが発生しにくい」同時性。私は機能的な仲間と彼らが何をしているのかを見ると、少なくともこの点で草は少し緑がかったようで、より良い香りがします。

シリアルアルゴリズム

つまり、あなたの特定の例について、あなたの問題が本質的にシリアルであり、Aが完了するまでBを実行できない場合、概念的には、AとBを何としても並行して実行することはできません 古いゲームの状態を使用して並行移動することに基づいて回答のように順序依存性を解消する方法を見つけるか、その一部を独立して変更できるデータ構造を使用して、他の回答で提案されている順序依存性を排除する必要があります、またはこの種のもの。ただし、このような概念設計上の問題は間違いなくあり、不変であるためすべてを簡単にマルチスレッド化できるとは限りません。可能であれば、順序の依存関係を解消するスマートな方法を見つけるまで、本質的にシリアルなものがいくつかあります。

より簡単な同時実行

ただし、スレッドセーフでない可能性があるために、パフォーマンスを大幅に向上させる可能性のある場所で副作用を伴うプログラムを並列化できない場合が多くあります。可変状態(または、より具体的には外部副作用)を排除することは、私が見ているように、「スレッドセーフであるかどうか」「確実にスレッドセーフ」に変えることで大いに役立ちます。

このステートメントをもう少し具体的にするために、コンパレーターを受け入れ、それを使用してエレメントの配列をソートするソート関数をCで実装するタスクを与えると考えてください。これは非常に一般化することを意図していますが、このようなスケール(数百万以上の要素)の入力に対して使用され、常にマルチスレッド実装を使用することは間違いなく有益であるという簡単な仮定を与えます。ソート機能をマルチスレッドできますか?

問題は、ソート関数が呼び出すコンパレーター可能性のあるすべてのケースについて、それらがどのように実装されているか(少なくとも文書化されているか)がわからない限り、副作用を引き起こします。コンパレータは、内部でグローバル変数を非アトミックな方法で変更するようなうんざりすることができます。コンパレータの99.9999%はこれを行えないかもしれませんが、副作用を引き起こすかもしれないケースの0.00001%のために、この汎用関数をマルチスレッド化することはできません。その結果、シングルスレッドとマルチスレッドの両方のソート機能を提供し、それを使用するプログラマーに責任を渡して、スレッドの安全性に基づいてどちらを使用するかを決定する必要があります。また、コンパレータがスレッドセーフかどうかわからない場合があるため、人々はまだシングルスレッドバージョンを使用し、マルチスレッドの機会を逃す可能性があります。

あらゆる場所にロックを投げることなく、物のスレッドの安全性を合理化することに関与することができる脳力がたくさんあります。機能が現在および将来にわたって副作用を引き起こさないという厳しい保証があれば、それは消え去ります。そして、恐怖があります:実際の恐怖。競合状態を何度もデバッグしなければならなかった人は、110%確信できないものをマルチスレッド化することにsuchするでしょう。最も妄想的な(少なくとも私はおそらく境界線である)場合でも、純粋な関数は、安心して並行して呼び出すことができるという安心感と自信を提供します。

そして、そのような関数が純粋な関数型言語で得られるスレッドセーフであることを強く保証できるなら、それはとても有益だと思う主なケースの1つです。もう1つは、関数型言語は最初の段階で副作用のない関数の作成を促進することが多いということです。たとえば、大規模なデータ構造を入力し、元のデータ構造に触れずに元のデータ構造のごく一部を変更しただけの新しいデータ構造を出力することが合理的に非常に効率的な永続的なデータ構造を提供します。そのようなデータ構造なしで作業している人は、それらを直接変更し、途中でいくつかのスレッドの安全性を失いたいかもしれません。

副作用

そうは言っても、私は機能的な友人(私は非常にクールだと思う)に敬意を払って1つの部分に同意しません。

[...]パラダイムが可変状態を回避するため。

同時実行性を私が見るほど実用的にするのは、必ずしも不変性ではありません。副作用の発生を回避する機能です。関数が配列を入力して並べ替え、コピーし、コピーを変更してその内容を並べ替えて出力する場合、同じ入力を渡しても、不変の配列型を操作するものと同じようにスレッドセーフです複数のスレッドからそれに配列します。だから、いわば、非常に並行性にやさしいコードを作成する可変型の場所があると思いますが、不変型には多くの追加の利点があります。副作用のない関数を作成するためにすべてを深くコピーする費用を排除します。

そして、追加のデータをシャッフルしてコピーするという形で、副作用のない関数を作成することでオーバーヘッドが発生することがあります。場合によっては、間接的なレベルを追加し、永続的なデータ構造の一部にGCを追加しますが、 32コアのマシンであり、より多くのことをより自信を持って並行して行うことができれば、交換はおそらく価値があると思います。


1
「可変状態」とは、常にプロシージャレベルではなく、アプリケーションレベルの状態を意味します。ポインターを削除してパラメーターをコピー値として渡すことは、FPに組み込まれた手法の1つです。しかし、有用な関数は何らかのレベルで状態を変化させる必要があります。関数型プログラミングのポイントは、呼び出し元に属する可変状態がプロシージャに入らないようにすることであり、突然変異は戻り値を除いてプロシージャを終了しません!しかし、状態をまったく変更せずに多くの作業を実行できるプログラムはほとんどなく、エラーは常にインターフェイスで再び忍び込みます。
スティーブ

1
正直に言うと、ほとんどの現代言語では機能スタイルのプログラミングを(少し規律をもって)使用できます。もちろん、機能パターン専用の言語もあります。しかし、これは計算効率の低いパターンであり、90年代のオブジェクト指向と同じように、すべての病気の解決策として一般的に誇張されすぎています。ほとんどのプログラムは、並列化の恩恵を受けるCPU集中型の計算に縛られておらず、並列実行に適した方法でプログラムを推論、設計、および実装することが難しいためです。
スティーブ

1
可変状態を扱うほとんどのプログラムは、何らかの理由で必要になるため、そうします。そして、ほとんどのプログラムは、共有状態を使用したり、異常に更新したりするため、正しくありません。通常は、予期しないゴミを入力として受け取る(ゴミ出力を決定する)か、入力に対して誤って動作するためです(目的の意味で間違っています)達成される)。機能パターンはこれにほとんど対処しません。
スティーブ

1
@Steve CやC ++などの言語からよりスレッドセーフな方法で物事を行う方法を模索しているので、少なくとも半分はあなたに同意するかもしれませんが、私たちは完全に行く必要はないと思います-それを行うために純粋な機能を吹き込んだ。しかし、少なくともFPの概念のいくつかは有用だと思います。ここでPDSがどのように役立つかについての答えを書いたところです。PDSについて私が見つけた最大の利点は、実際にはスレッドセーフではなく、インスタンス化、非破壊編集、例外の安全性、簡単な

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.