给定f :: a -> a
类型的函数,我们可以生成一个适用f
n
次的函数:
nTimes :: Int -> (a -> a) -> (a -> a)
nTimes 0 _ = id
nTimes 1 f = f
nTimes n f = f . nTimes (n-1) f
我可以在这里使用exponentiating by squaring方法来实现另一个nTimes
函数:
nTimes' :: Int -> (a -> a) -> (a -> a)
nTimes' = nTimes'' id
where
nTimes'' acc n f
| n == 0 = acc
| even n = nTimes'' acc (n `div` 2) (f . f)
| otherwise = nTimes'' (acc . f) (n-1) f
我的问题是:
nTimes
和nTimes'
是否会产生相同的结果?nTimes'
会更快吗?答案 0 :(得分:8)
虽然等同于,但如果在任何实际情况下ntimes'
实际上更快或节省内存,我会感到非常惊讶。问题是,与x * x
通过平方加倍普通求幂不同,f . f
实际上并不共享在应用f
时完成的任何真实工作。最后还是将最外面的单f
应用于由所有余数以某种方式构造的参数。并且ntimes (n-1) f x
将是关于你可以拥有的最紧凑的表示,直到它本身实际上需要被评估,这将需要将其最左边的f
应用于代表ntimes (n-2) f x
等。
编辑:让我补充一点,如果您正在进行 memoization ,这可能会发生显着变化,即将f . f
替换为memo (f . f)
,以修改一个修改要记住的函数的memo-combinator结果。在这种情况下,可以共享实际工作,这个版本的ntimes'
可能有时候会有所改进。其他时候,它可能会浪费大量的内存。
答案 1 :(得分:6)
在两种情况下都会产生相同的结果,因为*
和.
都是关联运算符。
然而,“加速”并不是你想到的加速。通过平方取幂是好的,因为它减少了*
运算符的应用次数,从线性到对数次数。在这种情况下,您将减少应用.
运算符的次数,从线性到对数次数。
然而,像ØrjanJohansen所说,与*
不同,.
运算符并没有真正做太多 - 它只需要两个函数值,并输出一个新的函数值,它基本上包含了给出了两个函数加上一些代码。
从nTimes'
获得的结果函数,当应用于某个值时,必须仍然运行f
n次。因此,实际上运行结果函数没有任何改进,只是使用.
构造结果函数的过程得到了改进。
答案 2 :(得分:4)
nTimes和nTimes'总是产生相同的结果?
是。 (除非你有bug,否则不检查)。
nTimes'更快?
可能不显着。 f
本身在两种情况下都是共享的,因此没有重新计算。
如果f
不够懒惰,那么您正在构建一个对f
的引用列表与一个共享f
引用的树的引用,因此您在这里节省了一些内存