【问题标题】:Scala: circular references in immutable data types?Scala:不可变数据类型中的循环引用?
【发布时间】:2011-12-04 07:49:16
【问题描述】:

我一直在考虑如何在 Scala 中仅使用不可变案例类来实现双向链接树或列表。对于大多数“更新”操作,我一直在使用复制和更新方法。例如,在设置父母的孩子时,我说

parent = parent.copy(child=child)

或者在设置孩子的父母时,我说

child = child.copy(parent=parent)

我意识到,如果我将父级设置为包含一个子级,然后创建和更新一个新的子级以包含父级,则父级将包含对旧子级的引用。同样,如果我尝试反过来做,孩子将包含对旧父母的引用。

我希望我的树是双向连接的,这样我就可以双向爬行:从树根向下爬到他的孩子,或者从叶子向上爬到他的父母。是否可以以这种方式“同时”链接父节点和子节点,给我循环引用,然后我可以双向爬行?

我可以使用可变数据轻松做到这一点,但在我的情况下,双向链接树将在创建后存在很长时间,我希望尽可能保持它不可变。

【问题讨论】:

标签: scala


【解决方案1】:

让我们一步一步来尝试。

根据经验,在创建不可变对象时,所有构造函数参数都应该在实例化时就知道,但是让我们作弊并按名称传递构造函数参数,然后使用惰性字段来延迟评估,这样我们就可以创建双向链接元素之间:

// p and n are passed by name 
// and won't be evaluated until prev and next are accessed
// for the first time
class Element [T] (val value: T, p : => Element[T], n : => Element [T]) {
  lazy val prev = p
  lazy val next = n
}

val e1:Element[Int] = new Element [Int] (1,null,e2)
val e2:Element[Int] = new Element [Int] (2,e1,e3)
val e3:Element[Int] = new Element [Int] (3,e2,e4)
val e4:Element[Int] = new Element [Int] (4,e3,null)

一旦我们运行代码,我们将收到一个不可变的双向链表:

null ← e1(1) ↔ e2(2) ↔ e3(3) ↔ e4(4) → null

并且将能够来回遍历它:

println(e1.next.next.next.value)
println(e4.prev.prev.prev.value)

4
1

现在,假设我们要在列表末尾添加第五个元素,使其看起来像这样:

null ← e1(1) ↔ e2(2) ↔ e3(3) ↔ e4(4) ↔ e5(5) → null

val e5:Element[Int] = new Element [Int] (5,e4,null)

此时我们得到:

null ← e1(1) ↔ e2(2) ↔ e3(3) ↔ e4(4) → null 
                                     ↖  ↑
                                       e5(5)

等一下,看起来不太对劲! e4 应该指向 e5 而不是指向 null,但是 e4 是不可变的,我们无法更改元素本身,所以看起来唯一的选择是制作一个副本并将其指向 e3e5。让我们尝试将这种新方法应用于初始列表:

null ← e1(1) ↔ e2(2) ↔ e3(3) ↔ e4(4) → null

val e4_b: Element[Int] = new Element [Int] (e4.value, // keeping original value 
                                            e3,e5)

val e5  : Element[Int] = new Element [Int] (5,e4_b,null)

这样更好,e4_b 通向 e5 又通向 e4_b:

null ← e1(1) ↔ e2(2) ↔ e3(3) ↔ e4(4) → null 
                           ↖           ↑
                             e4_b(4) ↔ e5(5)

但是现在我们有同样的原始问题,只是 e3 仍然指向 e4。你能看到正在出现的趋势吗?如果我们继续复制元素以很快解决问题,我们最终会得到:

null ← e1(1) ↔ e2(2) ↔ e3(3) ↔ e4(4) → null 
  ↑                                      ↑
e1_b(1) ↔ e2_b(2) ↔ e3_b(3) ↔ e4_b(4) ↔ e5(5)

原来的列表没有任何改变(事实证明我们并没有无缘无故地称其为“不可变”),相反,我们最终得到了一个全新的列表,尽管它具有相同的值。因此,每当我们尝试对不可变的双向链接数据结构进行更改时,我们都需要从头开始重建整个事物,并保留值

让我们来看看 Scala 标准的单链不可变列表:

e1(1) → e2(2) → e3(3) → e4(4) → 无

我们会注意到,我们能够更轻松地派生新列表,而无需从头开始重建整个数据结构,例如要删除第二个元素,我们只需复制第一个元素并指向它到第三个:

e1(1) → e2(2) → e3(3) → e4(4) → Nil
                 ↗
         e1_b(1) 

当然,因为原始列表是不可变的,所以它并没有真正改变。

【讨论】:

  • 关于你的最后一个例子:删除 last 元素时会发生什么?这需要一个新的e3 指向Nil。但是由于我们不能修改e2 以指向新的e3,我们还需要一个新的e2,因此也需要e1。所以我们必须完全重建列表?
  • e1, e2, e3, e4 都应该是惰性 val 否则 scala 编译器会引发“错误的前向引用”
【解决方案2】:

你可以用懒惰来做,例如:

trait Link[A] {
  def value: A
  def get: Link[A]
}

class Circular[A](val value: A, getter: => Link[A]) extends Link[A] {
  lazy val get = getter
}

object circles {
  def create[A](as: (A, A)): Link[A] = {
    lazy val b: Link[A] = new Circular(as._1, new Circular(as._2, b))
    b
  }
}

话虽如此,您可能想长时间地问自己为什么想要这样的东西。

【讨论】:

    【解决方案3】:

    我创建了一篇博文,描述了您的问题的一种可能解决方案。 http://akikhtenko.github.io/blog/2013/12/15/immutable-double-linked-tree-construction-in-scala/ 它以树为例,但将其应用于其他数据类型应该不成问题。

    【讨论】:

      【解决方案4】:

      scala 中的不变性意味着在我们构建完一个对象之后,它不应该改变。在对象构造期间,它实际上是可变的。解决方法是将一段代码传递给 在字段变为不可变之前计算所需值的对象。

      {
        // Create a with the creation of b as a parameter.
        val a=new A( (uncomplete:A)=>new B(uncomplete) )
      }
      
      class A( bFactory:A=>B){
        //Call the bFactory then assign the result to b.
        val b=bFactory(this);
      }
      
      class B(val a:A){
      }
      

      由于问题涉及树,我还将包括使用相同技术生成基本树。

       class MakeTree {
        val tree = new Node(None, createSubTree _, createSubTree _);
      
        def createSubTree(parent: Node): Option[Node] = {
          if (parent.depth < 3)
            Some(new Node(None, createSubNode _, createSubNode _))
          else
            None
        }
      }
      
      class Node(val parent: Option[Node], leftFactory: (Node) => Option[Node], rightFactory: (Node) => Option[Node]) {
        val left = leftFactory(this);
        val right = rightFactory(this);
      
        def depth(): Int = parent.map(_.depth + 1).getOrElse(0);
      }
      

      像这样进行构造会留下一个纯粹的不可变结构,而不会增加访问惰性值的开销。

      【讨论】:

        【解决方案5】:
        class A(val b: B)
        abstract class B {
          val a: A
        }
        new B {
          val a = new A(this)
        }
        

        【讨论】:

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