非機能言語での永続データ構造の使用


17

純粋に関数型またはほぼ純粋に関数型の言語は、不変であり、関数型プログラミングのステートレススタイルによく適合するため、永続的なデータ構造の恩恵を受けます。

ただし、Javaのような(状態ベース、OOP)言語の永続データ構造のライブラリが時々見られます。永続的なデータ構造を支持してよく聞かれる主張は、不変であるためスレッドセーフであるということです。

ただし、永続データ構造がスレッドセーフである理由は、1つのスレッドが永続コレクションに要素を「追加」すると、操作は元の要素に要素が追加された新しいコレクションを返すためです。したがって、他のスレッドは元のコレクションを参照します。もちろん、2つのコレクションは多くの内部状態を共有しています。そのため、これらの永続的な構造は効率的です。

しかし、スレッドごとにデータの状態が異なるため、永続データ構造だけで、あるスレッドが他のスレッドに見える変更を行うシナリオを処理するのに十分ではないように思われます。このためには、アトム、リファレンス、ソフトウェアトランザクションメモリ、またはクラシックロックや同期メカニズムなどのデバイスを使用する必要があるようです。

それでは、なぜPDSの不変性が「スレッドセーフ」にとって有益であると宣伝されているのでしょうか。PDSが同期、または並行性の問題の解決に役立つ実際の例はありますか?または、PDSは、関数型プログラミングスタイルをサポートするオブジェクトへのステートレスインターフェイスを提供する単なる方法ですか?


3
あなたは「永続的」と言い続けます。「プログラムの再起動後も生き残ることができる」などの「永続的な」、または「作成後に変更しない」などの「不変」という意味ですか。
キリアンフォス

17
@KilianFoth永続データ構造には、「確立されたデータ構造とは、変更されたときに以前のバージョンを常に保持するデータ構造」という十分に確立された定義があります。したがって、「プログラムの再起動後も存続できる」などの永続性ではなく、それに基づく新しい構造が作成されたときに、以前の構造を再利用することです。
ミチャウKosmulski

3
あなたの質問は、非機能言語での永続的なデータ構造の使用に関するものではなく、パラダイムに関係なく、並行性と並列性のどの部分がそれらによって解決されないのかということです。

私の間違い。「永続的なデータ構造」が単なる永続化とは異なる技術用語であることは知りませんでした。
キリアンフォス

@delnanはい、そうです。
レイトーアル

回答:


15

永続的/不変のデータ構造は、並行性の問題を単独で解決するわけではありませんが、それらを解決するのははるかに簡単です。

セットSを別のスレッドT2に渡すスレッドT1を考えます。Sが可変の場合、T1には問題があります。Sで起こることの制御が失われます。スレッドT2はそれを変更できるため、T1はSのコンテンツにまったく依存できません。逆も同様です-T2はT1 T2が動作している間、Sは変更されません。

1つの解決策は、T1とT2の通信に何らかの種類のコントラクトを追加して、スレッドの1つだけがSを変更できるようにすることです。これはエラーが発生しやすく、設計と実装の両方に負担がかかります。

別の解決策は、T1またはT2がデータ構造(またはそれらが調整されていない場合は両方)を複製することです。ただし、Sが永続的でない場合、これは高価なO(n)操作です。

永続的なデータ構造がある場合、この負担はありません。構造体を別のスレッドに渡すことができ、構造体が何をするかを気にする必要はありません。両方のスレッドは元のバージョンにアクセスでき、それに対して任意の操作を実行できます。他のスレッドの表示には影響しません。

参照:永続データ構造と不変データ構造


2
この文脈でああ、そう「スレッドセーフ」だけで一つのスレッドは、彼らが表示されるデータを破壊し、他のスレッドを心配する必要はありますが、同期して、我々はデータを扱うとは何の関係もありませんしないことを意味したいスレッド間で共有することを。それは私が考えていたものと一致していますが、エレガントに「独自にコンカレンシーの問題を解決しないでください」と述べることで+1されます。
レイトーアル

2
@RayToalはい、このコンテキストでは「スレッドセーフ」とはまさにそれを意味します。スレッド間でデータを共有する方法は別の問題であり、前述のように多くの解決策があります(個人的には、STMの構成可能性が気に入っています)。スレッドセーフにより、共有後のデータで何が起こるかを心配する必要がなくなります。これは実際には大したことです。スレッドは、誰がいつデータ構造を操作するかを同期する必要がないためです。
ペトルプドラク

@RayToalこれにより、アクターなどのエレガントな同時実行モデルが可能になり、開発者が明示的なロックとスレッド管理に対処する必要がなくなり、メッセージの不変性に依存するようになります。転送先の俳優。
ペトルプドラク

Petrに感謝します。俳優にもう一度見てみましょう。私はすべてのClojureメカニズムに精通しており、Rich Hickey は少なくともErlangで例示されているように、アクターモデルを使用しないことを明示的に選択したことに注意しました。それでも、あなたがよりよく知っているほど。
レイトーアル

@RayToal興味深いリンク、ありがとう。私はアクターを例として使用しましたが、それが最良の解決策だと言っているわけではありません。私はClojureを使用したことはありませんが、それがSTMであることが好ましいと思われます。これは俳優よりも間違いなく好むでしょう。STMは永続性/不変性にも依存しています。データ構造を変更できない場合、トランザクションを再開することはできません。
ペトルプドラク

5

それでは、なぜPDSの不変性が「スレッドセーフ」にとって有益であると宣伝されているのでしょうか。PDSが同期、または並行性の問題の解決に役立つ実際の例はありますか?

その場合のPDSの主な利点は、すべてを一意にすることなく(つまり、すべてを深くコピーすることなく)データの一部を変更できることです。これには、副作用のない安価な関数を作成できること以外にも多くの潜在的な利点があります:コピーと貼り付けデータのインスタンス化、簡単な取り消しシステム、ゲームでの簡単なリプレイ機能、簡単な非破壊編集、簡単な例外安全性など。


2

永続的であるが変更可能なデータ構造を想像できます。たとえば、最初のノードへのポインターで表されるリンクリストと、新しいヘッドノードと前のリストで構成される新しいリストを返すprepend-operationを使用できます。前のヘッドへの参照がまだあるため、このリストにアクセスして変更できます。このリストは、新しいリスト内にも埋め込まれています。可能ですが、このようなパラダイムは永続的で不変のデータ構造の利点を提供しません。たとえば、デフォルトではスレッドセーフではありません。ただし、スペース効率などのために、開発者が実行していることを開発者が知っている限り、その用途があります。また、コードの変更を妨げるものは何もないため、構造は言語レベルで変更可能ですが、

つまり、不変性(言語または慣例により強制)がなければ、データ構造の永続性は利点(スレッドセーフ)の一部を失いますが、他のシナリオ(一部のシナリオではスペース効率)は失われません。

非機能言語の例については、JavaでString.substring()私が永続データ構造と呼ぶものを使用しています。文字列は、文字の配列と、実際に使用される配列の範囲の開始オフセットと終了オフセットによって表されます。部分文字列が作成されると、新しいオブジェクトは同じ文字配列を再利用します。ただし、開始オフセットと終了オフセットが変更されます。以来String不変である、それは(に関してあるsubstring()不変永続データ構造動作しないなど)。

データ構造の不変性は、スレッドの安全性に関連する部分です。それらの永続性(新しい構造が作成されたときの既存のチャンクの再利用)は、そのようなコレクションで作業するときの効率に関連しています。それらは不変であるため、アイテムを追加するような操作は既存の構造を変更せず、追加の要素が追加された新しい構造を返します。構造全体がコピーされるたびに、空のコレクションから始めて1000要素を1つずつ追加して1000要素のコレクションになると、0 + 1 + 2 + ... + 999 =の一時オブジェクトが作成されます合計500000の要素があり、これは大きな無駄です。永続的なデータ構造では、1要素のコレクションが2要素のコレクションで再利用され、3要素のコレクションで再利用されるなど、これを回避できます。


状態の1つの側面を除いてすべてが不変である準不変オブジェクトがあると便利な場合があります。状態がほとんど与えられたオブジェクトに似ているオブジェクトを作成する機能です。たとえば、AppendOnlyList<T>2のべき乗の配列に支えられて、各スナップショットのデータをコピーすることなく不変のスナップショットを作成できますが、そのようなスナップショットの内容と新しいアイテムを含むリストを再コピーせずに作成することはできませんすべてを新しい配列に。
supercat 14

0

私は、言語とその性質、私のドメイン、さらには言語の使用方法によっても、C ++でそのような概念を適用するものとして偏見を抱いています。しかし、これらのことを考えると、スレッドセーフティ、システムに関する推論の容易さ、関数の再利用の発見など、関数型プログラミングに関連する利点の大部分を享受することになると、不変のデザインは最も面白くない側面だと思います不快な驚きなしにそれらを任意の順序で結合します)など。

この単純なC ++の例を見てください(明らかに、単純化のために最適化されていないため、画像処理の専門家の前で恥ずかしくなることはありません)。

// Inputs an image and outputs a new one with the specified size.
Image resized_image(const Image& src, int new_w, int new_h)
{
     Image dst(new_w, new_h);
     for (int y=0; y < new_h; ++y)
     {
         for (int x=0; x < new_w; ++x)
              dst[y][x] = src.sample(x / (float)new_w, y / (float)new_h);
     }
     return dst;
}

この関数の実装は、2つのカウンター変数と一時的なローカルイメージの形式でローカル(および一時)状態を変化させて出力しますが、外部からの副作用はありません。画像を入力し、新しい画像を出力します。それを心のコンテンツにマルチスレッド化できます。推論するのは簡単で、徹底的にテストするのは簡単です。何かがスローされた場合、新しいイメージは自動的に破棄され、外部の副作用をロールバックすることを心配する必要がないため、例外に対して安全です(関数のスコープ外で変更される外部イメージはありません)。

Image上記のコンテキストを不変にすることで、上記の関数を実装するのが面倒になり、おそらく少し効率が低下する場合を除いて、上記のコンテキストで不変にすることで、ほとんど得られず、潜在的に多くが失われます。

純度

したがって、純粋な関数(外部副作用がない)は非常に興味深いものであり、C ++でもチームメンバーに頻繁に使用することの重要性を強調しています。しかし、一般的にコンテキストとニュアンスがないだけで適用される不変のデザインは、言語の命令的な性質を考えると、あまり効率的ではないため、ローカルの一時オブジェクトを効率的に変更できることがしばしば便利で実用的です(両方とも開発者およびハードウェア向け)純粋な機能の実装。

多額の構造の安価なコピー

私が見つけた2番目に有用なプロパティは、厳密な入力/出力の性質を考慮して関数を純粋にするためにしばしば発生するように、そのコストが非常に重い場合に、非常に重いデータ構造を安価にコピーする能力です。これらは、スタックに収まる小さな構造ではありません。Sceneビデオゲームの全体のように、大きくて重たい構造になるでしょう。

その場合、レンダラーが同時に描画しようとしているシーンを物理が変化させていると同時に物理を深くしていると、物理と並列をロックおよびボトルネックせずに物理とレンダリングを効率的に並列化するのが難しいため、コピーのオーバーヘッドが効果的な並列処理の機会を妨げる可能性があります物理学を適用した1つのフレームを出力するためだけにゲームシーン全体をコピーすることも、同様に効果がない場合があります。ただし、物理システムが単にシーンを入力し、物理を適用した新しいシーンを出力するという意味で物理システムが「純粋」であり、そのような純度が天文学的なコピーのオーバーヘッドを犠牲にしていなければ、安全に並行して動作できます一方を待たないレンダラー。

したがって、アプリケーションの状態の非常に重いデータを安価にコピーし、処理とメモリ使用に最小限のコストで新しい修正バージョンを出力する機能は、純粋性と効果的な並列性の新しい扉を開くことができます。永続的なデータ構造の実装方法から。しかし、このようなレッスンを使用して作成するものはすべて、完全に永続的である必要はなく、不変のインターフェース(たとえば、コピーオンライト、または「ビルダー/一時」を使用する)を提供する必要はありません。関数/システム/パイプラインの並列性と純度を追求する際に、メモリ使用量とメモリアクセスを2倍にすることなく、コピーの一部だけをコピーして変更します。

不変性

最後に、これら3つの中で最も面白くないと考える不変性がありますが、特定のオブジェクト設計が純粋な機能のローカル一時として使用されることを意図していない場合、鉄の拳で強制することができます。すべてのメソッドで外部副作用を引き起こさない(オブジェクトのメソッドの直接のローカルスコープ外のメンバー変数を変更しない)ため、一種の「オブジェクトレベルの純度」。

そして、C ++のような言語では、これら3つのうちで最も面白くないと思いますが、確かに単純なオブジェクトのテストとスレッドセーフおよび推論を単純化できます。たとえば、オブジェクトにそのコンストラクターの外部で一意の状態の組み合わせを与えることはできず、参照やポインターによってもconstnessやread-元のコンテンツが変更されないことを保証しながら(イテレータとハンドルなどのみ)(少なくとも、言語内でできる限り)。

しかし、私はこれを最も面白くないプロパティだと思います。なぜなら、ほとんどのオブジェクトは一時的に、可変形式で使用されて純粋な機能(またはオブジェクトまたは一連の「純粋なシステム」単に何かを入力し、他の何かに触れることなく新しいものを出力するという究極の効果で機能します)、主に命令型言語での四肢への不変性は、かなり逆効果的な目標だと思います。コードベースの中で最も役立つ部分には、控えめに適用します。

最後に:

[...]永続的なデータ構造だけでは、あるスレッドが他のスレッドに見える変更を加えるシナリオを処理するのに十分ではないように思われます。このためには、アトム、参照、ソフトウェアトランザクションメモリ、またはクラシックロックや同期メカニズムなどのデバイスを使用する必要があるようです。

当然、デザインが(ユーザーエンドデザインの意味で)複数のスレッドが同時に発生するように見えるように変更する必要がある場合、同期または少なくとも描画ボードに戻って、これに対処するための洗練された方法(関数型プログラミングのこの種の問題を扱う専門家が使用する非常に精巧な例をいくつか見ました。

しかし、永続的なデータ構造の例のように、そのようなコピーと、巨大な構造の部分的に変更されたバージョンを安価に出力できるようになると、多くのドアと機会が開かれることがよくあります厳密なI / O並べ替えの並列パイプラインで互いに完全に独立して実行できるコードを並列化することについて、これまで考えたことはありませんでした。アルゴリズムの一部が本質的にシリアルである必要がある場合でも、その処理を単一のスレッドに延期するかもしれませんが、これらの概念に頼ることで簡単に、そして心配なく、多額の作業の90%を並列化することができます

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