你的函数是什么颜色的?What Color is Your Function?

前言

2015年,程序员Bob Nystrom在博客上发表文章What Color is Your Function,这篇文章反馈很高,且未随时代发展而过时,原文链接如下(想要看懂原文需要较高的英文水平,作者在里面玩了很多英文梗):

What Color is Your Function? – journal.stuffwithstuff.com

这篇文章的主旨,我认为是以设计编程语言的角度,探讨同步函数与异步函数、以及他们在语言层的最优实现。

本文的内容主要是对文章原内容,按照读者的理解进行的本土化翻译,并添加了一些实例作为解释

但是每个读者都有每个读者的理解,阅读原文还是十分必要的

正文

一门新语言

作者定义了一门语法很类似JS的语言,想讲明什么是作者口中的函数的颜色,同时,作者做出了如下定义:

  1. 每个函数要么是红色,要么是蓝色
  2. 红色函数可以调用蓝色函数,但是只能被红色函数调用
  3. 红色函数调用成本更高(这是个强制定义,可以理解为你调用红色函数就必须给领导写申请文档)
  4. 标准库里一些我们不得不用的函数是红色的

我们自然会得出一个基本结论,优先蓝色,迫不得已采用红色

好的,接下来我们正常的编写代码

我们定义两个不同的函数

String processData() {return "同步处理数据";
}String fetchData() {return "从远程服务器获取数据";
}

调用方法如下,一切顺利进行

void main() {var data = fetchData();print(data);
}

现在fetchData方法突然变得复杂起来,需要调用某个底层库函数的支持,糟糕的是要调用的函数是红色,原本正常的蓝色函数main也变成了红色,同时所有调用fetchData的函数都变成了红色,我们需要重写巨多代码,甚至重构代码结构,这太糟糕了。

红色具有强烈的传染能力。

色彩比喻

作者提到的红色函数就是异步函数,并且就此大力批评了js

1. 蓝色函数(同步)直接返回值,红色函数(异步)需要通过回调处理结果。

2. 在同步函数中无法直接调用异步函数,因为结果尚未准备好。

3. 异步函数很难处理,错误捕获处理方式不同,也不能和同步控制结构良好兼容。

4. Node.js 标准库大量使用异步函数,迫使开发者处理“红色函数”的复杂性。

事实上回调地狱就是红色函数导致的:

function getData(callback) {setTimeout(() => {callback("数据1");}, 1000);
}function processData(data, callback) {setTimeout(() => {callback(data + " -> 处理后的数据");}, 1000);
}function displayData(data) {console.log("显示: " + data);
}// 回调地狱
getData((data1) => {processData(data1, (processedData) => {getData((data2) => {processData(data2, (processedData2) => {displayData(processedData2);});});});
});
人们发明了Promise和Future

Node社区的人受够了回调的痛苦,所以发明了Promise,但是作者认为并没有改进很多,Promise,Async,Await的语法糖缓解了使用红色函数的成本,但是还是让函数有着两种颜色。

比如最开始我们要用回调处理异步结果和错误

function fetchData(callback, errorCallback) {setTimeout(() => {callback("数据加载成功");}, 1000);
}fetchData((data) => console.log(data),(error) => console.log("错误: " + error)
);

经过Promise的改进,我们可以这样写了

function fetchData() {return new Promise((resolve, reject) => {setTimeout(() => {resolve("数据加载成功");}, 1000);});
}fetchData().then((data) => console.log(data)).catch((error) => console.log("错误: " + error));

但是我们还是不能让它与同步代码结合:

let data = fetchData();  // 错误,返回的是 Promise 对象
console.log(data);  // 不能直接拿到结果

同步和异步的分裂还是存在,要么全部写异步,要么重新设计代码架构。

但是有人说,还有await语法糖,我们可以await这个结果

作者认为繁多的语法糖消除了红色函数的使用成本,但是我们仍然会面临困境:

function fetchData(IDs) {let dataList = IDs.map(async (ID) => {let data = await fetchDataFromRemote(ID);return data;});return dataList; // 这里返回的是 Promise 数组,而不是期望的结果
}

所以我们又要这样来写

async function fetchData(IDs) {let dataList = await Promise.all(IDs.map(ID => fetchDataFromRemote(ID)));return dataList;
}
问题的本质

底层的实际问题可以表述为“该如何在异步操作完成时,回到上一次代码执行的地方”。

由于异步操作(如 I/O)不能阻塞整个调用栈,必须回退整个栈并返回到事件循环,这样才能让操作系统处理异步任务。

同步代码可以通过调用栈追踪执行进度,而异步 I/O 需要用闭包保存状态。每次执行后,调用栈会销毁,但闭包让数据保留在堆中。这就是所谓的 continuation-passing style(CPS),将执行上下文通过函数传递下去。

如下面段代码中,每个函数闭合了它的上下文(如 iceCream 和 caramel),这些数据被存储在堆中,而不再依赖于调用栈。每个嵌套的函数都将控制权传递给下一个回调函数。

function makeSundae(callback) {scoopIceCream(function (iceCream) {warmUpCaramel(function (caramel) {callback(pourOnIceCream(iceCream, caramel));});});
}

异步带来的问题:

调用栈无法保存异步函数的执行状态,因此必须依赖回调或闭包去保存这些数据。

异步操作需要手动“回到”先前的执行上下文,而不是自然地通过调用栈返回。

Promise 也没完全解决问题:

尽管 Promise 改善了异步代码的可读性(通过 .then() 链式调用),但仍然需要手动管理这些闭包和函数字面量。代码结构依然是 continuation-passing style,只是 Promise 使其看起来更直观一些。

有了回调、承诺、异步等待和生成器,最终你会把你的异步函数展开成一堆堆在堆内存中的闭包。
你的函数将最外层的闭包传递给运行时。当事件循环或 I/O 操作完成时,它会调用那个函数,你可以从上次离开的地方继续。但这意味着你上面的所有东西也必须返回。你仍然必须展开整个栈。这就是 “红色函数只能被红色函数调用” 规则的由来。你必须将整个调用栈一直闭包化回到 main () 函数或事件处理程序。

结语、我的总结

作者认为Go语言是真正的无色代码

在我看来,Go 语言在这方面做得最为出色。一旦进行任何 I/O 操作,它就会暂停那个协程,并恢复任何其他没有被 I/O 阻塞的协程。

在 Go 语言中,并发是你选择如何构建程序的一个方面,而不是像在某些语言中那样成为标准库中每个函数的一个特定 “颜色” 属性。这意味着上文提到规则所带来的所有痛苦在 Go 语言中被完全消除了。

的确如此:Go 的 Goroutine 是语言原生支持的轻量级线程,启动 Goroutine 不需要特殊标记,调用 Goroutine 和普通函数没有区别,语言设计上没有“异步函数”这个概念,也不需要像 async、await 这样的显式标记,Go 的调度器会自动处理 I/O 的异步性,但对开发者是透明的。

事实上,对于我比较了解的语言,如kotlin,dart,js,他们都不是无色语言。

在 Kotlin 中,挂起函数有着特定的调用规则,即必须在挂起函数或者协程作用域中进行调用。Kotlin 的协程机制要求开发者更多的来参与处理协程的管理,以确保协程能够高效协作。

kotlin的协程并不能说比go差,但一定的是如果要用好协程,他对开发者对协程的使用有着较高的要求。

读完此篇文章后,联想到文章的发布日期--2015年,几乎10年前,不得不感叹笔者的前瞻性,致敬

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

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

相关文章

智慧园区的未来:三维可视化系统建设策略

园区三维可视化系统是指利用先进的数字技术和虚拟现实技术,对园区内部空间、设施、资源等进行实时模拟、展示和管理的系统。该系统可以帮助园区管理者更好地了解园区整体布局、资源分配情况,优化空间规划、设施管理,提高园区运营效率和服务水…

江恩理论(Gann Theory)

威廉江恩 威廉江恩(William D.Gann),1878年出生于美国德州,二十世纪最著名的投资家。在股票市场上的骄人成绩无人可比。 江恩理论 江恩理论是一种通过数学、几何学、宗教和天文学的综合运用,来分析和预测市场走势的投…

探索 Python 中的 XML 转换利器:xml2dict

文章目录 **探索 Python 中的 XML 转换利器:xml2dict**一、背景介绍二、xml2dict 是什么?三、如何安装 xml2dict?四、基本用法五、实际应用场景六、常见问题及解决方案七、总结 探索 Python 中的 XML 转换利器:xml2dict 一、背景…

【Unity精品插件】Magica Cloth:Unity布料模拟的新高度

📂 Unity 开发资源汇总 | 插件 | 模型 | 源码 💓 欢迎访问 Unity 打怪升级大本营 在Unity游戏开发中,物理模拟是使角色和场景更加生动和真实的重要工具之一。Magica Cloth 是一个专为物理效果设计的插件,它提供了轻量级、高性能的…

当LangGraph遇上Mem0:如何让你的AI Agent具有更智能的记忆与个性化的体验?

AI Agent(智能体)的记忆(Memory)被认为是一项必备的基础能力,它用来提取、存储会话中的重要信息并用于后续的检索与使用。可以把记忆简单地分成短期记忆与长期记忆两种,用来“记住”不同类型的信息&#xf…

华为CE交换机telnet登录失败故障的排查方法

检查网络状态 操作步骤 1、执行命令ping xxx.xxx.xxx.xxx(目的IP网段) 根据报文是否丢包确认是否可以访问该设备,网络不通请检查组网及网络地址、静态路由等相关配置。 检查VTY通道是否已占满? 操作步骤 1、在任意视图下,执行命令displ…

【MySQL】清理二进制日志文件 binlog.000XXX 以解决 Ubuntu 系统磁盘空间耗尽的问题

问题描述 在使用Ubuntu系统时,发现磁盘空间异常地被填满。通过使用ncdu工具进行检查,结果显示/var/lib/mysql文件夹占用了高达63GB的存储空间。 进一步查看该目录,发现存在几百个以binlog为前缀的文件。 原因分析 这些以binlog为前缀的文件…

2020年计算机网络408真题解析

第一题: 解析:OSI参考模型网络协议的三要素 网络协议的三要素:语法 ,语义,同步(时序) 语法:定义收发双方所交换信息的格式 语法:定义收发双方所要完成的操作 网页的加载 …

汽车电子笔记之-014:一场FIFO的思考引发将汽车电子DTC相关 - 故障发生前后关键数据记录并回读的功能浅研发

目录 1、概述 2、故障发生前数据记录 2.1、环形数组C语言实现 2.2、FIFO的C语言实现 3、故障发生后数据记录 4、数据存储 4.1、数据进FIFO设计思路 4.2、数据出FIFO设计思路 5、数据回读 1、概述 工作中DTC的冻结帧与扩展数据功能一般用于存储故障发生时刻的一些关键数…

Unity Apple Vision Pro 保姆级开发教程 - Simulator 模拟器使用

教程视频 Apple VisionPro Simulator 模拟器使用教程 Unity Vision Pro 中文课堂教程地址: Unity3D Vision Pro 开发教程【保姆级】 | Unity 中文课堂 ​ VsionOS Simulator 简介 visionOS Simulator 是一个用于开发和测试 visionOS 应用程序的工具。它模拟 Appl…

数仓模型规范设计

模型架构设计 数仓架构一般从宏观上分为三层:操作数据层ODS、公共维度模型层CDM和数据应用层ADS。其中CDM又包含明细数据层DWD、汇总数据层DWS,维度层DIM、根据生产经验这里可在加入数据临时层TMP。架构图如下: ODS 把操作系统的数据几乎无…

高中数学:立体几何-外接球的外心法

文章目录 一、外心法定义二、习题1、例题一2、例题二3、例题三4、例题四 一、外心法定义 依然以三棱锥为例 即,找到三棱锥的外接球的球心,从而可以确定出外接球的半径R。 而三棱锥有四个顶点,这四个顶点必然都在外接球的球面上。 寻找思路…

海蓝色主题移动端后台UI作品集模板源文件分享 figmasketch格式

页面数量:30页 页面尺寸:1920*1080px 发给你的文件:作品集Figma源文件、作品集sketch源文件、部字体文件、高质量作品集包装psd样机文件(含手机和电脑样机)

设计模式概览

设计模式是一种解决常见编程问题的经验总结,提供了代码的可重用性、可扩展性和可维护性。常见的设计模式有23个,主要分为三大类:创建型模式、结构型模式和行为型模式。下面是这三类设计模式的详细分类和讲解: 一、创建型模式 创建…

linux多窗口调试一些常用命令

在 vim 或 neovim 中使用分屏移动光标的方式: 希望光标从左窗口移动到右侧窗口: 按 Ctrlw 然后按 l(小写的 L),光标就会从左边窗口移动到右边窗口。 其它分屏操作: Ctrlw h:移动到左边的窗…

【我的 RT 学习手札】信息收集

相关笔记整理自B站up主泷羽sec全栈渗透测试教学(免费) 视频链接为泷羽sec的个人空间-泷羽sec个人主页-哔哩哔哩视频 笔记只是方便师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线&#xff0…

11 图书借阅功能实现(Vue3+element plus +Spring Boot)

目录 1 功能描述2 接口地址3 后端代码4 api/book.js中编写借阅图书的接口代码5 BookResourcesVue.vue组件中完成点击事件borrowBook6 功能演示 1 功能描述 普通用户借阅图书,点击借阅按钮,修改图书状态,最多能够借阅3本图书。 2 接口地址 …

保证缓存一致性的常用套路

缓存更新的套路 看到好些人在写更新缓存数据代码时,先删除缓存,然后再更新数据库,而后续的操作会把数据再装载的缓存中。然而,这个是逻辑是错误的。试想,两个并发操作,一个是更新操作,另一个是…

[MyBatis-Plus]扩展功能详解

代码生成 使用MP的步骤是非常固定的几步操作 基于插件, 可以快速的生成基础性的代码 安装插件安装完成后重启IEDA连接数据库 mp是数据库的名字?serverTimezoneUTC 是修复mysql时区, 不加会报错 生成代码 TablePrefix选项是用于去除表名的前缀, 比如根据tb_user表生成实体类U…

恒定电流下有功率密度,功率密度体积分就是恒定电流的功率

体积趋于0时,体积的功率就叫功率密度 恒定电流的 电场乘距离等于电压 电流面密度*面积等于电流注意:电流面密度不是电荷线面体密度,电荷线面体密度用在静电场中,即电荷不运动这种