【问题标题】:self-reference in Haskell functionsHaskell 函数中的自引用
【发布时间】:2011-06-15 23:44:25
【问题描述】:

我正在学习 Haskell,我在 Haskell Wiki 上的以下表达式 真的让我很困惑:

fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

我不太明白为什么会这样。

如果我应用标准 Currying 逻辑 (zipWith (+)) 返回一个以列表为参数的函数,然后返回另一个以另一个列表作为参数的函数,并返回一个列表 (zipWith::(a -> b -> c) -> [a] -> [b] -> [c])。因此,fibs 是对列表(尚未评估)的引用,(tail fibs) 是同一(未评估)列表的尾部。当我们尝试评估 (take 10 fibs) 时,前两个元素绑定到 01。换句话说,fibs==[0,1,?,?...](tail fibs)==[1,?,?,?]。第一次添加完成后,fibs 变为 [0,1,0+1,?,..]。同样,在第二次加法之后,我们得到[0,1,0+1,1+(0+1),?,?..]

  • 我的逻辑正确吗?
  • 有没有更简单的方法来解释这个? (知道 Haskell 编译器如何处理此代码的人有什么见解吗?)(欢迎提供链接和参考)
  • 这种类型的代码真的是因为惰性求值才有效吗?
  • 当我执行fibs !! 4 时会发生什么评估?
  • 此代码是否假定 zipWith 先到后处理元素? (我认为不应该,但我不明白为什么不)

EDIT2:我刚刚发现上面提出的问题并得到了很好的回答here。如果我浪费了任何人的时间,我很抱歉。

编辑:这是一个更难理解的案例(来源:Project Euler forums):

filterAbort :: (a -> Bool) -> [a] -> [a]
filterAbort p (x:xs) = if p x then x : filterAbort p xs else []

main :: Int
main = primelist !! 10000
         where primelist = 2 : 3 : 5 : [ x | x <- [7..], odd x, all (\y -> x `mod` y /= 0) (filterAbort (<= (ceiling (sqrt (fromIntegral x)))) primelist) ]

注意all (\y -&gt; x mod y /= 0)...这里引用x怎么不会导致无限递归?

【问题讨论】:

  • 这似乎是对的。这对我来说似乎也很简单——当你更多地使用 Haskell 时,你的大脑会开始接受这些模式,而且对你来说也很简单。好的开始。
  • 首先,filterAborttakeWhile 相同。其次,您可以通过写[7,9..] 来避免偶数。第三,如果您使用[5,7..],则无需在初始列表中包含5。最后,这个工作的原因是相当深刻的。这是因为对于每个素数pp^2 之前还有另一个素数。 Is 是 Lindemann 定理(p 和 2p 之间的素数)的一个微不足道的结果。
  • 谢谢,augustss。您建议的优化和清理是有道理的。至于终止,你能详细说明一下吗? x 绑定到 (\y -&gt; x mod y /= 0) 是什么?我怀疑我的错误在于认为它绑定到一个无限列表。如果它只绑定一个值(例如,7),那么就没有问题。你能确认一下吗?
  • x 绑定到单个元素,来自无限列表[7..]。这里没有递归。

标签: haskell recursion lazy-evaluation self-reference


【解决方案1】:

我会为你追踪评估:

> fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

与:

> zipWith f (a:as) (b:bs) = f a b : zipWith f as bs
> zipWith _ _      _      = []

> tail (_:xs)             =  xs
> tail []                 =  error "tail" 

所以:

  fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
↪ fibs = 0 : 1 : ((+) 0 1 : zipWith (+) (tail fibs) (tail (tail fibs)))
↪ fibs = 0 : 1 : 1 : ((+) 1 1 : zipWith (+) (tail (tail fibs)) (taii (tail (tail fibs)))))    
↪ fibs = 0 : 1 : 1 : 2 : ((+) 1 2 : zipWith (+) (tail (tail (tail fibs))) (tail (taii (tail (tail fibs))))))
↪ fibs = 0 : 1 : 1 : 2 : 3 : ((+) 2 3 : zipWith (+) (tail (tail (tail (tail fibs)))) (tail (tail (taii (tail (tail fibs)))))))

等等。因此,如果您对该结构进行索引,它将强制对 fibs 的每一步进行评估,直到找到索引为止。

为什么这样有效?一个词:分享

随着fibs 的计算,它在堆中增长,记录计算机的值。稍后对fibs 的反向引用可以重用之前为fibs 计算的所有值。免费记忆!

堆中的共享是什么样的?

当您请求列表末尾的对象时,Haskell 计算下一个值,记录它,并将自引用向下移动到一个节点。

这种终止的事实在很大程度上取决于懒惰。

【讨论】:

  • 谢谢你,唐。我对 Haskell 是如何知道为了获取下一个元素需要调用 fibs 以及如果 fibs 真的是一个列表而“调用 fibs”是什么意思还有些模糊。我环顾四周,发现了similar question,但我仍然不确定列表中的“1”节点和 Haskell 编译器的“fibs ...”节点之间有什么区别。
  • 您可以将fibs 视为一个指针。因此,每次您引用 fibs 时,它都会查找该值。该值恰好是一个列表。
  • 啊!那是个很好的观点。我应该纠正我的问题。符号0:1:zipWith ... 产生什么?它从一个列表开始,我理解。但是然后它会到达列表中的一个节点,它是一个函数调用?或者最好将列表中的所有节点都视为函数调用? IE。说ident x = x,所以fib = (ident 0):(ident 1):(zipWith ...)。在这种情况下,编译器会评估 all 单元格,但最后一个单元格最终会作为返回列表的函数调用?而(zipWith ...) 恰好指向fibs 列表。对吗?
  • 简化因为我不是专家:Haskell 中的每个非严格值都存储在一个称为 thunk 的结构中。它包含一个值,或者某种指向可以计算该值的代码的指针。因此,当您拥有 0:1:zipWith 时,第三个元素是一个指向代码的 thunk,而不是可以计算列表的其余部分。当该代码运行时,thunk 被一个 cons 单元格替换,该单元格具有列表的第三个元素的值,以及一个可以从第 4 个位置计算列表其余部分的 thunk。等等。
  • 谢谢,大卫。您正确理解 thunk 的概念在这里至关重要。读过Lazy Evaluation 第一个例子对我来说很有意义。一个人真的需要了解(编译器)覆盖下会发生什么,至少在概念上,才能写出好的代码。
猜你喜欢
  • 2015-06-25
  • 1970-01-01
  • 2011-07-20
  • 2018-06-14
  • 1970-01-01
  • 2020-06-13
  • 2014-11-01
  • 2011-08-03
  • 2013-03-17
相关资源
最近更新 更多