【问题标题】:Tail-Recursive Foldr尾递归文件夹
【发布时间】:2019-07-09 16:48:11
【问题描述】:

我想在 f# 中编写一个尾递归折叠器,以利用尾递归优化并了解有关函数式编程的更多信息。

我写了一个尾递归的 foldl 和一个非尾递归的 foldr。我想我可以通过反转提供给函数的列表来获得一个尾递归折叠器,然后在其上调用我的尾递归折叠器,但这不起作用,因为函数应该应用到的顺序不同。

let rec tail_recurse_foldl(list: List<'a>, func:('b -> 'a -> 'b), acc: 'b) =
    match list with 
    | [] -> acc
    | [x] -> func acc x
    | x :: xs -> tail_recurse_foldl(xs, func, func acc x)

和非尾递归折叠器

let rec foldr(list: List<'a>, func:('a -> 'b -> 'b), acc: 'b) =
    match list with
    | [] -> acc
    | [x] -> func x acc
    | x::xs -> func x (foldr(xs, func, acc))

【问题讨论】:

  • “这不起作用,因为函数应该应用到的顺序不同。”。你真的检查过这是否属实吗?
  • @sylwester 我没有很好地解释这一点。我使用 Haskell foldr 和 foldl 作为参考。它们具有以下类型签名: foldl :: (a -> b -> a) -> a -> [b] -> a foldr :: (a -> b -> b) -> b -> [a] - > b 编码这些,你不能只是颠倒列表。函数 foldr 和 foldl take 有不同的签名。 foldl 首先取累加器, foldr 其次取累加器。但是,进一步考虑,这里有两个警告 1) 可能有一些方法可以翻转 foldr 函数的签名以匹配 foldl。 2) 除了约定之外,没有什么能让我坚持那个签名。
  • 好的。知道 foldlfoldr 比 Haskell 更老。在 Lisp,尤其是 Scheme 中,R6RS 跟随累加器作为 foldr 的最后一个参数出现了一些翻转,但早期和较旧版本具有相同的签名,尽管我认为这是因为这些不是像 Haskell 这样的咖喱懒惰语言。

标签: functional-programming f#


【解决方案1】:

有两种方法可以做到这一点。一个更简单的方法是反转列表(这是一个尾递归操作),然后从左侧运行 fold。一种更复杂的方法是使用延续传递风格。

在基于延续的版本中,您添加了一个额外的参数,continuation,这是一个在列表处理完成后应调用的函数(以便您可以将 func 调用在这个延续中,而不是把它放在堆栈上)。

let rec foldr(list: List<'a>, func:('a -> 'b -> 'b), acc: 'b, cont:'b -> 'b) =
    match list with
    | [] -> cont acc
    | x::xs -> foldr(xs, func, acc, fun r -> cont (func x r))

当我们得到一个空列表时,我们只需调用初始状态为acc 的延续。当您有非空列表时,我们会递归调用 foldr(尾)并给它一个延续,它将在结果上运行 func,然后使用它自己的 cont 调用报告最终聚合值。

【讨论】:

  • 您是否同意正确折叠仅在惰性求值语言或使用尾递归模 cons 的急切语言中有用?
猜你喜欢
  • 2016-03-21
  • 2018-03-17
  • 1970-01-01
  • 2020-04-07
  • 1970-01-01
  • 2017-11-11
  • 1970-01-01
  • 1970-01-01
  • 2014-06-21
相关资源
最近更新 更多