【问题标题】:How does this list comprehension over the inits of itself work?这个对自身单位的列表理解是如何工作的?
【发布时间】:2015-07-06 02:21:43
【问题描述】:

在#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 用户可以理解的方式执行此代码时会发生什么?

【问题讨论】:

  • 如果你从字面上理解这个问题,正确的定义当然是xs = [0..],因为第一个之前所有条目的平方和为0,因此每个后续的总和也是如此。跨度>
  • 与您的问题无关,但您可以改写为let xs = 1 : [sum (map (^2) ys) | ys &lt;- tail (inits s)]
  • @augustss 我一直在寻找类似的东西,但没有考虑。这样干净多了!
  • @SebastianRedl:你的意思是[0,0..](或repeat 0)。
  • 另一种选择:xs = 1 : scanl1 (+) (map (^2) xs)

标签: haskell recursion lazy-evaluation


【解决方案1】:

归结为惰性评估。让我们使用 augustss 的定义,因为它稍微简单一些,但将其称为 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 形式。答案是肯定的,first1rest 是那么大的理解 (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),它会在评估其余部分之前生成[][x]列表。换句话说,如果您不超越这些,它将永远不会评估其余部分。

所以,到目前为止,big 已知为(1 : stub1),所以inits 返回[] : stub2tail 与此匹配,选择它的第二个分支,并返回 stub2stub2big 在无所不在的空列表之后的inits 列表,尚未生成。

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

由于big 现在被进一步评估,stub1 也是如此。现在知道它是stub3 : stub4,我们终于可以在!! 前进了。第一个分支不适用,因为列表不为空。第二个分支不适用,因为3 /= 0。第三个分支适用,将xs 绑定到stub4n3。递归调用是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 仍然不匹配!! 的第一个分支(永远不会匹配,因为我们正在处理一个无限列表)。 2 仍然与第二个分支不匹配。我们有另一个递归调用,然后是另一个,遵循与到目前为止相同的模式。当索引最终达到 0 时,我们有:

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 并返回。

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

【讨论】:

  • 非常感谢。从昨天开始,我已经读了几次,它开始变得有意义了。还没有闪现的洞察力,但我慢慢地理解了。我想我可能不得不为自己重新做你的工作,但要自己用笔和纸,并认真考虑评估顺序和分享发生的情况。感谢您进行冗长而彻底的说明。
【解决方案2】:

好的,我试试。

(我不确定您要寻找的“中级”级别,所以我会向自己解释一下,希望它不会太“次中级”。)

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

生成器也很简单:y 接受xs 的所有非空初始序列,即(有点滥用符号)y &lt;- [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))) : ...

等等。

之所以如此有效,是因为列表中的每个元素都只依赖于前面的元素——你总是可以依靠过去来告诉你发生了什么;未来不太可靠。

【讨论】:

  • 谢谢。将tail (inits xs) 视为增加takes 的无限列表具有创造性,实际上有助于从根本上理解代码的作用。我仍然很难将它与 Haskell 机器的实际执行联系起来,但我想这就是 Sebastians 的回答是一个很好的补充。感谢您的回答!
  • @kqr 关于 Haskell 的实际执行,我了解到的一件事是,非常努力地思考它并没有太大帮助 ;-) The Implementation of Functional Programming Languages [Peyton-Jones 等人] 可用如果你想深入挖掘,在线。可读性很强。
  • 如此真实!不过,在某些情况下,我的好奇心会胜出……而且,对于喜欢死树格式的人来说,那本书很贵。 :(
【解决方案3】:

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

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

【讨论】:

    猜你喜欢
    • 2019-08-30
    • 2016-04-01
    • 2020-03-19
    • 2013-04-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-15
    • 2019-12-07
    相关资源
    最近更新 更多