如何打结"在Haskell的这个循环链表上工作?

时间:2017-01-06 16:26:46

标签: haskell lazy-evaluation

我正在学习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" "传回"?

谢谢!

2 个答案:

答案 0 :(得分:2)

由于Haskell是惰性的,因此在严格必要时评估值。我们可以使用等式推理来查看一个简单的例子,看看它在哪里得到了我们。

从最简单的例子开始:单元素列表。

mkDList [1] == let (first, last) = go last [1] first in first

您似乎无法致电go,因为您还不知道lastfirst等于什么。但是,您可以将它们视为未经评估的黑匣子:它们不是它们是什么,只是您可以与它们进行等式推理。

-- 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

要评估此表达式,我们只需要提出以下问题:

  1. " x的价值是多少?"答:我们需要首先与mkDList [1]进行模式匹配。
  2. " mkDList的价值是多少?"答案:first
  3. " first的价值是多少?"答案:this
  4. " this的价值是多少?"答案:DLNode last 1 rest
  5. 此时,您有足够的信息可以看到x == 1lastrest不需要进一步评估。但是,您可以再次模式匹配以查看例如p是什么,并发现

    p == last == last2 == this == DLNode last 1 rest
    

    n == rest == first == this == DLNode last 1 rest
    

    魔术就像(first, last) = go last xs first这样的调用实际上需要的参数值;它只需要占位符来跟踪firstlast在评估时最终会得到什么值。这些占位符称为" 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是递归的:同名可以出现在等式符号的左侧和右侧,并且将引用同一个实体。

首先,使用golast[1]调用firstlastfirst都没有引用任何值;它们只作为身份存在,仍然是空盒子的“命名指针”,尚未被“填充”值的盒子。

进入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).