Scala 可以写一个不可变的双链表吗?

Scala 可以写一个不可变的双链表吗?,scala,functional-programming,immutability,immutable-collections,Scala,Functional Programming,Immutability,Immutable Collections,问这个问题我觉得有点傻,但我目前正在学习函数式编程,并完成了一个关于创建单链表的练习,这让我想到,甚至有可能创建一个不可变的双链表吗 假设列表A::B,在构建时,A需要了解B,但B也需要了解A。我在Scala中做过这项工作,所以我不确定它是否特定于Scala,但我无法想象这将如何工作 我不是在寻找替代品,因为我什么都不需要,我只是好奇。是的,这是可能的。但通常不会这样做,因为与单链表不同,双链表没有任何子结构,例如,当删除一个元素时,这些子结构可以重用。此外,这样的列表似乎没有做任何不可变的向量

问这个问题我觉得有点傻,但我目前正在学习函数式编程,并完成了一个关于创建单链表的练习,这让我想到,甚至有可能创建一个不可变的双链表吗

假设列表A::B,在构建时,A需要了解B,但B也需要了解A。我在Scala中做过这项工作,所以我不确定它是否特定于Scala,但我无法想象这将如何工作


我不是在寻找替代品,因为我什么都不需要,我只是好奇。

是的,这是可能的。但通常不会这样做,因为与单链表不同,双链表没有任何子结构,例如,当删除一个元素时,这些子结构可以重用。此外,这样的列表似乎没有做任何不可变的
向量所不能做的事情

然而,让我们把它写下来,因为它很有趣

简化问题:循环两元素“列表”

作为一个热身,请看一看简化的问题:一个循环的两元素“列表”,其中两个节点相互引用:

case class HalfRing(val value: Int)(otherHalf: => HalfRing) {
  def next = otherHalf
}

object HalfRing {
  def fullRing(a: Int, b: Int): HalfRing = {
    lazy val ha: HalfRing = HalfRing(a){hb}
    lazy val hb: HalfRing = HalfRing(b){ha}
    ha
  }
}
这确实有效,我们可以构建这个小的两节点数据结构,并在其上循环运行数百万次迭代:

var r = HalfRing.fullRing(42, 58)
for (i <- 0 until 1000000) {
  r = r.next
  if (i % 100001 == 0) println(r.value)
}
循环演示的是:这是一个实际的数据结构,而不是一些奇怪的嵌套函数族,这些函数在访问元素几次后就会破坏堆栈


不可变的双链接列表

我决定用双链接连接的节点和两端的两个显式
Nil
-元素来表示列表:

sealed trait DLL[+A] extends (Int => A)
case class LeftNil[+A]()(n: => DLL[A]) extends DLL[A] {
  def next = n
  def apply(i: Int) = next(i)
}
case class RightNil[+A]()(p: => DLL[A]) extends DLL[A] {
  def prev = p
  def apply(i: Int) = 
    throw new IndexOutOfBoundsException("DLL accessed at " + i)
}
case class Cons[+A](value: A)(p: => DLL[A], n: => DLL[A]) extends DLL[A] {
  def next = n
  def prev = p
  def apply(i: Int) = if (i == 0) value else next(i - 1)
}
apply
-部分基本上是无关的,我添加它只是为了以后可以检查和打印内容。有趣的问题是:我们如何实际实例化这样一个列表?以下是将单链表转换为双链表的方法:

object DLL {
  def apply[A](sll: List[A]): DLL[A] = {
    def build(rest: List[A]): (=> DLL[A]) => DLL[A] = rest match {
      case Nil => RightNil[A]() _
      case h :: t => {
        l => {
          lazy val r: DLL[A] = build(t){c}
          lazy val c: DLL[A] = Cons(h)(l, r)
          c
        }
      }
    }
    lazy val r: DLL[A] = build(sll){l}
    lazy val l: DLL[A] = LeftNil(){r}
    l
  }
}
这里发生的事情本质上与上面的双元素环相同,但重复了多次。我们只是以与连接两个半环相同的方式连接各个部分,除了这里我们首先将小的
Cons
-元素连接到列表的长尾,最后将
LeftNil
与第一个
Cons
连接

同样,一个小演示,一个“迭代器”,它在列表上来回运行数百万次迭代,偶尔打印当前元素:

val dll = DLL((42 to 100).toList)

println((1 to 20).map(dll))

@annotation.tailrec 
def bounceBackAndForth(
  dll: DLL[Int], 
  maxIters: Int, 
  direction: Int = +1
): Unit = {
  if (maxIters <= 0) println("done")
  else dll match {
    case ln: LeftNil[Int] => bounceBackAndForth(ln.next, maxIters - 1, +1)
    case rn: RightNil[Int] => bounceBackAndForth(rn.prev, maxIters - 1, -1)
    case c: Cons[Int] => {
      if (maxIters % 100003 == 0) println(c.value)
      if (direction < 0) {
        bounceBackAndForth(c.prev, maxIters - 1, -1)
      } else {
        bounceBackAndForth(c.next, maxIters - 1, +1)
      }
    }
  }
}

bounceBackAndForth(dll, 1000000)

// cs_XIIIp4
val dll=dll((42到100).toList)
println((1到20).map(dll))
@注释.tailrec
def来回反弹(
dll:dll[Int],
马克西特斯:Int,
方向:Int=+1
):单位={
if(最大值反弹前后(ln.next,最大值-1,+1)
案例rn:RightNil[Int]=>bounceBackAndForth(rn.prev,maxIters-1,-1)
案例c:Cons[Int]=>{
如果(最大值%100003==0)println(c.值)
如果(方向<0){
反弹前后(c.prev,最大值-1,-1)
}否则{
反弹前后(c.next,最大值-1,+1)
}
}
}
}
bounceBackAndForth(dll,1000000)
//cs_XIIIp4

备注:我不觉得递归的
构建
-方法特别直观,我不能不在纸上涂鸦几分钟就直接写下来。老实说,每次它起作用时我都有点惊讶。

你可以让一个列表在构建后保持不变,并以常量给出内容指令参数。是的,您可以了解(并引用它)没有完全初始化。Bergi,我觉得这是最近在一次
let rec
对话中出现的,如果我的记忆正确的话。我找不到问题tho…@Bergi你怎么做到这一点?我认为不变性的目的之一是初始化实际上是原子化的?如果你想让你的头受伤的话位,阅读Haskell中的“信用卡转换”和“打结”。给定表达式
case-class-HalfRing(val-value:Int)中的
otherHalf
(otherHalf:=>HalfRing)
隐式地是一个
var
上述示例不会被视为纯粹的函数。@sungiant这是一个thunk,而不是
var
。在Haskell或任何其他纯函数式语言中,相互递归的
let
表达式应该以相同的方式工作。是的,对不起,您是对的。它是一个按名称调用的参数。
val dll = DLL((42 to 100).toList)

println((1 to 20).map(dll))

@annotation.tailrec 
def bounceBackAndForth(
  dll: DLL[Int], 
  maxIters: Int, 
  direction: Int = +1
): Unit = {
  if (maxIters <= 0) println("done")
  else dll match {
    case ln: LeftNil[Int] => bounceBackAndForth(ln.next, maxIters - 1, +1)
    case rn: RightNil[Int] => bounceBackAndForth(rn.prev, maxIters - 1, -1)
    case c: Cons[Int] => {
      if (maxIters % 100003 == 0) println(c.value)
      if (direction < 0) {
        bounceBackAndForth(c.prev, maxIters - 1, -1)
      } else {
        bounceBackAndForth(c.next, maxIters - 1, +1)
      }
    }
  }
}

bounceBackAndForth(dll, 1000000)

// cs_XIIIp4