在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 ...
是尾递归的。我的问题是:
更新:让我举一个具体的反例:根据上面的定义,f ... = (...) >>= \x -> f ...
monad不是尾递归的。如果是,那么
[]
必须是尾递归的。然而,贬低第二行导致
f 0 acc = acc
f n acc = do
r <- acc
f (n - 1) (map (r +) acc)
显然,这不是尾递归,并且无法制作恕我直言。原因是递归调用不是计算的结束。它被执行几次,结果结合起来得到最终结果。
答案 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的绑定是严格操作。