我编写了这个 Haskell 代码,用于使用系列来计算 pi π/4 = 1 - 1/3 + 1/5 - 1/7 + 1/9...
:
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 prelude 运行代码,并尝试将 pi 的近似值存储在变量中;类似:*Main> pi = calcPi 666666
,这显然需要多次递归迭代才能完成计算。据我了解,第一次检索 pi
的值时,由于惰性求值,需要一些时间来计算。但出于某种原因,这总是需要很长时间;无论我使用该值多少次或在屏幕上输出该值多少次,都需要几秒钟才能产生结果。由于惰性评估,它不应该只需要计算一次吗?
最佳答案
这里困扰你的是所谓的单态限制 - 但不同寻常的是,它缺乏单态限制,而不是它的存在。事实证明,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
!有 pi::Double
、pi::Float
、pi::Complex Double
等等。而且GHC不会缓存函数应用程序,因此每次都会重新计算pi
。
然而,在 Haskell 文件中,单态限制是有效的:变量绑定(bind),如 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 secs, 0 bytes)”的原因。我不清楚您是否认为可能,但 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"
关于haskell - 为什么每次我尝试获取它的值时都会调用这个函数?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42847682/