不可变的结构和深层次的层次结构


9

我正在开发一个GUI应用程序,该应用程序大量处理图形-为了示例起见,您可以将其视为矢量编辑器。使所有数据结构保持不变是非常诱人的-因此我几乎可以不费吹灰之力就可以撤消/重做,复制/粘贴以及许多其他操作。

为了简单起见,我将使用以下示例-应用程序用于编辑多边形,因此我有“多边形”对象,该对象只是不可变点的列表:

Scene -> Polygon -> Point

因此,我的程序中只有一个可变变量-一个保存当前Scene对象的变量。我尝试实现点拖动时就遇到了问题-在可变版本中,我只是抓住一个Point对象并开始修改其坐标。在不可变版本中-我被卡住了。我可以将的索引存储Polygon在current中Scene,将拖动点的索引存储在中Polygon,然后每次替换它。但是这种方法无法扩展-当成分级别达到5时,样板将变得难以忍受。

我确信这个问题可以解决-毕竟,Haskell具有完全不变的结构和IO monad。但是我找不到方法。

你能给我一个提示吗?


@Job-这就是现在的工作方式,这让我很痛苦。因此,我正在寻找替代方法-至少在我们向其添加用户交互之前,不变性对于此应用程序结构来说似乎很完美:)
Rogach 2011年

@Rogach:您能解释一下有关样板代码的更多信息吗?
rwong

Answers:


9

我可以在当前场景中存储多边形的索引,在多边形中存储拖动点的索引,并每次替换它。但是这种方法无法扩展-当成分级别达到5时,样板将变得难以忍受。

完全正确,如果无法绕开样板,这种方法就无法扩展。具体来说,用于创建带有一个小部分的全新场景的样板已更改。但是,许多功能语言提供了一种用于处理这种嵌套结构操纵的构造:镜头。

镜头基本上是不可变数据的获取器和设置器。镜头聚焦在较大结构的一小部分上。给定一个镜头,您可以使用两种方法进行操作-您可以查看较大结构值的一小部分,或者可以较大结构值的一小部分设置为新值。例如,假设您有一个镜头聚焦在列表中的第三项上:

thirdItemLens :: Lens [a] a

该类型表示较大的结构是事物的列表,而较小的子部分是这些事物之一。使用此镜头,您可以查看和设置列表中的第三项:

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

镜头之所以有用,是因为它们是代表getter和setter的,您可以像处理其他值一样对它们进行抽象。您可以创建返回镜片的listItemLens功能,例如,一个带数字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]]

每个镜头都封装了用于遍历数据结构一级的行为。通过将它们组合在一起,可以消除用于遍历复杂结构的多个级别的样板。例如,假设您有一个在场景scenePolygonLens i中查看第ith个多边形,一个在多边形中polygonPointLens n查看nthPoint的对象,则可以使一个镜头构造函数专注于整个场景中您关注的特定点,如下所示:

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

这很好地包含了用于遍历和更新内部场景的所有样板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

13

我已经研究了完全相同的问题(但只有3个合成级别)。基本思想是克隆,然后修改。在不可变的编程风格中,克隆和修改必须同时发生,这成为命令对象

请注意,在可变编程风格中,无论如何都需要克隆:

  • 允许撤消/重做
  • 显示系统可能需要同时显示重叠(作为重影线)的“编辑前”和“编辑中”模型,以便用户可以看到更改。

在可变的编程风格中,

  • 现有结构是深克隆的
  • 所做的更改是在克隆副本中进行的
  • 显示引擎被告知以幽灵线渲染旧结构,并以彩色渲染克隆/修改的结构。

以不变的编程风格,

  • 导致数据修改的每个用户操作都映射到一系列“命令”。
  • 命令对象精确地封装了要应用的修改以及对原始结构的引用。
    • 就我而言,我的命令对象只记住需要更改的点索引和新坐标。(即非常轻巧,因为我并不严格遵循不变的风格。)
  • 执行命令对象时,它将创建该结构的修改后的深层副本,从而使该修改永久存在于新副本中。
  • 当用户进行更多编辑时,将创建更多命令对象。

1
为什么要对不可变数据结构进行深层复制?您只需要将引用的“书脊”从已修改的对象复制到根,并保留对原始结构其余部分的引用。
恢复莫妮卡

3

深度不变的对象的优点是,深度克隆某些对象仅需要复制引用。它们的缺点是,即使对深层嵌套的对象进行很小的更改,都需要为其嵌套的每个对象构造一个新实例。可变对象的优点是更改对象很容易-只需做到即可-但深度克隆对象需要构造一个新对象,该对象包含每个嵌套对象的深克隆。更糟糕的是,如果要克隆对象并进行更改,克隆该对象,进行另一次更改等,那么无论更改大小是多少,都必须为该版本的每个保存版本保留整个层次结构的副本。对象的状态。讨厌。

可能值得考虑的一种方法是定义具有可变且深度不变的派生类型的抽象“ maybeMutable”类型。所有这些类型都将具有AsImmutable方法。在对象的深不可更改的实例上调用该方法将仅返回该实例。在可变实例上调用它会返回一个深度不变的实例,该实例的属性是其原始对象中的深度不变的快照。具有可变等效项的不可变类型将使用一种AsMutable方法,该方法将构造一个可变实例,其属性与原始属性相匹配。

要在一个深度不可变的对象中更改嵌套对象,首先需要用可变的对象替换外部的不可变对象,然后用可变的对象替换包含要更改的对象的属性,依此类推,等等。在尝试调用AsImmutable可变对象之前,总体对象将不需要创建任何其他对象(这将使可变对象可变,但返回保存相同数据的不可变对象)。

作为简单但重要的优化,每个可变对象都可以保留对其关联的不可变类型的对象的缓存引用,并且每个不可变类型都应缓存其GetHashCode值。调用AsImmutable可变对象时,在返回新的不可变对象之前,请检查它是否与缓存的引用匹配。如果是这样,则返回缓存的引用(放弃新的不可变对象)。否则,更新缓存的引用以保存新对象并返回该对象。如果完成,请重复拨打AsImmutable没有任何中间的突变将产生相同的对象引用。即使不节省构建新实例的成本,也可以避免保留新实例的内存成本。此外,如果在大多数情况下要比较的项是参考相等或具有不同的哈希码,则可以大大加快不可变对象之间的相等性比较。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.