在#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用户理解的方式执行时会发生什么?
答案 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
1
和rest
是理解力很强(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
与此匹配,选择其第二个分支,然后返回tail
。 stub2
是无所不在的空列表后stub2
的inits列表,但尚未生成。
列表推导然后尝试给big
ys
的第一个元素的值,因此必须对其进行评估。 stub2
的第二个结果仍然是已知的,它是inits
。 [1]
获得该值。此时,ys
已知为big
,其中1 : stub3 : stub4
和stub3 = 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 : stub6
和stub5 = 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 = a
,g a h = g a a = a+a^2 = a*(a+1)
,我们可以纯粹迭代地编码此流,
xs = 1 : iterate (\a -> a*(a+1)) 1