F# 中更安全的递归

作者:David Schaefer
排版:Alan Wang

这是 David Schaefer 的客座博客文章。David 是一名专注于函数式编程的自由软件开发人员。他是 G-Research 开源团队的一员。他致力于改进 F# 开发者工具的生态系统。此外,他还帮助维护各种开源的 F# 项目。

在函数式编程中,用递归的方式去定义算法是很常见的场景。这非常符合我们想要避免突变的心态,而且这通常不会导致性能下降。编译器在优化阶段会尝试将递归定义重写为更高效的循环。

然而,编译器并不总是能够将递归转换为循环。从这里开始,就有一定的危险了。

堆栈帧与生产环境

当我们在函数 f 内部调用函数 g 时,这个操作通常会在进程的调用堆栈上创建一个新的堆栈帧。函数 g 完成后,程序此时就不再需要它的堆栈帧了,从而可以重用它的空间。

现在,理解堆栈帧的创建对于递归函数是至关重要的。一般来说,对于每个递归调用,都会创建一个新的堆栈帧。在开发期间,当规模较小时,这通常不会引起问题。但在后期的生产环境中,随着规模的扩大,程序会因堆栈溢出而突然崩溃。

在生产环境中,由于递归调用创建了太多堆栈帧,堆栈的所有空间都被用完了,所以 runtime 决定停止它。为了演示这一场景,请看一下这个递归函数:

let rec countDown1 n =if n = 0then 0elsecountDown1 (n - 1) + 1

生成的 IL 代码如下所示:

.method public static int32 countDown1 (int32 n
) cil managed 
{// Method begins at RVA 0x2050// Header size: 1// Code size: 17 (0x11).maxstack 8IL_0000: nopIL_0001: ldarg.0IL_0002: brtrue.s IL_0006IL_0004: ldc.i4.0IL_0005: retIL_0006: ldarg.0IL_0007: ldc.i4.1IL_0008: subIL_0009: call int32 Program::countDown1(int32)IL_000e: ldc.i4.1IL_000f: addIL_0010: ret
} // end of method Program::countDown1

您可以在 IL_0009 处看到递归调用。当使用 n = 1_000 来调用 coundDown1时(就像在开发过程中随意写的数值)代码会正确的结束调用,但使用 n = 1_000_000 来调用它时,它会因堆栈溢出而崩溃。

将上面的内容与以下内容进行比较:

let rec countDown2 n =if n = 0then 0elsecountDown2 (n - 1)

结果是:

.method public static int32 countDown2 (int32 n
) cil managed 
{// Method begins at RVA 0x2064// Header size: 1// Code size: 13 (0xd).maxstack 8// loop startIL_0000: nopIL_0001: ldarg.0IL_0002: brtrue.s IL_0006IL_0004: ldc.i4.0IL_0005: retIL_0006: ldarg.0IL_0007: ldc.i4.1IL_0008: subIL_0009: starg.s nIL_000b: br.s IL_0000// end loop
} // end of method Program::countDown2

两段代码的不同之处在于,在 countDown1 中,递归调用的返回值还需要通过加 1 来输出函数的最终值。相反,在 countDown2 中,递归调用的返回值也是函数本身的值。这意味着,递归调用是函数定义中的最后一条指令——这种方式称为尾递归。这种风格允许编译器将函数转换为循环,从而无需创建新的堆栈帧。

除了编译器有机会将递归函数重写为循环之外,使用尾递归还将打开另一个逃生门:IL 前缀 tail。在 ECMA-335 中,是这样解释的:

它表示不再需要当前方法的堆栈帧,因此可以在执行调用指令之前将其删除。由于调用返回的值将是此方法返回的值,因此可以将调用转换为跨方法跳转。

为了演示它,我们使用 RFC-1011 中的示例,稍后会详细介绍。考虑相互递归的 F# 函数:

let foo x =printfn "Foo: %x" x[<TailCall>]
let rec bar x =match x with| 0 ->foo x           // OK: non-tail-recursive call to a function which doesn't share the current stack frame (i.e., 'bar' or 'baz').printfn "Zero"| 1 ->bar (x - 1)     // Warning: this call is not tail-recursiveprintfn "Uno"baz x           // OK: tail-recursive call.| x ->printfn "0x%08x" xbar (x - 1)     // OK: tail-recursive call.and [<TailCall>] baz x =printfn "Baz!"bar (x - 1)         // OK: tail-recursive call.

这里我们得到 bar 中案例 1 的以下 IL 代码:

  ...IL_0030: ldarg.0IL_0031: ldc.i4.1IL_0032: subIL_0033: call void Program::bar(int32)IL_0038: nopIL_0039: ldstr "Uno"IL_003e: newobj instance void class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`5<class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [System.Runtime]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>::.ctor(string)IL_0043: stloc.0IL_0044: call class [netstandard]System.IO.TextWriter [netstandard]System.Console::get_Out()IL_0049: ldloc.0IL_004a: call !!0 [FSharp.Core]Microsoft.FSharp.Core.PrintfModule::PrintFormatLineToTextWriter<class [FSharp.Core]Microsoft.FSharp.Core.Unit>(class [System.Runtime]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.PrintfFormat`4<!!0, class [System.Runtime]System.IO.TextWriter, class [FSharp.Core]Microsoft.FSharp.Core.Unit, class [FSharp.Core]Microsoft.FSharp.Core.Unit>)IL_004f: popIL_0050: ldarg.0IL_0051: tail.IL_0053: call void Program::baz(int32)IL_0058: ret...

因为对 baz 的调用以尾递归方式进行,所以编译器可以在 IL_0051 中使用 tail 前缀。将此与 IL_0033 中对 bar 的调用进行比较。

让编译器理解你的意图

所以,为了继续优雅的使用递归函数,我们只要用尾递归的方式去定义它们,对吗?

说起来容易做起来难。随着函数的复杂性增加以及不同的程序员对代码的处理,确保以尾递归方式定义函数可能是一项挑战。因此,编译器最好能根据开发人员的意图,对应该是尾递归的,但却不是尾递归的函数发出警告。

这正是 RFC-1011 所发生的情况。F# 编译器中实现了新属性 []。开发人员可以使用它来明确自己的意图——这个函数应该是尾递归的。使用此属性注释的函数将被检查是否确实是尾递归,如果不是,则会发出警告。

对警告的分析是在编译器的优化阶段之后进行的,因此这时候对递归函数的任何重写都已应用。否则,编译器可能会对一个可以重写为循环的函数发出错误警告。在分析过程中,将遍历类型化抽象语法树(TAST),并检查对具有新属性的函数的递归调用是否以尾递归方式发生。例如,序列表达式中的第一个位置会导致函数调用不具有尾部递归性。

将这个新属性应用于 countDown1,如下所示:

[<TailCall>]
let rec countDown1 n =if n = 0then 0elsecountDown1 (n - 1) + 1

对于 F# 8,编译器会针对函数的非尾递归定义发出警告:

[<TailCall>]
let rec countDown1 n =if n = 0then 0elsecountDown1 (n - 1) + 1

借助编译器的这一功能,开发人员将能够更加专注于他们的工作领域,而不必担心编译代码的技术细节。

致谢

实现此警告是迄今为止我对 F# 编译器的最大贡献。在 PR 获得批准之前,我不得不尝试不同的方法。随后我还修复了一些错误,这些错误没有在 .NET 8 中得到解决,但应该会在第一个补丁发布时得到解决。

我要感谢一路上帮助过我的所有人,以及感谢 Avi Avni 在多年前开启了此功能的第一个 PR。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/599391.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用爬虫爬取热门电影

文章目录 网站存储视频的原理M3U8文件解读网站分析代码实现 网站存储视频的原理 首先我们来了解一下网站存储视频的原理。 一般情况下&#xff0c;一个网页里想要显示出一个视频资源&#xff0c;必须有一个<video>标签&#xff0c; <video src"xxx.mp4"&…

【Python_PyQtGraph 学习笔记(九)】基于PlotWidget实现plot对象的坐标点添加标签

基于PlotWidget实现plot对象的坐标点添加标签 前言正文1、f_plotAddMark(self, xLst, yLst) 方法1、方法传参介绍2、方法内参数介绍2、方法调用3、案例完整代码4、实现效果前言 本文介绍如何在 PlotWidget 的 plot 对象坐标点上添加标签,也可以说是在 PlotWidget 上添加点的标…

Note: A Journey Across Canada

A Journey Across Canada 一场横穿加拿大的旅行 across journey After a quiz last autumn, Kuang crossed the continent eastward to Toronto to visit his schoolmate, the distance measuring approximately 5000 kilometers. 去年秋天一次考试后&#xff0c;Kuang向东穿…

数字人克隆系统开发公司?

广州硅基技术开发限公司是一家位于中国广东省广州市的科技公司。该公司专注于人工智能&#xff08;AI&#xff09;领域的研发和创新。广州硅基以技术创新和解决方案为核心&#xff0c;致力于为客户提供高质量的人工智能产品和服务。 广州硅基技术的主要业务包括但不限于&#…

Java中的序列化和反序列化:深入理解和实战

文章目录 1. 简介2. Java中的序列化3. Java中的反序列化4. Java序列化中的常见问题和解决策略5. 自定义序列化6. 常见问题及解答7. 使用场景8. 结尾 1. 简介 序列化和反序列化的本质是解决在进行远程通信和持久化数据时&#xff0c;如何保存和恢复数据的问题。 ** 序列化&…

stm32学习笔记:TIIM-输入捕获

输入捕获理论 4个输入捕获和输出比较通道&#xff0c;共用4个CCR寄存器 另外它们的CH1到CH4&#xff0c;4个通道的引脚&#xff0c;也是共用的。 所以对于同一个定时器&#xff0c;输入捕获和输出比较只能使用其中一个&#xff0c;不能同时使用。 电平跳变&#xff1a;上升沿…

《动手学深度学习》学习笔记 第5章 深度学习计算

本系列为《动手学深度学习》学习笔记 书籍链接&#xff1a;动手学深度学习 笔记是从第四章开始&#xff0c;前面三章为基础知道&#xff0c;有需要的可以自己去看看 关于本系列笔记&#xff1a; 书里为了让读者更好的理解&#xff0c;有大篇幅的描述性的文字&#xff0c;内容很…

Spring学习 Spring概述

1.1.Spring介绍 ​ Spring是轻量级Java EE应用开源框架&#xff08;官网&#xff1a; http://spring.io/ &#xff09;&#xff0c;它由Rod Johnson创为了解决企业级编程开发的复杂性而创建 1.2.简化应用开发体现在哪些方面&#xff1f; IOC 解决传统Web开发中硬编码所造成的…

python中collections.abc.Mapping 和collections.Mapping的区别

文章目录 在 Python 中&#xff0c;collections.abc.Mapping 和 collections.Mapping 都是用于表示映射类型&#xff08;即键值对的集合&#xff0c;例如字典&#xff09;的抽象基类。它们的区别在于它们的来源和使用方式。 collections.abc.Mapping 是 collections.abc 模块中…

1月5日代码随想录完全二叉树的节点个数

222.完全二叉树的节点个数 给你一棵 完全二叉树 的根节点 root &#xff0c;求出该树的节点个数。 完全二叉树 的定义如下&#xff1a;在完全二叉树中&#xff0c;除了最底层节点可能没填满外&#xff0c;其余每层节点数都达到最大值&#xff0c;并且最下面一层的节点都集中在…

使用ModelScope运行或者微调模型ModelScope国内一个“模型即服务”(MaaS)平台

TOC 一、ModelScope社区 ModelScope是一个“模型即服务”(MaaS)平台&#xff0c;由阿里推出维护&#xff0c;旨在汇集来自AI社区的最先进的机器学习模型&#xff0c;并简化在实际应用中使用AI模型的流程。ModelScope库使开发人员能够通过丰富的API设计执行推理、训练和评估&a…

即时设计:轻松实现设计稿动画,打造独具魅力的GIF作品

制作动画 随着动画设计越来越受欢迎&#xff0c;设计师们需要一款强大的工具&#xff0c;以便轻松控制设计稿元素的属性&#xff0c;实现动画效果。今天&#xff0c;我们向您推荐一款具备帧动画功能的设计工具&#xff0c;它可以让您轻松调整元素的宽高、相对位置等属性&#x…

Spring AI 指南

近年来&#xff0c;人工智能技术的迅猛发展改变了我们对科技的看法&#xff0c;并在各个领域引发了巨大的变革。每个人都希望在自己的项目上能够使用人工智能。Spring 框架提供了一个名为 “Spring AI” 的项目&#xff0c;Spring AI 项目旨在简化包含人工智能功能的应用程序的…

Python 基础面试第四弹

1. Python中常用的库有哪些&#xff0c;作用分别是什么 requests: requests 是一个用于发送 HTTP 请求的库&#xff0c;它提供了简单而优雅的 API&#xff0c;可以轻松地发送 GET、POST、PUT、DELETE 等请求&#xff0c;并处理响应数据。它支持会话管理、身份验证、文件上传等常…

Matlab绘制动态心形线

1. 代码 for alpha0:0.1:30 x-1.8:0.001:1.8; y(x.^2).^(1/3)0.9*(3.3-x.^2).^(1/2).*sin(alpha*pi*x); plot(x,y,r-,LineWidth,1.2); set(gca,YGrid,on); axis([-3,3,-2,4]); text(-2,3.35,$f(x)x^{\frac{2}{3}}0.9(3.3-x^2)^{\frac{1}{2}}sin(\alpha\pi x)$,Interpreter,lat…

Geotrust DV通配符证书保护域名数量

Geotrust是一家知名的SSL证书提供商&#xff0c;旗下有多种类型的SSL数字证书&#xff0c;保护网站数据在传输过程中的安全性和完整性&#xff0c;帮助用户确认其网站的安全。通配符SSL证书是Geotrust颁发的一种可以同时保护多个域名站点的SSL证书。今天就随SSL盾小编了解Geotr…

Toshiba 数字隔离器助力工业应用实现稳定的高速隔离数据传输

隔离器件是将输入信号进行转换并输出&#xff0c;以实现输入、输出两端电气隔离的一种安规器件。电气隔离能够保证强电电路和弱电电路之间信号传输的安全性&#xff0c;如果没有进行电气隔离&#xff0c;一旦发生故障&#xff0c;强电电路的电流将直接流到弱电电路&#xff0c;…

啊哈c语言——逻辑挑战8:验证哥德巴赫猜想

上面这封书信是普鲁士数学家哥德巴赫在1742年6月7日写给瑞士数学家欧拉的&#xff0c;哥德巴赫在书信中提出了“任一大于2的整数都可以写成3个质数之和”的猜想。当时&#xff0c;哥德巴赫遵照的是“1也是素数”的约定。现今&#xff0c;数学界已经不使用这个约定了。哥德巴赫原…

Spring Boot 整合 Knife4j(快速上手)

关于 Knife4j 官方文档&#xff1a;https://doc.xiaominfo.com/ Knife4j是一个基于Swagger的API文档生成工具&#xff0c;它提供了一种方便的方式来为Spring Boot项目生成在线API文档。Knife4j的特点包括&#xff1a; 自动化生成&#xff1a;通过Swagger注解&#xff0c;Kn…

yq操作yaml插入列表数据支持传参

yq是基于golang语言开发的一款json、yaml以及xml命令行工具&#xff0c;支持多个平台&#xff0c;github官网&#xff1a;GitHub - mikefarah/yq: yq is a portable command-line YAML, JSON, XML, CSV, TOML and properties processor 文档地址&#xff1a;Shell Completion …