在什么情况下monadic计算尾递归?

时间:2012-11-14 12:44:45

标签: haskell monads tail-recursion

在Haskell Wiki的Recursion in a monad中,有一个声称是tail-recursive的例子:

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的{​​{1}}以获得答案。 显然将递归调用作为>>=块中的最后一行,这不是一个尾递归函数的充分条件。


假设 monad是尾递归 iff此monad中的每个递归函数定义为

do

或等效

f = do
    ...
    f ...

是尾递归的。我的问题是:

  1. 哪些monad是尾递归的?
  2. 我们可以使用一些通用规则来立即区分尾递归monad吗?

  3. 更新:让我举一个具体的反例:根据上面的定义,f ... = (...) >>= \x -> f ... monad不是尾递归的。如果是,那么

    []

    必须是尾递归的。然而,贬低第二行导致

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

    显然,这不是尾递归,并且无法制作恕我直言。原因是递归调用不是计算的结束。它被执行几次,结果结合起来得到最终结果。

2 个答案:

答案 0 :(得分:22)

引用自身的monadic计算从不是尾递归的。然而,在Haskell中你有懒惰和核心运动,这才是最重要的。让我们使用这个简单的例子:

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的情况下,消费者可以解释monadic计算。如果monad是[],那么(>>)在其第二个参数中是非严格的,当它的第一个参数是空列表时。因此forever []会产生[],而forever [1]会产生分歧。在IO monad的情况下,解释器本身就是运行时系统,在那里你可以认为(>>)在其第二个参数中始终是非严格的。

答案 1 :(得分:4)

真正重要的是持续的堆栈空间。由于懒惰,你的第一个例子是tail recursive modulo cons

(getLine >>=)将被执行并将消失,让我们再次打电话给f。重要的是,这是以一定数量的步骤发生的 - 没有任何重击。

你的第二个例子,

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

在thunk构建中只是线性的(在n中),因为从左边访问结果列表(再次由于懒惰,因为concat是非严格的)。如果它在头部消耗,它可以在O(1)空间中运行(不计算线性空间thunk,左边缘f(0), f(1), ..., f(n-1))。

更糟糕的是

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

do - 符号,

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

因为信息依赖性导致额外的强制。同样,如果给定monad的绑定是严格操作。