【问题标题】:F# tail recursive callF# 尾递归调用
【发布时间】:2013-08-27 19:54:49
【问题描述】:

我有这个代码:

let rec collect ( t : BCFile list ) ( acc : Set<BCFile> ) : Set<BCFile> =
    match t with
    | [] -> acc
    | hr::tl -> collect ( tl ) ( Set.union acc ( FindSourceFilesForTarget ( hr ) ) )
let s = collect (Set.toList targets) Set.empty

看起来它应该是尾递归的,但它不是(查看 IL)。知道为什么它没有被编译为使用尾递归吗?

【问题讨论】:

  • 你是在发布模式下编译吗?除非您处于发布模式,否则不会优化尾调用。

标签: .net f# tail-recursion tail cil


【解决方案1】:

据我所知,collect 函数实际上是尾递归的。第一种情况显然只返回acc。第二种情况首先调用FindSourceFilesForTarget,然后调用Set.union,然后返回。您可以将其重写如下(更清楚地显示尾递归):

| hr::tl -> 
    let sources = FindSourceFilesForTarget hr
    let acc = Set.union acc sources
    collect tl

因为这只是一个调用自身的单个函数,所以编译器将其优化为一个循环。这是编译后的代码的样子(当你使用反射器将其转换为 C# 时):

public static FSharpSet<int> collect(FSharpList<int> t, FSharpSet<int> acc) {
  while (true) {
    FSharpList<int> fSharpList = t;
    if (fSharpList.TailOrNull == null) break;
    // The following corresponds to the second case 
    FSharpList<int> tl = fSharpList.TailOrNull;
    int hr = fSharpList.HeadOrDefault;
    // Variables 'acc' and 't' are mutated (instead of calling the function)
    acc = SetModule.Union<int>(acc, Program.FindSourceFilesForTarget<int>(hr));
    t = tl;
  }
  return acc;
}

顺便说一句,您也可以使用标准库函数来表达这一点:

t |> Seq.map FindSourceFilesForTarget |> Set.unionMany

【讨论】:

  • 感谢您的回答。作为一个附带问题,如果使用unionMany,管道是否会在它可用时立即开始合并集,或者等到它收集上一个管道步骤的所有输出(在这种情况下为“Seq.map FindSourceFilesForTarget”)?我进行递归调用的原因是在集合变得可用时进行联合,因为它们有很多相同的数据并且有很多迭代(数千个),所以我不想缓存所有结果并想丢弃重复项尽快
  • t 是惰性数据源 (IEnumerable) 时,unionMany 操作应该按需读取它们(因此 FindSourceFilesForTarget 也将按需评估)。所以我认为在这种情况下,整个数据集不会一路加载到内存中。
猜你喜欢
  • 2016-03-21
  • 1970-01-01
  • 1970-01-01
  • 2011-09-13
  • 2011-03-15
  • 1970-01-01
  • 1970-01-01
  • 2015-08-10
  • 1970-01-01
相关资源
最近更新 更多