“怎么做?”;“结婚”;在Haskell作品中的循环链表上?
我正在学习Haskell,正在通读如何构建循环链表。在代码中“怎么做?”;“结婚”;在Haskell作品中的循环链表上?,haskell,lazy-evaluation,Haskell,Lazy Evaluation,我正在学习Haskell,正在通读如何构建循环链表。在代码中 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
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”的第一个结果是如何传入的
谢谢 由于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
要评估此表达式,我们只需要问以下问题:
first
的值是多少?”回答:这个
DLNode last 1 rest
x==1
,last
和rest
不需要进一步计算。不过,您可以再次进行模式匹配,以查看例如,p
是什么,并发现它
p == last == last2 == this == DLNode last 1 rest
及
神奇的是,调用类似于
(first,last)=go last xs first
实际上不需要参数值;它只需要占位符来跟踪计算值时,first
和last
最终得到的值。这些占位符称为“thunks”,它们表示未计算的代码片段。他们让我们参考那些我们还没有装满任何东西的盒子,我们可以将空盒子传递给go
safe,因为我们知道有人会在其他人试图查看之前填满它们。(事实上,go
本身从来不会这样做;它只是一直传递它们,直到mkDList
之外的人试图查看它们。)我们可以先尝试一些简单的输入,看看那里发生了什么:
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中的是递归的:相同的名称可以出现在等式符号的左侧和右侧,并将引用相同的实体
首先,使用last
、[1]
和First
调用go
。last
和first
均未引用任何值;它们只作为标识存在,类似于指向仍然是空的框的“命名指针”,这些框尚未被值“填充”
进入go
,两个名称都“充实”,然后返回最终值(this,last2)
;然后模式(第一个,最后一个)
与该值匹配。这就是last
最终获取其值的方式,即使它在go
调用中被用作命名标识。这就是所谓的打结:想象一支箭从last
进入go
调用,从深处返回;与第一个相同;因此,创建等效链:
first := this = (DLNode prev 1 rest)
last := last2 := this
prev = last
rest := next = first
以上内容遵循了Haskell的语义作为单一赋值语言的一种强制性观点。:=
运算符用作伪代码,用于提供一个视觉线索,说明值是由右侧的表达式计算的,并且“传递”到等号左侧模式中的变量(当该模式与计算值匹配时)
事实上,名称“next”并不好,因为我们只是沿着第一个节点一直向下传递,以用作最后一个节点的下一个节点: 这可以通过等效的序言定义来“描述”:
mkDList([X | XS],第一):-%mkDList(输入,输出)
开始(最后,[X | XS],第一,第一,最后)。%前进(进、进、进、出、出)
go(Prev[X | XS],First,This,Last):-This=dlnode(Prev,X,Rest),
开始(这个,XS,第一个,其余,最后一个)。%剩下的填好,最后回来
开始(上一页,[],第一页,第一页,上一页)。
的确
?-mkDList([1,2,3],X)。
X=dlnode(_S1,1,_S2),%其中
_S2=dlnode(X,2,_S1),
_S1=dlnode(_S2,3,X)。
Initiallyfirst
和last
都是简单的thunk,即未计算的表达式,诀窍在于go
函数在参数值准备好之前不会计算参数。作为一个实验,我尝试使用一个显式表达式,以一种过于文字化的方式将打结代码改编为Java
first := this = (DLNode prev 1 rest)
last := last2 := this
prev = last
rest := next = first
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)