如何在Clojure中创建循环(且不可变)数据结构而无需额外的间接寻址?

如何在Clojure中创建循环(且不可变)数据结构而无需额外的间接寻址?,clojure,immutability,directed-graph,Clojure,Immutability,Directed Graph,我需要在Clojure中表示有向图。我想将图中的每个节点表示为一个对象(可能是一条记录),其中包括一个名为:edges的字段,该字段是可从当前节点直接访问的节点集合。希望不用说,但我希望这些图是不变的 我可以用这种方法构造有向无环图,只要我进行拓扑排序并“从叶子上”构建每个图 然而,这种方法不适用于循环图。我能想到的一个解决办法是为整个图形的所有边创建一个单独的集合(可能是一个贴图或向量)。然后,每个节点中的:edges字段将键(或索引)放入图形的边集合中。添加这种额外级别的间接寻址是有效的,因

我需要在Clojure中表示有向图。我想将图中的每个节点表示为一个对象(可能是一条记录),其中包括一个名为
:edges
的字段,该字段是可从当前节点直接访问的节点集合。希望不用说,但我希望这些图是不变的

我可以用这种方法构造有向无环图,只要我进行拓扑排序并“从叶子上”构建每个图

然而,这种方法不适用于循环图。我能想到的一个解决办法是为整个图形的所有边创建一个单独的集合(可能是一个贴图或向量)。然后,每个节点中的
:edges
字段将键(或索引)放入图形的边集合中。添加这种额外级别的间接寻址是有效的,因为我可以在键(或索引)引用的内容存在之前创建键(或索引),但这感觉像是一个难题。我不仅需要在访问相邻节点时执行额外的查找,而且还必须绕过全局边集合,这感觉非常笨拙


我听说一些Lisp有一种不用变异函数就能创建循环列表的方法。有没有一种方法可以在Clojure中创建不可变的循环数据结构?

您可以将每个节点包装在一个ref中,为其指定一个稳定的句柄(并允许您修改可以以nil开头的引用)。这样就有可能构建循环图。这当然有“额外”的间接性


但我认为这不是一个好主意。您的第二个想法是更常见的实现。我们构建了类似这样的东西来保存RDF图,可以不费吹灰之力地从核心数据结构和顶层索引构建RDF图。

我以前遇到过这个挑战,并得出结论,目前在Clojure中使用真正不变的数据结构是不可能的

但是,您可能会发现以下一个或多个选项是可接受的:

  • 使用deftype和“:unsynchronized mutable”在每个节点中创建一个mutable:edges字段,在构建过程中只更改一次。从那时起,您可以将其视为只读,而不需要额外的间接寻址开销。这种方法可能会有最好的性能,但有点像黑客
  • 使用atom实现:边。有一点额外的间接性,但我个人发现读取原子非常有效

    • 这几天我一直在玩这个

      我首先尝试使每个节点都包含一组到边的参照,并且每个边都包含一组到节点的参照。我在
      (dosync…(ref set…)
      操作类型中将它们设置为彼此相等。我不喜欢这样,因为更改一个节点需要大量更新,而打印出图形有点棘手。我必须重写
      print方法
      multi方法,这样repl就不会堆栈溢出。此外,每当我想向现有节点添加一条边时,我必须首先从图中提取实际节点,然后执行各种边更新之类的操作,以确保每个人都保留另一条边的最新版本。而且,因为事物在一个ref中,所以确定某个事物是否与另一个事物相连是一个线性时间操作,这似乎不雅观。我没走多远就断定用这种方法实际执行任何有用的算法都是困难的

      然后我尝试了另一种方法,这是其他地方提到的矩阵的一种变体。该图是一个clojure贴图,其中关键点是节点(不是节点的引用),值是另一个贴图,其中关键点是相邻节点,每个关键点的单个值是该节点的边,表示为指示边的强度的数值或我在别处定义的边结构

      对于
      1->2,1->3,2->5,5->2,它看起来是这样的

      (def graph {node-1 {node-2 edge12, node-3 edge13},
                  node-2 {node-5 edge25},
                  node-3 nil ;;no edge leaves from node 3
                  node-5 {node-2 edge52}) ;; nodes 2 and 5 have an undirected edge
      
      要访问节点1的邻居,可以转到
      (键(图形节点1))
      或调用在别处定义的函数
      (邻居图形节点1)
      ,也可以说
      ((图形节点1)节点2)
      ,从
      1->2
      获取边

      几个优点:

    • 图中节点和相邻节点的常量时间查找,如果不存在,则返回nil
    • 简单灵活的边定义。将邻居添加到映射中的节点条目时,有向边隐式存在,并且显式提供其值(或用于详细信息的结构),或为零
    • 您无需查找现有节点即可对其执行任何操作。它是不可变的,因此您可以在将其添加到图形之前定义它一次,然后在情况发生变化时,您就不必到处寻找最新版本。如果图形中的连接发生更改,则更改的是图形结构,而不是节点/边本身
    • 这结合了矩阵表示法的最佳功能(图形拓扑在图形映射中,未在节点和边中编码、恒定时间查找和非变异节点和边),以及邻接列表(每个节点“有”其相邻节点的列表,因为没有任何“空白”,所以节省了空间)类似于标准稀疏矩阵)
    • 节点之间可以有多条边,如果不小心定义了一条已经完全存在的边,贴图结构会确保不复制该边
    • clojure保留节点和边标识。我不必提出任何索引方案或公共参考点。映射的键和值是它们所表示的内容,而不是其他位置的查找或引用。您的节点结构可以全部为零,只要它是唯一的,就可以在图中表示
    • o