这个列表如何理解其自身的工作原理?

时间:2015-04-27 14:48:44

标签: haskell recursion lazy-evaluation

在#haskell IRC频道有人问

  

是否有一种简洁的方法来定义一个列表,其中第n个条目是之前所有条目的平方和?

我认为这听起来像一个有趣的谜题,递归地定义无限列表是我真正需要练习的事情之一。所以我启动了GHCi并开始使用递归定义。最终,我设法进入

λ> let xs = 1 : [sum (map (^2) ys) | ys <- inits xs, not (null ys)]

似乎产生了正确的结果:

λ> take 9 xs
[1,1,2,6,42,1806,3263442,10650056950806,113423713055421844361000442]

不幸的是,我不知道我写的代码是如何工作的。是否有可能解释当代码以中间Haskell用户理解的方式执行时会发生什么?

3 个答案:

答案 0 :(得分:5)

归结为懒惰的评价。让我们按照奥古斯都的定义,因为它只是稍微简单一点,但称之为big而不是xs,因为该标识符通常用于实用程序。

Haskell仅在必要时评估代码。如果不需要某些东西,那里有一个存根,基本上是一个指向函数闭包的指针,可以根据需要计算该值。

我们想说我要评估big !! 4!!的定义是这样的:

[] !! _ = error "Prelude.(!!): index too large"
(x:_) !! 0 = x
(_:xs) !! n = xs !! (n-1)

big的定义是

big = 1 : [sum (map (^2) ys) | ys <- tail (inits big)]

因此,在评估索引访问时,首先要做的是必须选择正确的函数变量。列表数据类型有两个构造函数[]first : rest。调用是big !! 4!!的第一个分支只检查列表是否为[]。由于列表显式以1 : stub1开头,因此答案为否,并跳过分支。

第二个分支想知道是否选择了first : rest形式。答案是肯定的,first 1rest是理解力很强(stub1),其价值无关紧要。但第二个参数不是0,所以也跳过了这个分支。

第三个分支也匹配first : last,但接受第二个参数的任何内容,因此适用。它会忽略first,将xs绑定到未评估的理解stub1,将n绑定到4。然后它以递归方式调用自身,第一个参数是理解,第二个3。 (从技术上讲,那个(4-1)并且还没有被评估过,但作为一种简化,我们假设它是。)

再次递归调用必须评估其分支。第一个分支检查第一个参数是否为空。由于到目前为止的参数是未评估的存根,因此需要对其进行评估。但只能足以决定分支是否为空。所以让我们开始评估理解:

stub1 = [sum (map (^2) ys) | ys <- tail (inits big)]

我们需要的第一件事是ys。它设置为tail (inits big)tail很简单:

tail [] = []
tail (_:xs) = xs

inits实现起来相当复杂,但重要的是它会懒惰地生成结果列表,即如果你给它(x:unevaluated),它将生成[]和{{1在评估列表的其余部分之前。换句话说,如果你不要超越这些,它就不会评估其余的。

因此,到目前为止[x]已为big,因此(1 : stub1)会返回inits[] : stub2与此匹配,选择其第二个分支,然后返回tailstub2是无所不在的空列表后stub2的inits列表,但尚未生成。

列表推导然后尝试给big ys的第一个元素的值,因此必须对其进行评估。 stub2的第二个结果仍然是已知的,它是inits[1]获得该值。此时,ys已知为big,其中1 : stub3 : stub4stub3 = sum (map (^2) [1])是第一次迭代后的列表理解。

由于stub4现在进一步评估,big也是如此。它现在已知为stub1,我们终于可以进入stub3 : stub4。第一个分支不适用,因为列表不是空的。第二个分支不适用,因为!!。第三个分支适用,将3 /= 0绑定到xs,将stub4绑定到n。递归调用是3

我们需要评估一点stub4 !! 2。这意味着我们进入理解的下一次迭代。我们需要stub4的第三个元素。由于inits big现在已知为big,因此可以在不进一步评估的情况下计算第三个元素1 : stub3 : stub4[1, stub3]绑定到此值,ys评估为stub4,其中stub5 : stub6stub5 = sum (map (^2) [1, stub3])是前两次迭代后的理解。评估stub6后,我们现在知道stub4

所以big = 1 : stub3 : stub5 : stub6仍然不匹配stub4的第一个分支(从来没有,因为我们正在处理无限列表)。 !!仍然与第二个分支不匹配。我们有另一个递归调用,然后是另一个,遵循我们到目前为止的相同模式。当指数最终达到0时,我们有:

2

我们当前的呼叫是big = 1 : stub3 : stub5 : stub7 : stub9 : stub10 stub3 = sum (map (^2) [1]) stub5 = sum (map (^2) [1, stub3]) stub7 = sum (map (^2) [1, stub3, stub5]) stub9 = sum (map (^2) [1, stub3, stub5, stub7]) stub10 = whatever remains of the list comprehension ,最终匹配第二个分支。 (stub9 : stub10) !! 0绑定x并返回。

直到现在,如果您真的尝试打印或以其他方式处理stub9,所有这些存根最终都会被评估为实际数字。

答案 1 :(得分:3)

好的,我会试试。

(我不确定你正在寻找什么“中级”水平,所以我会向自己解释这一点,希望它不是太“次级中间”。)

sum (map (^2) ys)很简单:列表的平方和。

生成器也很简单:y接受xs的所有非空初始序列,即(略微滥用符号)y <- [take 1 xs, take 2 xs, take 3 xs,...]

(我将在下面保留take符号,因为我认为非常清楚。很可能不是你的闪亮Haskell机器内部发生的事情。)

唯一棘手的事情就是组合它们,因为xs是我们定义的价值 这不是一个大问题,因为我们知道xs的第一个元素 - 它是1。 它并不多,但它是用take 1 xs推动球的所需要的一切。

再多忙,xs

1 : (sum (map (^2) (take 1 xs))) : (sum (map (^2) (take 2 xs))) : ...

那是(因为我们知道第一个元素是1):

xs = 1 : (sum (map (^2) [1])) : (sum (map (^2) (take 2 xs))) : ...

xs = 1 : 1 : (sum (map (^2) (take 2 xs))) : (sum (map (^2) (take 3 xs))) : ...

我们有第二个元素,我们可以继续:

xs = 1 : 1 : (sum (map (^2) [1,1])) : (sum (map (^2) (take 3 xs))) : ...

xs = 1 : 1 : 2 : (sum (map (^2) (take 3 xs))) : (sum (map (^2) (take 4 xs))) : ...

xs = 1 : 1 : 2 : (sum (map (^2) [1,1,2])) : (sum (map (^2) (take 4 xs))) : ...

等等。

这一点起作用的原因是列表中的每个元素仅取决于之前的元素 - 你总是可以依靠过去来告诉你发生了什么;未来不太可靠。

答案 2 :(得分:1)

稍微动摇你的代码,直到它变成一种更容易理解的形式,我们得到

xs = 1 : [sum (map (^2) ys) | ys <- inits xs, not (null ys)]
   = 1 : (map (sum . map (^2)) . map (`take` xs)) [1..]
   = 1 : map (sum . map (^2) . (`take` xs)) [1..]
   = 1 : scanl1 (\a b-> a+b^2) xs
   = x1 : xs1
              where { x1  = 1; 
                      xs1 = scanl1 g xs 
                          = scanl g x1 xs1;   -- by scanl1 definition
                      g a x = a+x^2 }

scanl适用于非空列表

scanl g a xs = a : case xs of (h:t) -> scanl g (g a h) t

所以xs1 = scanl g a xs1首先将当前已知的累积值放在其输出头部(xs1 = (a:_)),然后才会读取该输出,因此这个定义很有效。我们还看到h = ag a h = g a a = a+a^2 = a*(a+1),我们可以纯粹迭代地编码此流,

xs = 1 : iterate (\a -> a*(a+1)) 1