对效率的适度尝试:
让我们接受 Robin 的前提,即递归是这里的实际主题。
递归就是用一个更简单的问题,或者用一个更简单的问题列表替换一个相对困难的问题。
例如,evenNat 43 是evenNat 42 的否定。如果我们沿着这条路线走,经过 40 多步后,我们会到达 evenNat 0,也就是众所周知的 True。该方案有效,但对于大量数据可能会有点慢。
一种更快的方法是在每一步从手头的数字中减去 2 的最大可能幂。这样的减法不会改变数字的偶数字符。所以 43 是偶数当且仅当 (43-32) = 11 是偶数。接下来,我们考虑 (11-8) = 3。所以它看起来肯定是一种更快但更复杂的方法。
首先,我们需要 2 的幂的序列,直到手头的数字。当然,这可以递归地完成:
powersOf2UpToN :: Integer -> [Integer]
powersOf2UpToN n =
let -- defining auxiliary function sq:
sq m pwrs = if (head pwrs > m) -- if reached big enough
then (tail pwrs) -- then stop and return result
else -- augment:
let hps = head pwrs in sq m ((hps+hps):pwrs)
in (sq n [2])
请注意,我使用的是(hps+hps) 而不是(2*hps),因为练习规则禁止本地乘法。
基本上,只要我们保持在手头的数字以下,我们就会在幂序列中添加越来越大的 2 幂。回想一下在 Haskell 中,将元素添加到列表中是一种廉价的操作。
在ghci解释器下测试:
$ ghci
λ>
λ> :load q64544005.hs
Ok, one module loaded.
λ>
λ> powersOf2UpToN 337
[256,128,64,32,16,8,4,2]
λ>
接下来,我们必须实现减去 2 的最大可能幂的函数。这样的函数将采用 (number, powers of 2) 对,并返回一个“改进的”对,即具有较小的数字和缩短的功率列表。类型签名如下:
reduce2 :: (Integer, [Integer]) -> (Integer, [Integer])`
整体逻辑是:
reduce2 (n, pwrs) =
if (null pwrs) -- did we just run out of powers of 2 ?
then (n, []) -- nothing left to do
else if ((head pwrs) <= n) -- return a lower n if possible
...
请在此处暂停阅读,看看是否可以填空。
解决方案:如果可能,我们减去 2 的最大剩余幂,它恰好位于(递减)列表的头部。
reduce2 (n, pwrs) =
if (null pwrs)
then (n, []) -- nothing left to do
else if ((head pwrs) <= n) -- return a lower n if possible
then reduce2 ((n - (head pwrs)), tail pwrs)
else reduce2 (n, tail pwrs)
reduce2 的机制是关键。因此,为了帮助理解,让我们利用 Haskell 方便的小型跟踪工具。
在下面的代码中,表达式DT.trace msg res 的计算结果仅为res,并具有打印msg 的副作用。是的,Haskell 中通常禁止副作用,但跟踪工具享有特殊特权。
所以这是我们reduce2函数的跟踪版本:
import qualified Debug.Trace as DT
type DebugMsg = String
traceReduce2 :: DebugMsg -> (Integer, [Integer]) -> (Integer, [Integer])
traceReduce2 msg (n, pwrs) =
let res = if (null pwrs)
then (n, [])
else let msg1 = (show n) ++ " " ++ (show pwrs)
rem = n - (head pwrs)
in if ((head pwrs) <= n)
then traceReduce2 msg1 (rem, tail pwrs)
else traceReduce2 msg1 (n, tail pwrs)
in DT.trace msg res
测试我们的跟踪版本:
λ>
λ> traceReduce2 "TOP" (337, (powersOf2UpToN 337))
TOP
337 [256,128,64,32,16,8,4,2]
81 [128,64,32,16,8,4,2]
81 [64,32,16,8,4,2]
17 [32,16,8,4,2]
17 [16,8,4,2]
1 [8,4,2]
1 [4,2]
1 [2]
(1,[])
λ>
所以我们差不多完成了。在处理完 2 的所有幂之后,0 或 1 的值保留为该对的左侧。我们可以继续编写我们的evenNat 函数:
evenNat :: Integer -> Bool
evenNat n =
let (remainder, ys) = reduce2 (n, powersOf2UpToN n)
in
-- here, remainder is set to 0 if and only if n is even
...
其余的应该很容易。