如果我写这样的递归因子:
fact 0 = 1
fact n = n * (fact (n-1))
ghci告诉我它的类型为(Eq p, Num p) => p -> p
。
我希望Haskell能够变得聪明,并使这个阶乘实现变得更快。所以,如果我写:fact (10 :: Int)
所有这些都是Int
s的快速数学运算,如果我写fact (1000 :: Integer)
,所有或部分计算将是{ {1}}秒。
ghci在这里很有用:
Integer
我有两个问题:
Q1:当我跑
时会发生什么λ: :t fact (2 :: Int)
fact (2 :: Int) :: Int
λ: :t fact (2 :: Integer)
fact (2 :: Integer) :: Integer
显然Haskell无法为更大的参数计算正确的结果,因为它会溢出。所以我对λ: fact (10 :: Int)
>3628800
λ: fact (40 :: Int)
>-7060926325325235253252...
λ: fact (66 :: Int)
>0
的结果并不感到惊讶,我得到的结果与fact 40
相似。但是从fact 65
开始,函数总是返回0.为什么呢?
fact 66
调用的情况下,所有对fact的递归调用都使用相同的类型是正确的,即使在某些调用中Int可以替换为Integer(即整个计算)如果编译器能够在每次调用的基础上在运行时决定Int和Integer之间的速度,那么它会慢得多。
答案 0 :(得分:9)
Int
是64位整数(至少在您的平台上),因此您看到的结果是模2 ^ 64(然后解释为有符号)。 fact 66
是恰好是2^64
的倍数的第一个因子,因此fact 66 `mod` 2^64
为0.因为每个阶乘都是前一个阶乘的倍数,所有更大的阶乘也是{的倍数{1}}。
鉴于对事实的调用,对事实的所有递归调用都将使用相同的类型
是否正确
是的,2^64
的类型是*
,因此Num a => a -> a -> a
的两个操作数必须具有相同的类型,这也是其结果的类型。因此,在*
中,n * fact (n-1)
必须与fact (n-1)
具有相同的类型(和n
,其类型与n-1
相同,因为n
与-
)具有相同的类型。
答案 1 :(得分:6)
我得到的结果与
fact 65
相似。但从fact 66
开始,该函数始终返回0
。怎么样?
如果给定数字的fact
产生0
,那么对于索引较大的其他fact
,结果也会0
,因为{{ 1}}仍为n * 0
。像0
这样的类型具有固定的位数(例如Int
的16位)。如果我们将两个较大的数字相乘,产生一个超过16位的数字,则忽略较高的位。所以例如:
Int16
此处没有值移出 fact 7 | 5040 | 0001 0011 1011 0000
x 8 | x 8 | x 1000
------------------------------------------
40 320 | 40 320 | 1001 1101 1000 0000
------------------------------------------
-25 216 | -25 216 | 1001 1101 1000 0000
,但由于签名解释,我们得到负值。稍后我们乘以Int16
,然后得到:
9
因此CPU将在更大的寄存器(例如32位)中执行计算,并取得该结果的最低16位。由于每次乘以偶数,都会将值向左移动至少一个位置,因此设置位最终将从具有固定位数的整数表示中移出(尽管如果我们有更多位,则此过程将为当然需要更长的时间)。
对于 n 位的数字,我们在 2×n 步骤中达到零(因为每次索引为偶数时,我们将其移位 at至少向右一个位置。)
Q2:鉴于对事实的调用,所有对事实的递归调用都将使用相同的类型是正确的,即使在某些调用中Int可能已被替换为Integer
类型不在运行时决定(所以“每次调用”),但是在编译时。 Haskell首先假设它具有 fact 8 | -25 216 | 1111 1111 1111 1111 1001 1101 1000 0000
x 9 | x 9 | x 1001
---------------------------------------------------------------
40 320 | -226 944 | 1111 1111 1111 1100 1000 1001 1000 0000
---------------------------------------------------------------
-30 336 | -30 336 | 1000 1001 1000 0000
类型来分析该函数,其中a -> b
和a
可以是任何类型。
接下来做一些分析:
b
表示输入类型fact 0 = 1
和输出类型a
必须属于b
类型类。此外,我们执行隐式等式检查(好的Haskell将在幕后执行Num
)。所以现在我们知道类型是:
(0 ==)
现在我们可以分析递归调用:
fact :: (Num a, Num b, Eq a) => a -> b
这相当于:
fact n = n * fact (n-1)
Haskell 不假设此递归-- equivalent to
fact n = (*) n (fact (n-1))
调用具有相同的类型。因此,递归调用具有类型fact
。
然而,我们可以分析函数(*) Num e => e -> e -> e
。注意,在我们执行乘法时,在Haskell中,两个操作数和结果都具有相同的类型。由于第一个操作数是(Num c, Num d, Eq c) => c -> d
,我们知道n
和e ~ a
(递归调用的结果类型)和e ~ d
(我们的外部{{1的结果) }})。所以我们知道e ~ b
。这意味着fact
和e ~ a ~ b ~ d
属于同一类型,因此a
具有类型:
b
因此我们知道递归调用fact
。因此递归调用也具有类型fact :: (Num a, Eq a) => a -> a
(递归函数调用)。确实,这里的递归调用使用相同的类型。
答案 2 :(得分:2)
回答问题2,此类型只有一种检查方式:
fact n = n * fact (n-1)
在fact
处专攻Int
,递归调用也必须专注于Int
。
(fact :: Int -> Int) n = n * (fact :: Int -> Int) (n-1)
在其他情况下,递归调用可能会默认为Integer
,但如果您始终使用(至少)-Wall
进行编译,则默认会触发警告。