当前位置: 首页 > 知识库问答 >
问题:

将“几乎尾部位置”中的递归调用移动到真正尾部位置

宋子辰
2023-03-14

在阅读Guido关于不在Python中添加尾部递归消除的推理时,我在Haskell中编造了这个几乎尾部递归的示例:

triangle :: Int -> Int
triangle 0 = 0
triangle x = x + triangle (x - 1)

这当然不是尾部调用,因为尽管递归调用本身在“return”中,但是x阻止了当前堆栈被重用为递归调用。

但是,可以将其转换为尾递归代码(尽管相当丑陋和冗长):

triangle' :: Int -> Int
triangle' = innerTriangle 0
    where innerTriangle acc 0 = acc
          innerTriangle acc x = innerTriangle (acc + x) (x - 1)

这里innerTriangle是尾递归的,由triangle启动。虽然微不足道,但这样的转换似乎也适用于其他任务,例如构建列表(这里acc可能只是正在构建的列表)。

当然,如果递归调用不在函数return中,这似乎是不可能的:

someRecusiveAction :: Int -> Bool
someRecursiveAction x = case (someRecursiveAction (x * 2)) of
    True -> someAction x
    False -> someOtherAction x

但我只是指几乎尾部调用,其中递归调用是返回值的一部分,但由于另一个函数应用程序包装它而不在尾部位置(例如上面三角形示例中的x)。

这在功能上下文中可以概括吗?那么命令式的呢?所有返回中包含递归调用的函数能否转换为返回位于尾部位置的函数(即可以进行尾部调用优化的函数)?

没关系,这些都不是在Haskell中计算三角形数的“最佳”方法,AFAIK是三角形x=sum[0... n]。该代码纯粹是这个问题的一个人为示例。

注意:我已经阅读了是否有不能使用尾递归编写的问题?,所以我相当有信心我的问题的答案是肯定的。然而,答案提到了延续传递风格。除非我误解了CPS,否则我的转换后的三角形'似乎仍然是直接风格。在这种情况下,我很好奇如何使这种转换在直接风格中推广。

共有1个答案

华森
2023-03-14

有一个有趣的尾部递归模算子优化空间,可以转换一些函数,使其在恒定空间中运行。最著名的可能是尾递归模cons,其中不完全的尾调用是构造函数应用程序的参数。(这是一个古老的优化,可以追溯到Prolog编译器的早期——我认为Warren Abstract Machine fame的David Warren是第一个发现它的人)。

但是,请注意,这种优化不太适合懒惰的语言。像Haskell这样的语言有非常不同的评估模型,其中尾部调用并不那么重要。在Haskell中,可能需要一个包含递归调用的构造函数应用程序,因为它将阻止对递归调用的立即求值,并允许惰性地使用计算。请参阅Haskell Wiki页面上的讨论。

下面是一个用严格语言进行模约束优化的示例:

let rec map f = function
  | [] -> []
  | x::xs -> f x::map f xs

在这个OCaml函数中,对映射的递归调用是位于尾部的构造函数应用程序的参数,因此可以应用模约束优化。(OCaml尚未进行此优化,尽管有一些实验补丁正在四处浮动。)

转换后的函数可能类似于以下psuedo-OCaml。注意,内部循环是尾部递归的,通过改变之前的cons来工作:

let rec map f = function
  | [] ->
  | x::xs ->
    let rec loop cons = function
      | [] -> cons.[1] <- []
      | y::ys ->
        let new_cons = f y::NULL in
        cons.[1] <- new_cons;
        loop new_cons ys in
    loop (f x::NULL) xs

(其中NULL是GC不会阻塞的某个内部值。)

尾考虑在Lisp中也很常见,通过一种不同的机制:在那里,突变倾向于显式编程,混乱的细节隐藏在宏中,例如循环。

如何推广这些方法是一个有趣的问题。

 类似资料:
  • 我想知道是否有一些通用方法可以用foo(…)转换“正常”递归foo(…) 作为尾部递归的最后一个调用。 例如(scala): 函数语言将递归函数转换为等价尾部调用的一般解决方案: 一种简单的方法是将非尾部递归函数包装在蹦床单子中。 所以pascal函数不再是递归函数。然而,蹦床单子是需要完成的计算的嵌套结构。最后,是一个尾递归函数,它遍历树状结构,解释它,最后在基本情况下返回值。 Rúnar Bj

  • 我只是想知道这样的函数是否可以递归地完成。我觉得很难,因为它自己叫了两次。 这是我在javascript中的非尾部递归实现。(是的,我知道大多数javascript引擎不支持TCO,但这只是理论上的。)目标是找到给定数组(arr)中具有特定长度(大小)的所有子列表。例如:getSublistsWithFixedSize([1,2,3],2)返回[[1,2]、[1,3]、[2,3]]

  • 我有一个关于如何将“递归”转换为“尾部递归”的问题。 这不是家庭作业,只是在我试图润色算法书籍中的递归定理时出现的一个问题。 我熟悉使用递归的两个典型示例(阶乘和斐波那契序列),也知道如何以递归方式和尾部递归方式实现它们。 我的代码如下(我使用Perl只是为了使其简单,但可以轻松地转换为C/Java/C)。 运行代码时,输出如下: 递归函数在返回之前使用不同的参数调用自己两次。我尝试了几种方法将其

  • 我试图了解如何将各种递归函数转换为尾递归。我已经查看了许多将斐波那契和阶乘转换为尾递归的示例,并理解了这些示例,但很难跳到具有某种不同结构的问题。一个例子是: 如何将其转换为尾部递归实现? 我已经看过类似的问题,例如:将正常递归转换为尾部递归,但这些似乎并没有转化为这个问题。

  • 我想我理解了教科书中对尾部递归函数的定义:在函数调用后不执行任何计算的函数。我还发现,作为一个结果,尾部递归函数的内存效率会更高,因为它每次调用只需要一条记录,而不是每次都需要保留一条记录(就像在普通递归中那样)。 我不太清楚的是,这个定义如何应用于嵌套调用。我将提供一个例子: 我最初给出的答案是,根据定义,它不是尾部递归的(因为外部调用是在计算内部调用之后执行的,所以其他计算是在第一次调用之后完

  • 问题内容: 我需要获取一个字符串,并通过获取char来递归地重新排列它,并按该char的形式将字符串上的char移到末尾,例如“ Hello world!”。,’l’=>“ Heo word!lll”我在理解递归思维方式时遇到了问题,所以我从这里开始: 谢谢您的帮助 :) 问题答案: 递归是在内部重用方法的实践。在这种情况下,我将提供一个解决方案来解释发生的情况: 如果执行: 这将产生所需的结果: