我使用系列π/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
的值时,由于延迟评估,计算时间会很长。但出于某种原因,它总是需要很长时间;无论我使用该值多少次或输出屏幕上的值,都需要几秒钟才能产生结果。由于懒惰的评估,它不应该只计算一次吗?
答案 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
。
x = …
)永远不会被推断为具有像C a => …
这样的类型类多态类型。如果可能的话,这样的定义是默认的 - 一个Haskell在Integer
或Double
单态的标准数字类型中使值具有多态性的过程。这就是为什么你可以写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
另外几点说明:
懒惰与第二次计算pi'
快速的原因无关;在严格的语言中也是如此。懒惰就是为什么我们定义pi
/ pi'
的行报告“(0.00秒,0字节)”。
我不清楚你是否认为它可能,但Haskell 永远不会缓存你正在进行的递归调用。像calcPi 666666
这样的东西总是会进行所有666,666个递归调用。只有在您停止重新计算的变量中明确保存它之后才会这样做。
单态限制和类型默认在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 ......”