【问题标题】:How to initialize and "modify" a cyclic persistent data structure in Scala?如何在 Scala 中初始化和“修改”循环持久数据结构?
【发布时间】:2011-04-15 20:55:35
【问题描述】:

我已经搜索并找到了有关此主题的一些信息,但答案要么令人困惑要么不适用。

我有这样的事情:

class Thing (val name:String, val refs:IndexedSeq[Ref])
class Ref (val name:String, val thing:Thing)

现在,我想说的是,加载一个文件,解析它并从中填充这个数据结构。它是不可变的和循环的,如何做到这一点?

另外,假设我确实填充了这个数据结构,现在我想修改它,比如更改 rootThing.refs(3).name,如何做到这一点?


感谢您在此处发布的想法。在这一点上,我在想,如果一个人真的想要这样的持久数据结构,那么跳出框框思考并考虑客户端代码需要提出哪些问题。因此,与其考虑对象和字段,不如考虑查询、索引等。首先,我在考虑: Is there a bidirectional multimap persistent data structure?

【问题讨论】:

  • 文件长什么样?
  • 文件无关。我只列出了表明我需要基于运行时数据而不是基于编译时的数据来初始化它。
  • 由于您不知道解决方案,因此您无法评估什么是相关的或不相关的。但如果你必须,我会改变这个问题。数据结构是什么样的?任意图,还是具有双向链接的树?它是如何初始化的?从根到底部,还是从任意点?
  • 它可能不是一个文件——它可能是内存中的东西,或者它可能来自一些用户交互,结构可能来自任何地方。整个数据结构是上面代码中给出的事物列表。把它想象成一组类型。每种类型(给定代码中的事物)都有一个属性列表(IndexedSeq[Ref])。每个属性都是一个类型,所以 Ref 有一个对它的引用。

标签: scala immutability cyclic-reference cyclic-graph


【解决方案1】:

如果你准备修改它以引入一定程度的惰性,你可以初始化这种形式的循环数据结构,

scala> class Thing (val name:String, refs0: => IndexedSeq[Ref]) { lazy val refs = refs0 } ; class Ref (val name:String, thing0: => Thing) { lazy val thing = thing0 }
defined class Thing
defined class Ref

scala> val names = Vector("foo", "bar", "baz")                                                                                                                       
names: scala.collection.immutable.Vector[java.lang.String] = Vector(foo, bar, baz)

scala> val rootThing : Thing = new Thing("root", names.map { new Ref(_, rootThing) })
rootThing: Thing = Thing@1f7dab1

scala> rootThing.refs(1).name
res0: String = bar

但是,您不能使其持久化:由于是循环的,任何更改都可以通过结构的每个元素看到,因此没有机会在版本之间共享。

【讨论】:

  • 非常好,我的印象是不可能创建一个自引用的不可变结构!谢谢迈尔斯!
  • Grr...您找到了一个关键点。无论是否可以初始化它,它基本上都不能使用典型的持久数据结构优化来通过仅替换最少数量的节点来从中创建派生值。
【解决方案2】:

对于单个循环引用,可以使用惰性:

lazy val t: Thing = new Thing("thing", Vector(new Ref("ref", t)))

但显然这会因多对多连接而变得复杂。

我不知道是否存在通用的纯函数循环图数据结构。对于无环图,这很容易,因为您可以对其进行拓扑排序,然后逐步对其进行初始化。

也许您可以选择使用间接方式,例如通过标识符而不是实际的 scala 对象引用来引用对象?

case class ThingByID(id: Int, name: String, refs: IndexedSeq[RefByID])
case class RefByID(name: String, thingID: Int)

然后,您可以在加载文件后通过 ID 将事物收集到不可变映射中(例如 collection.immutable.IntMap),并在来自 ref 时查找它们。

编辑

Miles 关于lazy val t 的第一个案例是正确的。确实,您需要他的回答中的名称参数。

class Thing(val name: String, val refs: IndexedSeq[Ref])
class Ref(val name: String, _thing: => Thing) { def thing = _thing }

val t: Thing = new Thing("thing", Vector(new Ref("ref", t)))

【讨论】:

  • 恐怕你对 t 的定义不够懒惰。任何尝试使用它都会导致 StackOverflowError。
  • 你提到的ID这件事也是我问这个后想出来的。这似乎意味着对性能的重大影响。但它也使它成为一个持久的数据结构,尽管人们必须有一种方法来找到所有指向某个 ID 的 Ref,这会使修改更加昂贵。这引出了一个问题:考虑到所涉及的所有工作,这样做是否值得与仅使用可变数据结构相比?有什么优势?
  • 啊,但是知道从 Ref 到 Thing 的所有链接无论如何都是有用的——而且是我们需要的东西,所以这让我想到了发生在我身上的另一件事......链接的许多数据结构。所以......也许考虑将数据结构转换为可以持久的东西而不是试图找出一些使循环工作的复杂方法是​​有利可图的。
【解决方案3】:

还有一种替代方法需要重新考虑对象关联的表示方式:而不是将对象之间的关联存储在对象本身内部(通常在 OO 代码中完成),然后将它们作为 Maps 添加到单独的层中。

由于对象图中的所有节点在创建关联时都已存在,因此可以使用 Maps 轻松创建不可变的双向关联。

scala> class Thing (val name:String)
defined class Thing

scala> class Ref (val name:String)
defined class Ref

scala> new Thing("Thing1")
res0: Thing = Thing@5c2bae98

scala> new Ref("Ref1")
res1: Ref = Ref@7656acfa       

scala> val thing2Ref = Map(res0 -> res1)
thing2Ref: scala.collection.immutable.Map[Thing,Ref] = Map(Thing@5c2bae98 -> Ref
@7656acfa)

scala> val ref2Thing = Map(res1 -> res0)
ref2Thing: scala.collection.immutable.Map[Ref,Thing] = Map(Ref@7656acfa -> Thing
@5c2bae98)

如果您考虑一下,这种方法类似于关系数据库的工作方式。表之间的多值关联不存储在行本身中,而是存储在单独的索引中。即使不存在关联索引并且使用表扫描解决查询,它也使用主键索引来定位结果的所有候选行。

这种风格的一个优点是可以添加或更改关联而不影响对象本身,并且可以为不同的受众/用例采用不同的关联。一个缺点是在关联开始的实例上不能直接访问关联映射。它必须单独传递和提供。

【讨论】:

    【解决方案4】:

    不可变数据结构可以完全由其构造函数初始化,或者您可以接受在更改其属性时继续复制该结构的需要。因此,要回答问题的第一部分,您可以通过定义一个接受数据中所有信息的构造函数将数据加载到不可变数据结构中,或者确保您了解循环子图:

    我认为,循环数据结构不一定是完全循环的。如果您想象单个实例/状态所拥有的指针网络,您可以有一个子图,其中包含相互指向的父子图,但没有其他循环结构。在这种情况下,复制实例 1 以延迟创建具有不同父节点的实例 2(例如)将需要复制父节点和子节点,因为它们形成循环子图。但是除了父级之外的子级中的引用可以继续是对与第一个实例相同的不可变结构的引用。

    例如,我的类 House 引用了 Door、Window 和 Roof。 Door 有颜色,toHouse 引用了 House,Window 有 size,Roof 有 pitch。所以我创建了带有绿色门、大窗户和平屋顶的 bobsHouse。事实上,由于所有这些都是不可变的,理论上只有一个大窗户——所有有大窗户的房子都有相同的窗户。第二个实例 janesHouse 与 bobsHouse 类似,但带有山墙屋顶。因此,如果我说 janesHouse = bobsHouse.withRoof(gabled),那么我应该得到一个新的 House 实例,带有一个新的(也是绿色的)门和一个新的(山墙的)屋顶,但具有相同的窗口。

    因此,如果 janesHouse 被延迟评估,则只需在引用 Door 或 Roof 时创建一个新 House。如果 janesHouse.Window 被请求,它根本不需要创建一个新的房子——只需要 bobsHouse。

    tl;dr:您可以拥有持久(惰性)循环数据结构,但前提是您可以在其中找到非循环子图,即它不是链。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-03-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多