【问题标题】:Does F# do TCO (tail call optimization) with |> Option.bindF# 是否使用 |> Option.bind 进行 TCO(尾调用优化)
【发布时间】:2016-01-26 20:57:50
【问题描述】:

这是我的功能:

let rec applyAll rules expr =
  rules
  |> List.fold (fun state rule ->
    match state with
    | Some e ->
      match applyRule rule e with
      | Some newE -> Some newE
      | None -> Some e
    | None -> applyRule rule expr) None
  |> Option.bind (applyAll rules)

它需要一组规则并应用它们,直到输入表达式尽可能地减少。我可以将Option.bind 重写为match 表达式,它显然会利用尾调用优化。但是,这对我来说更优雅,所以我想保持原样,除非它会不必要地消耗堆栈。 F# 是否使用此代码实现 TCO?

编辑:此代码始终返回 None;我会解决这个问题,但我认为这个问题仍然有意义。

【问题讨论】:

  • 您可以查看 IL 并查看生成了什么?我看到了tail. :)。
  • 我冒昧地稍微重新格式化了您的代码。将|> 运算符放在您“插入”的函数之前之外的任何其他位置,这绝对是让 99.5% 的 F# 程序员望而却步的方法。 ;-)
  • 有关(|>) 的尾调用的更多信息,请参见stackoverflow.com/a/40165030/82959 - 您的结果可能因调试和发布模式而异。

标签: f# tail-recursion


【解决方案1】:

我将您的代码粘贴到文件 tco.fs 中。我添加了一个 applyRule 函数以使其可编译。

tco.fs

let applyRule rule exp =
    Some ""

let rec applyAll rules expr =
  rules
  |> List.fold (fun state rule ->
    match state with
    | Some e ->
      match applyRule rule e with
      | Some newE -> Some newE
      | None -> Some e
    | None -> applyRule rule expr) None
  |> Option.bind (applyAll rules)

然后我做了一个批处理文件来分析IL。

compile_and_dasm.bat

SET ILDASM="C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6 Tools\ildasm.exe"

Fsc tco.fs

%ILDASM% /out=tco.il /NOBAR /tok tco.exe

作为输出,我们找到包含 IL 的 tco.il。相关功能在这里。

.method /*06000002*/ public static class [FSharp.Core/*23000002*/]Microsoft.FSharp.Core.FSharpOption`1/*01000003*/<!!b> 
      applyAll<a,b>(class [FSharp.Core/*23000002*/]Microsoft.FSharp.Collections.FSharpList`1/*01000008*/<!!a> rules,
                    string expr) cil managed
{
    .custom /*0C000003:0A000003*/ instance void [FSharp.Core/*23000002*/]Microsoft.FSharp.Core.CompilationArgumentCountsAttribute/*01000007*/::.ctor(int32[]) /* 0A000003 */ = ( 01 00 02 00 00 00 01 00 00 00 01 00 00 00 00 00 ) 
    // Code size       26 (0x1a)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  newobj     instance void class Tco/*02000002*//applyAll@13/*02000003*/<!!b,!!a>/*1B000004*/::.ctor(class [FSharp.Core/*23000002*/]Microsoft.FSharp.Collections.FSharpList`1/*01000008*/<!1>) /* 0A000004 */
    IL_0006:  newobj     instance void class Tco/*02000002*//'applyAll@6-1'/*02000004*/<!!a>/*1B000005*/::.ctor() /* 0A000005 */
    IL_000b:  ldnull
    IL_000c:  ldarg.0
    IL_000d:  call       !!1 [FSharp.Core/*23000002*/]Microsoft.FSharp.Collections.ListModule/*01000009*/::Fold<!!0,class [FSharp.Core/*23000002*/]Microsoft.FSharp.Core.FSharpOption`1/*01000003*/<string>>(class [FSharp.Core/*23000002*/]Microsoft.FSharp.Core.FSharpFunc`2/*01000002*/<!!1,class [FSharp.Core/*23000002*/]Microsoft.FSharp.Core.FSharpFunc`2/*01000002*/<!!0,!!1>>,
                                                                                                                                                                                                             !!1,
                                                                                                                                                                                                             class [FSharp.Core/*23000002*/]Microsoft.FSharp.Collections.FSharpList`1/*01000008*/<!!0>) /* 2B000001 */
    IL_0012:  tail.
    IL_0014:  call       class [FSharp.Core/*23000002*/]Microsoft.FSharp.Core.FSharpOption`1/*01000003*/<!!1> [FSharp.Core/*23000002*/]Microsoft.FSharp.Core.OptionModule/*0100000A*/::Bind<string,!!1>(class [FSharp.Core/*23000002*/]Microsoft.FSharp.Core.FSharpFunc`2/*01000002*/<!!0,class [FSharp.Core/*23000002*/]Microsoft.FSharp.Core.FSharpOption`1/*01000003*/<!!1>>,
                                                                                                                                                                                                        class [FSharp.Core/*23000002*/]Microsoft.FSharp.Core.FSharpOption`1/*01000003*/<!!0>) /* 2B000002 */
    IL_0019:  ret
} // end of method Tco::applyAll

我们看到这里生成了尾部操作码。这是从 IL 编译器到 JIT 编译器(实际生成可执行机器代码)的提示,这里应该可以进行尾调用。

是否实际执行尾调用取决于 JIT 编译器,可以阅读 here

【讨论】:

    【解决方案2】:

    答案是“不”。

    正如你所说,调用将通过“扩展”Option.Bindmatch 表达式来优化。这样做会将对 applyAll 的递归调用正确地置于尾部位置。

    Option.Bind 在尾部位置,堆栈会像这样增长

    + …
    + applyAll
    + Option.Bind
    + applyAll
    + Option.Bind
    _ applyAll
    

    F# 编译器不会对此进行优化。

    【讨论】:

      猜你喜欢
      • 2011-04-09
      • 2021-05-27
      • 1970-01-01
      • 2021-05-15
      • 1970-01-01
      • 2020-10-21
      • 2010-10-23
      • 2012-04-18
      相关资源
      最近更新 更多