为什么每次尝试获取其值时都会调用此函数?

时间:2017-03-17 01:30:24

标签: haskell recursion pi

我使用系列π/4 = 1 - 1/3 + 1/5 - 1/7 + 1/9...来编写pi来编写pi的Haskell代码:

quarterPi total n s = if (n /= total) then s / (2 * n - 1) + quarterPi total (n + 1) (-s) else s / (2 * n - 1)

calcPi n = 4 * quarterPi n 1 1

我使用GHCi前奏运行代码并尝试在变量中存储pi的近似值;类似于:*Main> pi = calcPi 666666,在完成计算之前显然会进行许多递归迭代。据我所知,第一次检索pi的值时,由于延迟评估,计算时间会很长。但出于某种原因,它总是需要很长时间;无论我使用该值多少次或输出屏幕上的值,都需要几秒钟才能产生结果。由于懒惰的评估,它不应该只计算一次吗?

2 个答案:

答案 0 :(得分:2)

你在这里咬什么是所谓的单态限制 - 但不同寻常的是,它是单形态限制的缺乏,而不是它的存在。结果pi并不像你预期的那样恒定,因为它的类型比你可能意识到的更为通用。

如果我们在REPL打开分析信息:

*Main> :set +s

然后我们可以看到pi每次重新计算,就像你说的那样。

*Main> pi = calcPi 666666
(0.00 secs, 0 bytes)
*Main> pi
3.141591153588293
(0.94 secs, 551,350,552 bytes)
*Main> pi
3.141591153588293
(0.99 secs, 551,300,808 bytes)

这是因为pi类型被推断为类型类多态:

*Main> :t pi
pi :: (Fractional a, Eq a) => a

所以pi在某种意义上说“不是真正的常数”;有很多pi s!有pi :: Double,有pi :: Float,有pi :: Complex Double等等。并且GHC不缓存函数应用程序,因此每次都会重新计算pi

但是,在Haskell 文件中,单态限制生效:变量绑定(如x = …)永远不会被推断为具有像C a => …这样的类型类多态类型。如果可能的话,这样的定义是默认的 - 一个Haskell在IntegerDouble单态的标准数字类型中使值具有多态性的过程。这就是为什么你可以写x^2而不会得到“模糊类型变量”错误(即使指数的类型不影响输出的类型);默认情况下,2被推断为Integer类型。

但单态限制仅适用于文件内部。在GHCi中,它已被禁用。如果我们重新开启单态限制:

*Main> :set -XMonomorphismRestriction

然后我们可以定义pi',我们只需要计算一次。

*Main> pi' = calcPi 666666
(0.00 secs, 0 bytes)
*Main> pi'
3.141591153588293
(0.87 secs, 551,303,896 bytes)
*Main> pi'
3.141591153588293
(0.00 secs, 315,168 bytes)

那是因为pi'确实是一个常数。

*Main> :t pi'
pi' :: Double

另外几点说明:

  1. 懒惰与第二次计算pi'快速的原因无关;在严格的语言中也是如此。懒惰就是为什么我们定义pi / pi'的行报告“(0.00秒,0字节)”。

  2. 我不清楚你是否认为它可能,但Haskell 永远不会缓存你正在进行的递归调用。像calcPi 666666这样的东西总是会进行所有666,666个递归调用。只有在您停止重新计算的变量中明确保存它之后才会这样做。

  3. 单态限制和类型默认在Haskell 2010报告的技术语言中进行了解释:§4.4.5 "The Monomorphism Restriction§4.3.4 "Ambiguous Types, and Defaults for Overloaded Numeric Operations"。 GHC用户指南提到GHCi中禁用了单态限制,并提到了如何重新启用它:§9.17.1 "Switching off the dreaded Monomorphism Restriction"

答案 1 :(得分:0)

GHCi没有优化就足以弄清楚每次打印时都不应该重新评估它。如果使用通常的-O2标志进行编译,则正常GHC将正确缓存结果。

Try it online并将总运行时间与打印出来的次数进行比较“Pi:3.14 ......”