作为练习,我想我会尝试在Scala中实现一个不可变的双向链表。目前lazy val
导致堆栈溢出。有人可以解释为什么会这样吗?我非常确定递归函数通常会终止,但长度为3是一个非常小的数字,可以从终止函数创建溢出。似乎懒惰的事情意味着它会陷入某个循环中。
class Node(val prev: Option[Node], val next: Option[Node], val id: Int){
override def toString = "["+id+"] "+next.toString
}
def addNodes(nNodes: Int, last: Node): Node = {
if(nNodes > 0){
lazy val newNode: Node =
new Node(Some(last), Some(addNodes(nNodes-1, newNode)),nNodes)
newNode
} else {
new Node(Some(last), None, nNodes)
}
}
def doublyLinked(n:Int) = {
lazy val list: Node = new Node(None, Some(addNodes(n-2, list)),n-1)
list
}
val x = doublyLinked(3)
println(x)
答案 0 :(得分:6)
此处的问题不在于addNodes
,而在于lazy val
本身。
lazy val newNode: Node =
new Node(Some(last), Some(addNodes(nNodes-1, newNode)),nNodes)
要计算newNode
,您需要调用addNodes(nNodes-1, newNode)
,但这意味着您需要newNode
值。 lazy val
内部是通过方法实现的,所以这是导致堆栈溢出的递归。
在没有懒惰的情况下构建循环不可变数据结构是不可能的,事实证明你的实现不够懒惰。尝试使用此结构(与您已有的addNodes
和doublyLinked
实现完全相同):
class Node(_prev: =>Option[Node], _next: =>Option[Node], val id: Int){
lazy val prev = _prev
lazy val next = _next
override def toString = s"[$id]${next.toString}"
}
我们的想法是使prev
和next
按名称调用参数,并通过公共惰性值公开它们。这给了足够的懒惰来实现不可变的双向链表。在这种情况下,上面没有描述递归,因为按名称调用参数意味着在读取相应节点的Some(addNodes(nNodes-1, newNode))
字段之前不会计算next
。
但请注意,不可变的双向链表是一种不方便的结构。为了向列表添加新元素,您必须从头开始重建它,与单链表相反:在单链表的前面添加元素是一个恒定时间操作,不需要重建整个清单。插入中间需要仅重建插入元素之前的部分。这就是它在功能语言中如此受欢迎的原因。双链表需要完全重建任何修改。