“怎么做?”;“结婚”;在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

我正在学习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 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
要评估此表达式,我们只需要问以下问题:

  • “x的值是多少?”回答:我们需要先对mkDList[1]进行模式匹配
  • “mkDList的值是多少?”回答:
    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)。
    
    Initially
    first
    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)