Haskell 序曲求幂很难理解
我在读哈斯克尔的前奏曲,发现它很容易理解,然后我偶然发现了指数的定义:Haskell 序曲求幂很难理解,haskell,functional-programming,haskell-prelude,Haskell,Functional Programming,Haskell Prelude,我在读哈斯克尔的前奏曲,发现它很容易理解,然后我偶然发现了指数的定义: (^) :: (Num a, Integral b) => a -> b -> a x ^ 0 = 1 x ^ n | n > 0 = f x (n-1) x where f _ 0 y = y f x n y = g x n where
(^) :: (Num a, Integral b) => a -> b -> a
x ^ 0 = 1
x ^ n | n > 0 = f x (n-1) x
where f _ 0 y = y
f x n y = g x n where
g x n | even n = g (x*x) (n `quot` 2)
| otherwise = f x (n-1) (x*y)
_ ^ _ = error "Prelude.^: negative exponent"
我不明白是否需要两个嵌套的where
s
到目前为止,我所理解的是:
(^) :: (Num a, Integral b) => a -> b -> a
基数必须是数字和指数整数,ok
x ^ 0 = 1
基本情况下,容易
g x n | even n = g (x*x) (n `quot` 2)
| otherwise = f x (n-1) (x*y)
平方幂。。。有点为什么需要f
助手?为什么要给f
和g
取单字母名称?是否只是优化,我是否遗漏了一些明显的东西
_ ^ _ = error "Prelude.^: negative exponent"
N>0之前被检查过,如果我们到达这里,N是负数,所以错误
我的实施将直接翻译为以下代码:
Function exp-by-squaring(x, n )
if n < 0 then return exp-by-squaring(1 / x, - n );
else if n = 0 then return 1; else if n = 1 then return x ;
else if n is even then return exp-by-squaring(x * x, n / 2);
else if n is odd then return x * exp-by-squaring(x * x, (n - 1) / 2).
平方函数exp(x,n)
如果n<0,则通过平方(1/x,-n)返回exp;
否则,如果n=0,则返回1;否则,如果n=1,则返回x;
否则,如果n为偶数,则通过平方(x*x,n/2)返回exp;
否则,如果n是奇数,则通过平方(x*x,(n-1)/2)返回x*exp。
维基百科的伪代码。
f
确实是一种优化。简单的方法是“自上而下”,计算x^(n`div`2)
,然后将结果平方。这种方法的缺点是它构建了一个中间计算堆栈。f
让这个实现做的是首先平方x
(一次乘法),然后递归地将结果提升到缩减指数尾部。最终结果是,该函数可能完全在机器寄存器中运行g
似乎有助于避免在指数为偶数时检查循环结束,但我不确定这是否是一个好主意。据我所知,只要指数为偶数,就可以通过平方运算来解决幂运算
这就引出了为什么在奇数情况下需要f
的答案-在gx1
情况下,我们使用f
返回结果,在其他每一个奇数情况下,我们使用f
返回g
例程
我想,如果你看一个例子,你会看到最好的结果:
x ^ n | n > 0 = f x (n-1) x
where f _ 0 y = y
f x n y = g x n
where g x n | even n = g (x*x) (n `quot` 2)
| otherwise = f x (n-1) (x*y)
2^6 = -- x = 2, n = 6, 6 > 0 thus we can use the definition
f 2 (6-1) 2 = f 2 5 2 -- (*)
= g 2 5 -- 5 is odd we are in the "otherwise" branch
= f 2 4 (2*2) -- note that the second '2' is still in scope from (*)
= f 2 4 (4) -- (**) for reasons of better readability evaluate the expressions, be aware that haskell is lazy and wouldn't do that
= g 2 4
= g (2*2) (4 `quot` 2) = g 4 2
= g (4*4) (2 `quot` 2) = g 16 1
= f 16 0 (16*4) -- note that the 4 comes from the line marked with (**)
= f 16 0 64 -- which is the base case for f
= 64
现在谈谈使用单字母函数名的问题——这是你必须习惯的一种方式,这是社区中大多数人的一种写作方式。它对编译器命名函数的方式没有影响——只要它们以小写字母开头 为了说明@dfeuer在说什么,请注意
f
的编写方式:
f
返回一个值f
使用新参数调用自身f
是尾部递归的,因此可以很容易地转换为循环
另一方面,考虑通过平方的交替实现幂:
-- assume n >= 0
exp x 0 = 1
exp x n | even n = exp (x*x) (n `quot` 2)
| otherwise = x * exp x (n-1)
这里的问题是,在otherwise子句中,执行的最后一个操作是乘法。因此exp
x
exp
不是尾部递归,因此无法转换为循环。正如其他人所指出的,为了提高效率,函数是使用尾部递归编写的
但是,请注意,可以删除最里面的where
,同时保留尾部递归,如下所示:而不是
x ^ n | n > 0 = f x (n-1) x
where f _ 0 y = y
f x n y = g x n
where g x n | even n = g (x*x) (n `quot` 2)
| otherwise = f x (n-1) (x*y)
我们可以使用
x ^ n | n > 0 = f x (n-1) x
where f _ 0 y = y
f x n y | even n = f (x*x) (n `quot` 2) y
| otherwise = f x (n-1) (x*y)
这也可以说更具可读性
然而,我不知道为什么前奏曲的作者选择了他们的变体。如果您能告诉我们如何通过平方例程来编写指数,也许会有助于指导我们的答案,然后我们可以解释为什么前奏曲版本是这样写的。@user5402来自维基百科的伪代码补充道,ghc中使用的求幂比前奏曲中的求幂更复杂。问题是,你不需要额外的乘法。你有一个多余的乘以1的乘法。“前奏曲一也是次优的。”奥古斯都看起来哈斯克尔的优化不是一件小事。与命令式优化非常不同。它在命令式设置中也非常重要。您的示例正是我编写它的方式(我会用backtics编写
pow
,但这是次要的)。请注意,还有一种将非尾部递归函数转换为尾部递归函数的标准方法,通过向函数添加附加参数。附加参数称为累加器
,因为值(某种程度上)累加到它中,以逐步构造最终结果。函数f
的参数y
正好扮演了这个角色。不幸的是,我没有很好的技术参考。@DominiqueDevriese但是额外的堆栈空间将是O(logn)
rigth?当您可以将其作为快速循环(即尾部递归)编写时,为什么要使用额外的堆栈?这可能不值得,但是对于一个库例程,我愿意花费一些额外的精力。你说f
和g
是corecorshive,也就是说,一个在需要时调用另一个,反之亦然?@Caridorc corecorshion。正如我提到的,这避免了在奇数情况下检查零。这是否真的是一个优势是另一个问题。前奏曲版本已被弃用。对于更复杂的问题:可以随意改进它,但它不能有额外的乘法。