在什么情况下是一元计算尾递归?(Under what circumstances are monad

2019-08-02 18:15发布

在Haskell维基的递归在单子存在被声称是一个例子尾递归 :

f 0 acc = return (reverse acc)
f n acc = do
    v  <- getLine
    f (n-1) (v : acc)

虽然势在必行符号使我们相信它是尾递归,它不是那么明显,在所有(至少对我来说)。 如果我们去糖do我们得到

f 0 acc = return (reverse acc)
f n acc = getLine >>= \v -> f (n-1) (v : acc)

和重写第二行导致

f n acc = (>>=) getLine (\v -> f (n-1) (v : acc))

所以我们看到, f的第二个参数内发生>>= ,而不是在尾递归的位置。 我们就需要检查IO>>=得到答案。 显然具有递归调用如在最后一行do块不是充分条件的函数是尾递归。


比方说,一个单子就是尾递归当且仅当在这个单子每一个递归函数定义为

f = do
    ...
    f ...

或等价

f ...  =  (...) >>= \x -> f ...

是尾递归。 我的问题是:

  1. 什么单子是尾递归?
  2. 有没有我们可以用它来区分立即尾递归单子一些一般性的规则?

更新:让我做一个具体的反例:在[]单子不是根据上述定义尾递归。 如果是这样,那么

f 0 acc = acc
f n acc = do
    r <- acc
    f (n - 1) (map (r +) acc)

必须是尾递归。 然而,脱糖第二行导致

f n acc = acc >>= \r -> f (n - 1) (map (r +) acc)
        = (flip concatMap) acc (\r -> f (n - 1) (map (r +) acc))

很显然,这并不是尾递归,恕我直言无法进行。 其原因是递归调用不是计算结束。 这是多次执行和结果相结合,使最终结果。

Answer 1:

这是指本身就是一个一元计算是从来没有尾递归。 然而,在Haskell你的懒惰和corecursion,这是最重要的。 让我们用这个简单的例子:

forever :: (Monad m) => m a -> m b
forever c' = let c = c' >> c in c

这样的运算在恒定空间中运行,当且仅当(>>)是不严格的在其第二个参数。 这真的是很相似,列表和repeat

repeat :: a -> [a]
repeat x = let xs = x : xs in xs

由于(:)构造是非严格在其第二个参数这部作品而列表可以被遍历,因为你有一个有限的弱头部正常形态(WHNF)。 只要消费者(例如列表倍)永远只要求提供WHNF这部作品,并在不断的空间中运行。

在的情况下,消费者forever是任何解释一元计算。 如果单子是[]然后(>>)是不严格的在其第二个参数,当它的第一个参数是空列表。 所以forever []将导致[]forever [1]是发散的。 在的情况下, IO单子的解释是很运行时系统本身,而且有你能想到的(>>)是永远不严格在其第二个参数。



Answer 2:

真正重要的是恒定的栈空间。 你的第一个例子是尾递归模利弊 ,由于懒惰。

(getLine >>=)将被执行,会蒸发掉,再留给我们调用f 。 重要的是,这种情况发生在一个恒定的步数 - 有没有thunk的积聚。

你的第二个例子,

f 0 acc = acc
f n acc = concat [ f (n - 1) $ map (r +) acc | r <- acc]

将只有线性的(在n在其形实转换积聚),作为结果列表是从左侧访问(再次由于懒惰,如concat是不严格的)。 如果在它可以在O(1)空间中运行的头消耗(不算线性空间形实转换, f(0), f(1), ..., f(n-1)在左边缘)。

更糟糕的是

f n acc = concat [ f (n-1) $ map (r +) $ f (n-1) acc | r <- acc]

do -notation,

f n acc = do
  r <- acc
  f (n-1) $ map (r+) $ f (n-1) acc

因为有多余的,由于信息的依赖强迫。 同样,如果对于一个给定单子的绑定是一个严格的操作。



文章来源: Under what circumstances are monadic computations tail-recursive?