不変の構造と深い構成階層


9

私はGUIアプリケーションを開発しています。グラフィックを多用しています。例として、ベクターエディターと考えることができます。すべてのデータ構造を不変にするのは非常に魅力的です。そのため、ほとんど労力をかけずに、元に戻す/やり直し、コピー/貼り付け、その他多くのことを行うことができました。

簡単にするために、次の例を使用します。アプリケーションを使用して多角形を編集するため、「不変点」の単なるリストである「Polygon」オブジェクトがあります。

Scene -> Polygon -> Point

そして、私はプログラムに1つだけの可変変数があります-現在のシーンオブジェクトを保持する変数です。私が抱えている問題は、ポイントドラッグを実装しようとしたときに発生します。可変バージョンでは、Pointオブジェクトを取得してその座標の変更を開始するだけです。不変バージョン-行き詰まっています。PolygoncurrentのSceneインデックス、にドラッグされたポイントのインデックスを格納しPolygon、毎回それを置き換えることができます。しかし、このアプローチはスケーリングしません。構成レベルが5に達すると、ボイラープレートは耐えられなくなります。

この問題は解決できると確信しています-結局のところ、完全に不変の構造とIOモナドを持つHas​​kellがあります。しかし、私はその方法を見つけることができません。

ヒントをください。


@ジョブ-それは今それがどのように機能するかであり、それは私に多くの苦痛を与えます。だから私は代替のアプローチを探しています-そして、少なくともユーザーインタラクションをそれに追加する前に、不変性はこのアプリケーション構造にとって完璧に思えます:)
Rogach

@Rogach:ボイラープレートコードについて詳しく説明できますか?
rwong

回答:


9

ポリゴンのインデックスを現在のシーンに保存し、ドラッグしたポイントのインデックスをポリゴンに保存して、毎回置き換えることができます。しかし、このアプローチはスケーリングしません。構成レベルが5に達すると、ボイラープレートは耐えられなくなります。

あなたが絶対に正しい、あなたがボイラープレートを回避できない場合、このアプローチはスケーリングしません。具体的には、小さなサブパートでまったく新しいシーンを作成するためのボイラープレートが変更されました。ただし、多くの関数型言語は、この種の入れ子構造操作を処理するための構成体、つまりレンズを提供しています。

レンズは基本的に不変データのゲッターおよびセッターです。レンズは、大きな構造の一部に焦点を当てています。レンズが与えられた場合、それを使ってできることは2つあります。大きな構造の値の小さな部分を表示するか、大きな構造の値の小さな部分を新しい値に設定できます。たとえば、リストの3番目のアイテムに焦点を合わせるレンズがあるとします。

thirdItemLens :: Lens [a] a

そのタイプは、より大きな構造が物事のリストであり、小さなサブパートがそれらの物事の1つであることを意味します。このレンズを指定すると、リストの3番目の項目を表示および設定できます。

> view thirdItemLens [1, 2, 3, 4, 5]
3
> set thirdItemLens 100 [1, 2, 3, 4, 5]
[1, 2, 100, 4, 5]

レンズが役立つ理由は、レンズがゲッターとセッターを表すであり、他の値と同じようにそれらを抽象化できるからです。レンズを返す関数を作成できます。たとえばlistItemLens、数値を受け取り、リストのth番目のアイテムをn表示するレンズを返す関数などnです。さらに、レンズを構成することができます:

> firstLens = listItemLens 0
> thirdLens = listItemLens 2
> firstOfThirdLens = lensCompose firstLens thirdLens
> view firstOfThirdLens [[1, 2], [3, 4], [5, 6], [7, 8]]
5
> set firstOfThirdLens 100 [[1, 2], [3, 4], [5, 6], [7, 8]]
[[1, 2], [3, 4], [100, 6], [7, 8]]

各レンズは、データ構造の1つのレベルをトラバースするための動作をカプセル化します。それらを組み合わせることにより、複雑な構造の複数のレベルを横断するためのボイラープレートを排除できます。たとえば、あなたが持っていると仮定scenePolygonLens i見ているiシーンで目のポリゴンを、とpolygonPointLens n見てそのnthポリゴン内のポイントを、あなたはそうのようなシーン全体に気にだけ、特定のポイントに焦点を合わせるためのレンズのコンストラクタを行うことができます。

scenePointLens i n = lensCompose (polygonPointLens n) (scenePolygonLens i)

ここで、ユーザーがポリゴン14のポイント3をクリックして、10ピクセル右に移動したとします。次のようにシーンを更新できます。

lens = scenePointLens 14 3
point = view lens currentScene
newPoint = movePoint 10 0 point
newScene = set lens newPoint currentScene

これには、Sceneをトラバースおよび更新するためのすべてのボイラープレートが含まれてlensいます。気をつけなければならないのは、ポイントを変更したいものだけです。することができますさらに抽象化してこのlensTransformレンズを受け入れる機能、ターゲット、および機能レンズを通して対象のビューを更新します:

lensTransform lens transformFunc target =
  current = view lens target
  new = transformFunc current
  set lens new target

これは関数を受け取り、それを複雑なデータ構造の「アップデーター」に変え、関数をビューにのみ適用し、それを使用して新しいビューを構築します。したがって、14番目のポリゴンの3番目のポイントを右の10ピクセルに移動するシナリオに戻ると、次のように表すことができますlensTransform

lens = scenePointLens 14 3
moveRightTen point = movePoint 10 0 point
newScene = lensTransform lens moveRightTen currentScene

シーン全体を更新するために必要なのはこれだけです。これは非常に強力なアイデアであり、関心のあるデータの一部を表示するレンズを構築するためのいくつかの優れた機能がある場合に非常にうまく機能します。

ただし、これは現在、関数型プログラミングコミュニティにおいても、かなりかなり存在しています。レンズを操作するための適切なライブラリサポートを見つけることは困難であり、レンズがどのように機能するか、同僚にどのような利点があるかを説明することはさらに困難です。塩の粒でこのアプローチを取ります。


素晴らしい説明!今、私はレンズが何であるかを手に入れました!
Vincent Lecrubier 2016年

13

私はまったく同じ問題に取り組みました(ただし、3つの構成レベルのみ)。基本的な考え方は、クローン作成してから変更することです。不変のプログラミングスタイルでは、複製と変更を同時に行う必要があり、コマンドオブジェクトになります

可変プログラミングスタイルでは、とにかくクローン作成が必要になることに注意してください。

  • 元に戻す/やり直しを許可するには
  • 表示システムでは、「編集前」モデルと「編集中」モデルを(ゴーストラインとして)重ねて同時に表示して、ユーザーが変更を確認できるようにする必要があります。

可変プログラミングスタイルでは、

  • 既存の構造はディープクローンです
  • クローンされたコピーに変更が加えられます
  • ディスプレイエンジンは、古い構造をゴーストラインで、クローン/変更された構造をカラーでレンダリングするように指示されます。

不変のプログラミングスタイルでは、

  • データを変更する各ユーザーアクションは、一連の「コマンド」にマッピングされます。
  • コマンドオブジェクトは、適用される変更と、元の構造への参照を正確にカプセル化します。
    • 私の場合、コマンドオブジェクトは、変更が必要なポイントインデックスと新しい座標のみを記憶しています。(つまり、不変のスタイルに厳密に従っていないため、非常に軽量です。)
  • コマンドオブジェクトが実行されると、構造の変更されたディープコピーが作成され、変更は新しいコピーで永続的になります。
  • ユーザーがさらに編集すると、より多くのコマンドオブジェクトが作成されます。

1
なぜ不変のデータ構造の深いコピーを作成するのですか?変更されたオブジェクトからルートに参照の「スパイン」をコピーし、元の構造の残りの部分への参照を保持するだけです。
モニカを

3

不変オブジェクトには、参照をコピーするだけで何かをディープクローンできるという利点があります。深くネストされたオブジェクトに小さな変更を加えるだけでも、ネストされているすべてのオブジェクトの新しいインスタンスを作成する必要があるという欠点があります。可変オブジェクトには、オブジェクトを変更するのが簡単であるという利点があります-それを行うだけです-オブジェクトをディープクローンするには、ネストされたすべてのオブジェクトのディープクローンを含む新しいオブジェクトを構築する必要があります。さらに悪いことに、オブジェクトのクローンを作成して変更を加えたい場合、そのオブジェクトのクローンを作成したり、別の変更を加えたりする場合など、変更がどれほど大きくても小さくても、保存されているすべてのバージョンの階層全体のコピーを保持する必要があります。オブジェクトの状態。不快な。

検討する価値のあるアプローチは、可変で深く不変の派生型を持つ抽象「maybeMutable」型を定義することです。このようなタイプはすべてAsImmutableメソッドを備えています。オブジェクトの深く不変のインスタンスでそのメソッドを呼び出すと、単にそのインスタンスが返されます。可変インスタンスでそれを呼び出すと、プロパティが元の同等のものの非常に不変のスナップショットである、非常に不変のインスタンスが返されます。同等の変更可能な不変型AsMutableは、元のプロパティと一致するプロパティを持つ変更可能なインスタンスを構築するメソッドを備えています。

不変オブジェクトの入れ子になったオブジェクトを変更するには、最初に外側の不変オブジェクトを変更可能なオブジェクトで置き換え、次に変更するものを含むプロパティを変更可能なオブジェクトで置き換えるなど、ただし同じことを同じ要素に繰り返し変更する必要があります。オブジェクト全体AsImmutableは、変更可能なオブジェクトを呼び出そうとするまで、追加のオブジェクトを作成する必要はありません(変更可能なオブジェクトは変更可能のままですが、同じデータを保持する不変のオブジェクトを返します)。

単純ですが重要な最適化として、各可変オブジェクトは、関連する不変タイプのオブジェクトへのキャッシュされた参照を保持でき、各不変タイプはそのGetHashCode値をキャッシュする必要があります。AsImmutable可変オブジェクトを呼び出すときは、新しい不変オブジェクトを返す前に、キャッシュされた参照と一致することを確認してください。もしそうなら、キャッシュされた参照を返します(新しい不変オブジェクトを破棄します)。それ以外の場合は、キャッシュされた参照を更新して新しいオブジェクトを保持し、それを返します。これが行われた場合、繰り返し呼び出しAsImmutable間に変異がないと、同じオブジェクト参照が生成されます。新しいインスタンスを構築するコストを節約しなくても、それらを維持するためのメモリコストを回避できます。さらに、ほとんどの場合、比較される項目が参照等価であるか、または異なるハッシュコードを持っている場合、不変オブジェクト間の等価比較は大幅に促進されます。

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