web-dev-qa-db-ja.com

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

FPの支持者は、パラダイムが変更可能な状態を回避するため、同時実行性は容易であると主張しています。わかりません。

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

世界(現在の現実)にはこの状態の概念があり、FP=がこれを克服する方法がありません。ヒーローが行動を起こすと、関数が世界の状態を修正します。すべての決定(AIまたは人間)は、現在の世界の状態に基づいている必要があります。同時実行を許可する場所はどこですか?1つのプロセスベースに基づいて、複数のプロセスで同時に世界の状態を修正することはできません。現在の世界のオブジェクトグラフで表される現在の状態を常に処理するために、すべての制御が単一の制御ループ内で発生する必要があるように感じます。

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

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

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

21
Mario T. Lanza

いくつかのリッチヒッキーの講演を聴くと-- これ 特に-混乱が緩和されました。ある彼は、並行プロセスが最新の状態でなくても大丈夫だと述べた。私はそれを聞く必要がありました。私がダイジェストで問題を抱えていたのは、プログラムが実際には世界のスナップショットに基づいて決定を下すことで大丈夫であり、それ以降は新しいスナップショットに取って代わられているということでした。 FPが古い状態に基づいた決定の問題をどのように回避したかについて、私は不思議に思いました。

銀行のアプリケーションでは、それ以降、新しいスナップショットに取って代わられた状態のスナップショット(引き出しが発生した)に基づいて決定を下したくありません。

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

4
Mario T. Lanza

答えをヒントにしようと思います。これは答えではありません、導入図のみです。 @jkの回答は、本物のジッパーを指します。

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

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

From Wikipedia

岡崎の本 はこのような例でいっぱいです。

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

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

15
9000

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

しかし、「木の根を変更したい場合はどうすればよいのか」と既に考えているかもしれません。与えられた例をそのまま使用すると、すべてのノードを変更することになります。ここで Zippers が役に立ちます。フォーカスの要素をO(1)で変更でき、構造内の任意の場所にフォーカスを移動できます。

ジッパーのもう1つのポイントは、 Zipperがほとんどすべてのデータ型に対応していることです

9
jk.

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

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

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

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

あなたは、世界の状態が与えられたときにモンスターが何をするかを計算し、次の世界の状態を計算する一部としてすべてのモンスターに適用する機能を持っています。これは関数型言語で行うのが当たり前のことであり、コンパイラは「すべてのモンスターに適用する」ステップを並行して自由に実行できます。

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

4
Craig Gidney

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

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

シリアルアルゴリズム

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

簡単な同時実行

とはいえ、単に副作用が可能性があるためにパフォーマンスが大幅に向上する可能性がある場所で副作用が発生するプログラムを並列化できない場合が多くあります(== --- ==)可能性がありますスレッドセーフではありません。変更可能な状態(より具体的には、外部の副作用)を排除することは、私が見るようにそれが大きく役立つ「スレッドセーフかもしれないし、そうでないかもしれない」 "definitely thread-safe"に変換します。

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

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

関数が今も将来も副作用を引き起こさないという確固たる保証がある場合、どこにでもロックを投げずにスレッドの安全性を合理化することに関わることができる頭脳はたくさんあります。恐怖があります。実際の恐怖です。競合状態を何度かデバッグしなければならなかった人は、110%確実にできないものをマルチスレッド化することをためらうため、スレッドセーフであり、スレッドセーフであり続けるでしょう。最もパラノイアでも(おそらく私は少なくとも境界線です)、純粋な関数は、安心して並行して呼び出すことができるという安心感と自信を提供します。

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

副作用

とはいえ、私は機能的な友達(私は超クールだと思う)に敬意を払って、ある部分には同意しません。

[...]彼らのパラダイムは変更可能な状態を回避するためです。

私が見る限り、同時実行性がそれほど実用的であるのは、必ずしも不変性ではありません。副作用の発生を防ぐ機能です。関数が配列を入力してそれをコピーし、次にそのコピーを変更してその内容をソートして出力を出力する場合、同じ入力を渡していても、不変の配列型を操作するのと同じくらいスレッドセーフです。複数のスレッドからそれに配列します。つまり、非常に同時実行性に優れたコードを作成するには、可変型の場所がまだあると思います。ただし、不変型には多くの利点があります。たとえば、不変プロパティにはあまり使用しない永続データ構造があります。副作用のない関数を作成するためにすべてをディープコピーする必要がある費用を排除します。

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

0
user204677