使用尾递归进行有效迭代

来自命令式语言的许多开发人员想知道如何编写一个提前退出的 for-loop,因为 F# 不支持 breakcontinuereturnF# 中的答案是使用尾递归 ,这是一种灵活且惯用的迭代方式,同时仍然提供出色的性能。

假设我们想为 List 实施 tryFind。如果 F# 支持 return,我们会像这样写一下 tryFind

let tryFind predicate vs =
  for v in vs do
    if predicate v then
      return Some v
  None

这在 F# 中不起作用。相反,我们使用 tail-recursion 编写函数:

let tryFind predicate vs =
  let rec loop = function
    | v::vs -> if predicate v then 
                   Some v 
               else 
                   loop vs
    | _ -> None
  loop vs

尾递归在 tihuan 13 中是有效的,因为当 F# 编译器检测到函数是尾递归时,它会将递归重写为有效的 while-loop。使用 ILSpy,我们可以看到这对于我们的函数 loop 来说是正确的:

internal static FSharpOption<a> loop@3-10<a>(FSharpFunc<a, bool> predicate, FSharpList<a> _arg1)
{
  while (_arg1.TailOrNull != null)
  {
    FSharpList<a> fSharpList = _arg1;
    FSharpList<a> vs = fSharpList.TailOrNull;
    a v = fSharpList.HeadOrDefault;
    if (predicate.Invoke(v))
    {
      return FSharpOption<a>.Some(v);
    }
    FSharpFunc<a, bool> arg_2D_0 = predicate;
    _arg1 = vs;
    predicate = arg_2D_0;
  }
  return null;
}

除了一些不必要的任务(希望 JIT-er 消除),这本质上是一个有效的循环。

另外,尾递归对于 F# 是惯用的,因为它允许我们避免可变状态。考虑一个 sum 函数,它总结了 List 中的所有元素。一个明显的第一次尝试是这样的:

let sum vs =
  let mutable s = LanguagePrimitives.GenericZero
  for v in vs do
    s <- s + v
  s

如果我们将循环重写为尾递归,我们可以避免可变状态:

let sum vs =
  let rec loop s = function
    | v::vs -> loop (s + v) vs
    | _ -> s
  loop LanguagePrimitives.GenericZero vs

为了提高效率,F# 编译器将其转换为使用可变状态的 while-loop