为什么人们对即将推出的 Go 1.23 迭代器感到愤怒

news2024/11/19 3:37:16

在这里插入图片描述

原文:gingerBill - 2024.06.17

TL;DR 它让 Go 变得太“函数式”,而不再是不折不扣的命令式语言。

最近,我在 Twitter 上看到一篇帖子,展示了 Go 1.23(2024 年 8 月)即将推出的 Go 迭代器设计。据我所知,很多人似乎都不喜欢这种设计。作为一名语言设计者,我想谈谈自己的看法。

有关该提案的 合并 PR 可在此处找到:https://github.com/golang/go/issues/61897

其中有对设计的深入解释,说明了为什么要选择某些方法,因此我建议熟悉 Go 的人阅读一下。

以下是我从原始推文中看到的示例:

func Backward[E any](s []E) func(func(int, E) bool) {
	return func(yield func(int, E) bool) {
		for i := len(s)-1; i >= 0; i-- {
			if !yield(i, s[i]) {
				// 清理代码的位置
				return
			}
		}
	}
}

s := []string{"a", "b", "c"}
for _, el in range Backward(s) {
	fmt.Print(el, " ")
}
// 输出:c b a

这个示例在 做什么 上足够清晰,但对于一般/大多数用例来说,它的整个设计对我来说有点疯狂。

据我了解,这段代码将被转换成如下内容:

Backward(s)(func(_ int, el string) bool {
	fmt.Print(el, " ")
	return true // `return false` 相当于显式 `break`
})

这意味着 Go 的迭代器更接近于某些语言中的 “for each” 方法(例如 JavaScript 中的 .forEach()),并向其传递一个回调函数。有趣的是,这种方法在 Go 1.23 之前的版本中已经可以实现,但没有语法糖可以在 for range 语句中使用。

我将尝试总结 Go 1.23 迭代器的基本原理,但它们似乎希望尽量减少几个因素:

  • 让迭代器看起来像其他语言中的生成器(因此使用 yield
  • 尽量减少共享过多的栈帧
  • 允许使用 defer 进行清理
  • 减少在控制流之外存储数据

正如 Russ Cox (rsc) 在原始提案中解释的那样:

关于 push 和 pull 迭代器类型的说明:在绝大多数情况下,push 迭代器更方便实现和使用,因为可以围绕 yield 调用进行设置和拆卸,而不必将它们实现为独立的操作,然后再暴露给调用者。直接使用(包括 range 循环)push 迭代器需要放弃在控制流中存储任何数据,因此个别客户端可能偶尔需要 pull 迭代器。任何此类代码都可以轻松调用 Pull 并 defer 停止。

Russ Cox 在他的文章 Storing Data in Control Flow 中更详细地阐述了他喜欢这种设计方法的原因。

更复杂的示例

注意: 不用担心它实际上做了什么,我只是想展示一个需要 defer 进行清理的示例。

原始 PR 中的一个示例展示了一个需要清理的更复杂的方法,其中值是直接 pull 出来的:

// Pairs 返回一个迭代器,该迭代器依次迭代 seq 中的键值对。
func Pairs[V any](seq iter.Seq[V]) iter.Seq2[V, V] {
	return func(yield func(V, V) bool) bool {
		next, stop := iter.Pull(it)
		defer stop()
		v1, ok1 := next()
		v2, ok2 := next()
		for ok1 || ok2 {
			if !yield(v1, v2) {
				return false
			}
		}
		return true
	}
}

一个替代的伪提案(状态机)

注: 我并不建议 Go 采取这种方法。

在设计 Odin 语言时,我希望用户能够设计自己的“迭代器”,但它们要非常简单;事实上,它们只是普通的过程(procedure)。我不想为此在语言中添加特殊结构——这会使语言变得过于复杂,而这正是我想在 Odin 中尽量避免的。

我可以为 Go 的迭代器给出一个如下的伪提案:

func Backward[E any](s []E) func() (int, E, bool) {
	i := len(s) - 1
	return func(onBreak bool) (idx int, elem E, ok bool) {
		if onBreak || !(i >= 0) {
			// 这里放清理代码,如果有的话
			return
		}
		idx, elem, ok = i, s[i], true
		i--
		return
	}
}

这个伪提案的操作方式是这样的:

for it := Backward(s);; {
	_, el, ok := it(false)
	if !ok {
		break // 不需要调用 it(true),因为调用了 `false`
	}

	fmt.Print(el, " ")
}

这与我在 Odin 中的做法相似, Odin 不支持栈帧范围捕获闭包,只支持非范围捕获的过程字面量。由于 Go 是有 GC 的,所以我认为没有必要这样使用它们。主要区别在于,Odin 并不试图将这些概念统一到一个构造中。

我知道有些人会认为这种方法更加复杂。它与 Cox 在控制流中存储数据的偏好相反,而是将数据存储在控制流之外。但这通常是我对迭代器的期望,而不是 Go 将要做的。这就是问题所在:它消除了在控制流中存储数据的优雅性——正如 Cox 解释的那样,push/pull 的区别。

注: 我是一个非常命令式编程的程序员,喜欢了解事情的实际执行过程,而不是尝试让代码“看起来优雅”。因此,我上面写的方法基本上是关于执行方面的思考。

附注:typeclass/interface 的路线在 Go 中行不通,因为这不是一个正交的设计概念,实际上会比必要的更令人困惑——这就是我最初没有提出它的原因。不同的语言对哪些方法有效有不同的要求。

Go 的显见理念

Go 1.23 所采用的方法似乎与其显见理念背道而驰——即让 Google 的普通(坦率地说是平庸)程序员使用 Go,因为他们不想(也不能)使用像 C++ 这样“复杂”的语言。

引用 Rob Pike 的话:

关键点在于我们的程序员是 Googlers,他们不是研究人员。他们通常相当年轻,刚从学校毕业,可能学过 Java,也许学过 C 或 C++,可能学过 Python。他们无法理解一种复杂的语言,但我们希望他们能编写出好的软件。因此,我们提供给他们的语言必须易于理解和采用。

我知道很多人对这一评论感到不快,但通过了解你为谁设计语言,这才是 出色的 语言设计。这不是侮辱,而是实事求是的陈述,因为 Go 最初是为在谷歌和类似行业工作的人设计的。你可能是一个比普通 Googler “更好、更能干”的程序员,但这并不重要。人们喜欢 Go 是有原因的:它简单、坚持自己的设计理念,而且大多数人可以很快上手。

然而,这种迭代器设计确实不符合 Go 的特性,尤其是对于像 Russ Cox(假设他是最初的提议者)这样的 Go 团队提议者来说。它让 Go 变得更加复杂,甚至更“神奇”。我理解迭代器系统的工作原理,因为我是一个语言设计和编译器实现者。但它也可能存在性能问题,因为需要使用闭包和回调。

也许这种设计的理由是,普通 Go 程序员 并不需要 实现 迭代器,而只是 使用 它们。而且大多数人需要的迭代器已经在 Go 的标准库中,或者由第三方库提供。因此,责任在于库的 编写者,而不是库的 使用者

这就是为什么很多人似乎对这种设计感到“愤怒”。在很多人看来,它违背了 Go 最初的“宗旨”,看起来像一个非常复杂的“混乱”。我理解它看起来像生成器那样使用 yield 和内联代码的“美感”,但我不认为这符合许多人对 Go 的理解。Go 确实隐藏了很多神奇的运作方式,特别是垃圾收集、go routinesselect 语句和许多其他结构。然而,我认为这有点过于神奇了,因为它向用户暴露了太多的魔法,同时对普通 Go 程序员来说显得过于复杂。

另一个让人感到“困惑”的方面是,它是一个返回 funcfunc,后者以 func 作为参数。而 for range 的主体被转换为 func,所有的 break(以及其他逃逸控制流)都被转换为 return false。这只是三层深的过程(procedures),再次让人感觉像是函数式语言而不是命令式语言。

注意: 我并不是建议他们用我的方法替换迭代器设计,而是认为通用的迭代器方法可能一开始就不适合 Go。至少对我而言,Go 是一种不妥协的命令式语言,具有一流的类似 CSP 的结构。它并不试图成为一种类似函数式的语言。迭代器处于一个奇怪的位置,它们存在于命令式语言中,但作为一种概念,它们非常“函数式”。迭代器在函数式语言中可以非常 优雅,但在许多不妥协的命令式语言中,它们总是显得有些“奇怪”,因为 它们被统一为一个独立的结构,而不是将其部分分离出来(初始化+迭代器+销毁)。

题外话:Odin 的方法

正如我之前提到的,在 Odin 中,迭代器只是一个过程调用,多个返回值中的最后一个值只是一个布尔值,表示是否继续。由于 Odin 不支持闭包,相比于 Go 的 Backward 迭代器,Odin 中的等效迭代器需要编写更多的代码。

注意: 在有人说“这看起来更复杂”之前,请继续阅读本文。大多数 Odin 迭代器都不是这样的,而且我从不建议编写这样的迭代器,因为对于代码的读者和编写者来说,简单的 for 循环都是更可取的。

// 状态的显式结构体
Backward_Iterator :: struct($E: typeid) {
	slice: []E,
	idx:   int,
}

// 迭代器的显式构造
backward_make :: proc(s: []$E) -> Backward_Iterator(E) {
	return {slice = s, idx = len(s)-1}
}

backward_iterate :: proc(it: ^Backward_Iterator($E)) -> (elem: E, idx: int, ok: bool) {
	if it.idx >= 0 {
		elem, idx, ok = it.slice[it.idx], it.idx, true
		it.idx -= 1
	}
	return
}

s := []string{"a", "b", "c"}
it := backward_make(s)
for el, _ in backward_iterate(&it) { // 也可以写成 `for el in`
	fmt.print(el, " ")
}
// 输出:c b a

由于需要编写更多的代码,这看起来确实比 Go 方法复杂得多。然而,它实际上更容易理解、掌握,并且执行速度更快。迭代器不会调用 for 循环的主体,而是主体调用迭代器。我知道 Cox 喜欢 在控制流中存储数据 的能力,我也同意这很好,但它不太适合 Odin,特别是 Odin 缺乏闭包(因为 Odin 是手动内存管理的语言)。

一个“迭代器”只是以下代码的语法糖:

for {
	el, _, ok := backward_iterate(&it)
	if !ok {
		break
	}
	fmt.print(el, " ")
}

// 使用 `or_break`

for {
	el, _ := backward_iterate(&it) or_break
	fmt.print(el, " ")
}

Odin 的方法只是去除了魔法,让事情变得非常清楚。“构造”和“销毁”必须通过显式的过程手动处理。而迭代只是一个简单的过程,每次循环调用一次。这三个结构被分别处理,而不是像 Go 1.23 那样合并为一个令人困惑的结构。

Odin 不会隐藏魔法,而 Go 的方法实际上非常神奇。Odin 让你手动处理“类似闭包”的值以及“迭代器”本身的构造和销毁。

Odin 的方法也轻松允许你拥有任意多个返回值!一个很好的例子是 Odin 的 core:encoding/csv 包,其中的 Reader 可以被视为一个迭代器:

// core:encoding/csv
iterator_next :: proc(r: ^Reader) -> (record: []string, idx: int, err: Error, more: bool) {...}
// 用户代码
for record, idx, err in csv.iterator_next(&reader) {
	...
}

题外话:C++ 迭代器

我尽量避免在这篇文章中对 C++ 的“迭代器”进行大篇幅的抱怨。C++ 的迭代器远不止是简单的迭代器,而至少 Go 的方法仍然是简单的迭代器。我完全理解为什么 C++ 的“迭代器”会那样设计,但 99.99% 的时间我只需要一个简单的迭代器,而不是具有所有代数性质的可以在更“通用”的地方使用的东西。

对于那些不太了解 C++ 的人来说,迭代器是一个自定义的 struct/class,它需要重载 operators 来使其表现得像一个“指针”。历史上,C++ 的“迭代器”看起来是这样的:

{
	auto && __range = range-expression ;
	auto __begin = begin-expr ;
	auto __end = end-expr ;
	for ( ; __begin != __end; ++__begin) {
		range-declaration = *__begin;
		loop-statement
	}
}

并且在 C++11 的 ranged-for 循环语法(和 auto)成为一种规范之前,会被封装在一个“宏”中。

最大的问题是,C++ 的“迭代器”至少需要你定义五种不同的操作。

以下三个运算符重载:

  • operator==operator!=
  • operator++
  • operator*

还有两个返回迭代器值的独立过程或绑定方法:

  • begin
  • end

如果让我设计 C++ 的简单迭代器,我只会在 struct/class 中添加一个名为 iterator_next 的简单方法。就是这样。是的,这确实意味着其他代数性质会丧失,但老实说,我在处理任何问题时都不需要这些特性。当我处理这些类型的问题时,要么使用连续数组,要么手动实现算法,因为我要保证这种数据结构的性能。然而,我之所以创建了自己的语言(Odin),是因为我完全不同意 C++ 的整个哲学,并且我想远离那种混乱。

C++ 的“迭代器”比 Go 的迭代器复杂得多,但在本地操作上更“直接”。至少在 Go 中,你不需要构造一个包含五个不同属性的类型。

总结

我觉得 Go 的迭代器在设计原则上是合理的,但是看起来违背了大多数人对 Go 的看法。我知道 Go 随着时间的推移“不得不”变得更加复杂,特别是引入了泛型(我认为这些泛型设计得很好,只有一些语法上的小问题),但是引入这种类型的迭代器感觉 是错误的。

简而言之,这种设计感觉与许多人认为的 Go 的哲学相违背,并且这种方法非常具有函数式编程的风格,而不是命令式编程的风格。

正因为这些原因,我认为人们不喜欢迭代器的设计,尽管我完全理解这些设计选择。对很多人来说,它“感觉”不行 Go 最初的样子。

也许我和其他人的担忧被夸大了,大多数人实际上不会去实现它们,只是使用它们,而且它们的复杂性主要体现在实现上。

倒数第二个有争议的观点:也许 Go 需要更多地“把关”,告诉那些“函数式编程爱好者”走开,不要再要求这些特性,因为它们会让 Go 成为一种更复杂的语言。

最后一个有争议的观点:如果是我,我根本不会允许自定义迭代器进入 Go,但我不在 Go 团队(也不想在)。

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

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

相关文章

「动态规划」如何求乘积为正数的最长子数组长度?

1567. 乘积为正数的最长子数组长度https://leetcode.cn/problems/maximum-length-of-subarray-with-positive-product/description/ 给你一个整数数组nums,请你求出乘积为正数的最长子数组的长度。一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组。…

2024年GIS专业就业现状和解决办法

GIS专业发展历史 我国从20世纪80年代初引进和研究地理信息系统(GIS) 以来,经过30年的飞速发展,地理信息已成为信息时代重要的组成部分之一,被广泛应用于多个领域的建模和决策支持。 在国家数字化政策的加持下,GIS更成为新基建下…

敏感信息加密操作,让开发的系统更加的安全可靠!!

敏感信息加密操作,让开发的系统更加的安全可靠!!Jasypt(Java Simplified Encryption)是一个开源的Java库,用于简化加密操作。https://mp.weixin.qq.com/s/sPBV8Ej46YJsElImodRjAQ

反射的原理和操作

反射是框架设计的灵魂 (使用的前提条件:必须先得到代表的字节码的Class,Class类用于表示.class文件(字节码)) 在Java中,反射是指在运行时动态地获取、检查和操作类、对象、方法和属性的能力。J…

第2章 Rust初体验3/8:使用Result进行错误处理:编译时错误检查增强代码安全性:猜骰子冷热游戏

讲动人的故事,写懂人的代码 2.3.9 类型的关联函数:简化对象创建和初始化 席双嘉:“那个String::new(),毫无疑问,它确实像C++中的静态成员函数。” 贾克强:“哈哈!是的,两个冒号确实让人联想到一些东西,对吧?” “这其实是Rust中的关联函数(associated function,详…

第四集《唯识与净土》

请大家打开讲义第十面。 我们讲到念佛的功德有两个:第一个是“现世的安乐”,第二个是“来世究竟的解脱”。 这个净土法门的特色,是一种本尊相应法。 也就是说,我们在整个修行破恶生善的过程当中,主要是要仰仗本尊的…

设计师必看|精选免费icon生成网站推荐

在平面设计过程中,如何收集icon素材?(opens in a new tab or window)? 在这里我们为您准备了三个免费、易用的icon生成网站,需要icon材料的同学,记得点赞收藏。 01∣即时设计 作为国内领先的在线设计平台&#xff0…

创建第一个Springboot项目(环境准备、环境存在的问题、启动时存在的问题、启动的方式)

一、环境准备 专业版创建springboot,直接有一个选项可以选择 社区版,需要下载一个spring的插件 不要直接点 install 因为这个插件是付费的,直接点安装只有30天使用期限 在里面找免费版本的下载 然后安装 安装完成后,这个插件名会变…

作为程序员,科班计算机比起非科班有很大优势吗?

在当今这个快速变化的时代,个人的能力与优势成为了职场竞争中的关键因素。在众多的职业选择中,程序员因其独特的技能和市场需求而备受青睐。 然而,并非所有人在18岁就决定要进入计算机行业,许多人都是在大学毕业之后由于种种原因…

一步步教你用Python Selenium抓取动态网页任意行数据

引言 在现代网络中,动态网页越来越普遍,这使得数据抓取变得更具挑战性。传统的静态网页抓取方法在处理动态内容时往往力不从心。本文将详细介绍如何使用Python Selenium抓取动态网页中的任意行数据,并结合代理IP技术以提高抓取的成功率和效率…

PostgreSQL源码分析——缓冲区管理器

这里我们分析一下PG数据库缓冲区的代码。缓冲区是十分重要的,对数据库的性能和稳定性有着直接的影响。缓冲区是数据库SQL计算层与外部存储(磁盘)交互的关键。数据页的落盘与读取,都要经过缓冲区。 README src/backend/storage/…

本地部署AI模型-phi3

What: Phi-3-Mini被认为是Microsoft计划发布的三款小型机型中的首款。据报道,在语言、推理、编码和数学等领域,它在各种基准测试中的表现优于相同大小和下一个尺寸的模型。 从本质上讲,语言模型是 ChatGPT、Claude、Gemini 等 AI…

WPF/C#:显示分组数据的两种方式

前言 本文介绍自己在遇到WPF对数据进行分组显示的需求时&#xff0c;可以选择的两种方案。一种方案基于ICollectionView&#xff0c;另一种方案基于IGrouping。 基于ICollectionView实现 相关cs代码&#xff1a; [ObservableProperty] private ObservableCollection<Peo…

变声方法大公开,变女声很自然的3款变声器推荐,值得收藏

将男声变成女声并且要很自然的变声器有吗&#xff1f;很多喜欢玩游戏的小伙伴们在进行游戏连麦时&#xff0c;可能为了增加趣味&#xff0c;想要试试变成女声来交流&#xff0c;或者喜欢视频剪辑创作的小伙伴们在进行视频配音时&#xff0c;不想用自己的声音出镜&#xff0c;需…

WGCLOUD的web ssh提示websocket服务连接已断开

这个问题一般是server主机没有开放端口9998&#xff0c;因为9998是web ssh的端口&#xff0c;需要开放 我们只要在防火墙&#xff0c;或者安全软件&#xff0c;把这个端口开放了就可以了

STM32HAL库--NVIC和EXTI

1. 外部中断实验 1.1 NVIC和EXTI简介 1.1.1 NVIC简介 NVIC 即嵌套向量中断控制器&#xff0c;全称 Nested vectored interrupt controller。是ARM Cortex-M处理器中用于管理中断的重要组件。负责处理中断请求&#xff0c;分配优先级&#xff0c;并协调中断的触发和响应。 它是…

Scikit-Learn支持向量机回归

Scikit-Learn支持向量机回归 1、支持向量机回归1.1、最大间隔与SVM的分类1.2、软间隔最大化1.3、支持向量机回归1.4、支持向量机回归的优缺点2、Scikit-Learn支持向量机回归2.1、Scikit-Learn支持向量机回归API2.2、支持向量机回归初体验2.3、支持向量机回归实践(加州房价预测…

氮化铝与氧化铍用于大功率电阻器产品

在过去的几十年里&#xff0c;氧化铍&#xff08;BeO&#xff09;一直是用于高功率应用的射频电阻器和端接的主要基板材料。虽然BeO非常适合电子行业的大功率应用&#xff0c;但其粉尘颗粒是有毒的;如果吸入BeO颗粒&#xff0c;它们可能会导致铍中毒&#xff0c;即肺部炎症。由…

202406最新manjaro安装sogou输入法解决方案(采用aur本地package+sogo deb包解决方案)

本地执行安装方法 1.拉取源码 git clone https://gitee.com/liushuai05/fcitx-sogoupinyin.git cd fcitx-sogoupinyin 2.获取sogo下载地址并替换到源码中 - 下载地址&#xff1a;https://pinyin.sogou.com/linux/ - 点击立即下载->x86_64->下载&#xff0c;然后右键复制…

vue+echarts实现tooltip轮播

效果图如下&#xff1a; 实现步骤如下&#xff1a; 定义一个定时器 timer:null, len: 0,页面一加载就清空定时器&#xff0c;此操作是为了防止重复加载时会设置多个定时器在setOption后设置定时器 this.myChart.clear() this.myChart.setOption(option); this.autoShowTool…