我正在学习Haskell并正在阅读Tying the Knot有关如何构建循环链表的信息。在代码中
data DList a = DLNode (DList a) a (DList a)
mkDList :: [a] -> DList a
mkDList [] = error "must have at least one element"
mkDList xs = let (first,last) = go last xs first
in first
where go :: DList a -> [a] -> DList a -> (DList a, DList a)
go prev [] next = (next,prev)
go prev (x:xs) next = let this = DLNode prev x rest
(rest,last) = go this xs next
in (this,last)
我正在尝试理解他们将最后一个元素链接到第一个元素的调用通过"小技巧" (!)称打结:
mkDList xs = let (first,last) = go last xs first
但我很难看到它是如何工作的。什么是" go"最初打电话给?根据文章中的评论,第一个结果如何来自" go" "传回"?
谢谢!
答案 0 :(得分:2)
由于Haskell是惰性的,因此在严格必要时评估值。我们可以使用等式推理来查看一个简单的例子,看看它在哪里得到了我们。
从最简单的例子开始:单元素列表。
mkDList [1] == let (first, last) = go last [1] first in first
您似乎无法致电go
,因为您还不知道last
和first
等于什么。但是,您可以将它们视为未经评估的黑匣子:它们不是它们是什么,只是您可以与它们进行等式推理。
-- Just plug last and first into the definition of go
-- last2 is just a renaming of the argument for clarity
go last [1] first == let this = DLNode last 1 rest
(rest, last2) = go this [] first
in (this, last2)
让我们尝试以同样的方式评估对go
的下一次调用。
go this [] first == (first, this)
方便的是,我们不需要想象任何新的黑匣子; go
只是以稍微重新包装的方式评估其原始参数。
好的,现在我们可以回到我们的方式,并将go
的递归调用替换为其评估。
go last [1] first == let this = DLNode last 1 rest
(rest, last2) = (first, this)
in (this, last2)
然后我们将 重新插入原始等式mkDList
:
mkDList [1] == let (first, last) = let this = DLNode last 1 rest
(rest, last2) = (first, this)
in (this, last2)
in first
这看起来并没有太大帮助,但请记住,我们实际上还没有调用 mkDList
;我们只是使用等式推理来简化其定义。特别是,没有递归调用go
,只有一个let
表达式嵌套在另一个中。
由于Haskell很懒惰,我们不必在绝对必要之前评估其中的任何内容,例如当我们尝试与mkDlist [1]
的返回值进行模式匹配时:
let (DLNode p x n) = mkDList [1] in x
要评估此表达式,我们只需要提出以下问题:
x
的价值是多少?"答:我们需要首先与mkDList [1]
进行模式匹配。mkDList
的价值是多少?"答案:first
。first
的价值是多少?"答案:this
。this
的价值是多少?"答案:DLNode last 1 rest
此时,您有足够的信息可以看到x == 1
,last
和rest
不需要进一步评估。但是,您可以再次模式匹配以查看例如p
是什么,并发现
p == last == last2 == this == DLNode last 1 rest
和
n == rest == first == this == DLNode last 1 rest
魔术就像(first, last) = go last xs first
这样的调用实际上需要的参数值;它只需要占位符来跟踪first
和last
在评估时最终会得到什么值。这些占位符称为" thunks",它们代表未评估的代码片段。他们让我们参考我们还没有填充任何东西的盒子,我们可以将空盒子传递给go
安全,因为知道有人会在其他人试图查看它们之前填充它们。 (事实上,go
本身永远不会;它只是不断地传递它们,直到mkDList
的某个外试图查看它们。)
答案 1 :(得分:1)
我们可以先尝试一些简单的输入,看看那里发生了什么:
mkDList [1] = first
where
(first,last) := go last [1] first
= let { prev=last; next=first; -- prev = last
x=1; xs=[] } -- next = first
in go prev (x:xs) next
= let this := (DLNode prev 1 rest) -- a node is constructed, with
-- the two pointers still pointing into the unknown
(rest,last2) := go this [] next
= (next,this) -- rest := next
-- last2 := this
in (this,last2) -- first := this
-- last := last2
Haskell中的 let
是递归的:同名可以出现在等式符号的左侧和右侧,并且将引用同一个实体。
首先,使用go
,last
和[1]
调用first
。 last
和first
都没有引用任何值;它们只作为身份存在,仍然是空盒子的“命名指针”,尚未被“填充”值的盒子。
进入go
的胆量,两个名字都被“充实”,然后返回最终的值 (this, last2)
;然后模式(first, last)
与该值匹配。这就是last
最终获得其值的方式,即使它在go
调用中用作命名标识。这就是所谓的结的结合:想象一个箭头从last
“走出”进入go
调用,从深处回来;与first
相同;从而创建等价链:
first := this = (DLNode prev 1 rest)
last := last2 := this
prev = last
rest := next = first
上面是Haskell作为单一赋值语言的语义的一些必要观点。使用:=
运算符作为伪代码,为通过右侧表达式计算值的事实提供视觉线索,并将其“传递”到左侧模式中的变量中。等号(当该模式与计算值匹配时)。
实际上,名称“next”并不好,因为我们只是将第一个节点一直向下传递,以用作最后一个节点的下一个节点:
mkDList xs@(_:_) = first where (first,last) = go last xs first
go :: DList a -> [a] -> DList a -> (DList a, DList a)
go prev (x:xs) first =
(this, last) -- (this , last )
where
this := DLNode
prev x rest
( rest, last) := go
this xs first
go prev [] first =
(first, -- first --> rest of the last node
prev)
这可以通过等效的Prolog定义来“描述”:
mkDList( [X|XS], First) :- % mkDList( in, out)
go( Last, [X|XS], First, First, Last). % go( in, in, in, out, out)
go( Prev, [X|XS], First, This, Last) :- This = dlnode(Prev, X, Rest),
go( This, XS, First, Rest, Last). % fill the Rest, return Last
go( Prev, [], First, First, Prev).
实际上,
?- mkDList([1,2,3],X).
X = dlnode(_S1, 1, _S2), % where
_S2 = dlnode(X, 2, _S1),
_S1 = dlnode(_S2, 3, X).