懒惰的自然数 Lazy natural numbers
我们介绍了懒惰的自然数,并提供了一个应用示例,以使某种算法更快。
激励性的例子 Motivating example
如果列表 xs
很大,那么下面的程序就很慢。此外,如果列表 xs
是无限的,它将循环而不给出答案:
checkLengthBiggerThan :: [a] -> Int -> Bool
checkLengthBiggerThan xs n = length xs > n
Examples:
*Main> :set +s
*Main> checkLengthBiggerThan [1..10^6] 3
True
(0.04 secs, 72,067,432 bytes)
*Main> checkLengthBiggerThan [1..10^7] 3
True
(0.19 secs, 720,067,488 bytes)
*Main> checkLengthBiggerThan [1..10^8] 3
True
(1.47 secs, 7,200,067,600 bytes)
*Main> checkLengthBiggerThan [1..10^9] 3
True
(14.35 secs, 72,000,067,640 bytes)
我们可以通过以下方式使其更快,无论 xs
的长度如何,它最多需要 n
步。
checkLengthBiggerThan' :: [a] -> Int -> Bool
checkLengthBiggerThan' [] 0 = False
checkLengthBiggerThan' xs 0 = True
checkLengthBiggerThan' [] n = False
checkLengthBiggerThan' (x:xs) n = checkLengthBiggerThan' xs (n-1)
我们正在忽略负数。
Example:
*Main> checkLengthBiggerThan' [1..10^9] 3
True
(0.01 secs, 68,408 bytes)
懒惰的自然数 Lazy natural numbers
还有一种方法可以使上述算法变得快速,即用原算法中的懒惰自然数的类型 Nat
代替 Int
类型的使用。
data Nat = Zero | Succ Nat deriving (Eq,Ord)
我们的想法是,这代表自然数 0,1,2,3,...,如下所示
Zero
Succ Zero
Succ (Succ Zero)
Succ (Succ (Succ Zero))
...
我们可以在 Haskell 中定义为
one, two, three :: Nat
one = Succ Zero
two = Succ one
three = Succ two
此外,我们可以将一个非负整数转换成一个懒惰的自然数,如下所示:
toNat :: Int -> Nat
toNat 0 = Zero
toNat n = Succ (toNat (n-1))
但我们在 Nat
类型中也有无限大:
infty = Succ infty
这将永远计算下去,产生无限多的 Succ (Succ (Succ ...
的继承构造函数,但关键是,这种计算是懒惰的。
我们现在可以定义一个长度函数,如下所示:
length' :: [a] -> Nat
length' [] = Zero
length' (x:xs) = Succ (length' xs)
For example, we have that
length [0..]
loops without giving any answer, butlength' [0..] = infty
.
现在定义一个懒惰的比较算法,如下所示:
biggerThan :: Nat -> Nat -> Bool
Zero `biggerThan` y = False
(Succ x) `biggerThan` Zero = True
(Succ x) `biggerThan` (Succ y) = x `biggerThan` y
关键是在第二个方程中,x
不需要被评估就能得到答案 True
。比如说:
*Main> infty `biggerThan` three
True
这样一来,第一种算法就变得很快,而且也适用于无限的列表:
checkLengthBiggerThan'' :: [a] -> Int -> Bool
checkLengthBiggerThan'' xs n = (length' xs) `biggerThan` (toNat n)
For example:
*Main> checkLengthBiggerThan'' [1..10^9] 3
True
(0.02 secs, 69,032 bytes)
但实际上我们可以这样写,因为我们导出了Ord
:
checkLengthBiggerThan''' :: [a] -> Int -> Bool
checkLengthBiggerThan''' xs n = length' xs > toNat n
而这也同样快,这意味着 Ord
的推导机制必须产生一个与我们类似的懒惰比较算法:
*Main> checkLengthBiggerThan''' [1..10^9] 3
True
(0.01 secs, 70,200 bytes)
问题是,使用懒惰的自然数使得自然算法变得快速,而对于整数来说,自然算法是缓慢的。此外,使用懒惰自然数的一个好处是,它使无限列表的长度得到很好的定义,因为有懒惰自然数 infty
可用。