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

在这里插入图片描述

原文: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) - 1return func(onBreak bool) (idx int, elem E, ok bool) {if onBreak || !(i >= 0) {// 这里放清理代码,如果有的话return}idx, elem, ok = i, s[i], truei--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 routines、select 语句和许多其他结构。然而,我认为这有点过于神奇了,因为它向用户暴露了太多的魔法,同时对普通 Go 程序员来说显得过于复杂。

另一个让人感到“困惑”的方面是,它是一个返回 func 的 func,后者以 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, trueit.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_breakfmt.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.tpcf.cn/diannao/30143.html

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

相关文章

golang windows打包为linux可执行文件

使用go的交叉编译功能 set GOOSlinux set GOARCHamd64然后再执行go build 可能会报异常, 所以贴出我的go env配置仅供参考 go env环境配置 D:\GoWork\src\go-tzv>go env set GO111MODULEauto set GOARCHamd64 set GOBIN …

寄存器和存储器的区别与联系

在计算机系统中,寄存器和存储器是两个重要的概念,它们在数据存储和处理过程中扮演着不同但互补的角色。本文将详细讨论寄存器和存储器的区别与联系。 1. 基本概念 **寄存器(Register)**是一种速度极快的小容量存储单元&#xff…

Maven 插件列表详解

Maven 是一个强大的项目管理和构建工具,广泛应用于 Java 项目中。作为一款优秀的构建管理工具,Maven 不仅提供了标准化的项目结构和依赖管理,还通过其丰富的插件系统,极大地扩展了其功能和灵活性。无论是代码编译、测试、打包&…

2024级中国社科院中外合作办学双证博士和英国斯特灵的管理学博士

2024级中国社科院中外合作办学双证博士和英国斯特灵的管理学博士 中国社科院-英国斯特灵大学合办的创新与领导力博士项目(DOCTOR OF MANAGEMENT 简称“DMAN”)是中国社会科学院大学(中国社会科学院研究生院)第一个博士层次的中外…

Win 10/Win 11 共享/远程访问的后续设置以及如何查看电脑的账户密码

共享设置 如何给文件设置共享这里不谈,右键属性添加共享,类似的教程也有很多(如果这步都搞不定建议就不要折腾了,老老实实的用U盘吧) 如果不出意外的话,你在网络界面是看不到共享pc和文件的 这时候需要两…

【VS2022 编译UE5.1 错误 C4834 】

这里写自定义目录标题 错误 错误 使用VS2022编译UE5.1源码,错误 C4834 放弃具有 [[nodiscard]] 属性的函数的返回值 F:\UE\Engine\Plugins\Runtime\Steam\SteamVR\Source\SteamVRInputDevice\Private\SteamVRInputDeviceFunctionLibrary.cpp(513): error C4834: d…

python jieba中文文本切割成句子

# -*- coding: utf-8 -*- # Time : 2024/6/18 10:26 # Author : Cocktail_py import jieba.posseg as psegdef cut_sentences(text):"""中文句子分割"""# 使用jieba的分句模块sentences pseg.cut(text)result []tmp []for word, flag in …

要颜值有颜值,有性价比有性价比,华硕天选键、鼠组合分享

作为ROG产品的忠实粉丝,用过不少ROG 相关的产品,近期华硕天选TX98和天选MINI 鼠标的发布,独特配色令我眼前一亮。 华硕天选TX98键盘,作为新品,从看上的第一眼就觉得这款键盘是非常值得推荐。 它完美地诠释了潮玩新次元…

亲测:无影云电脑免费三个月已经缩短为1个月

亲测:无影云电脑免费三个月已经缩短为1个月,大家不要再找3个月的无影云电脑,已经没有了,目前最新消息是1个月。以前可以领3个月,现在只能领1个月,在阿里云免费中心 https://free.aliyun.com/ 大家自己看吧&…

光伏电站管理app,一站式管理,一体化接单

早在1839年,法国科学家贝壳雷尔就发现了“光伏效应”,随后诞生了“光学发电”技术。随着现代化与工业化的不断发展、进步,出于资源再生与保护环境的需要,人们越来越重视光伏发电技术,也越来越追求光伏电站的智能化管理…

JavaScript------const

这里写目录标题 介绍在声明时赋值不是真正的常数常量对象可以更改常量数组可以更改 介绍 ES2015 引入了两个重要的 JavaScript 新关键词:let 和 const。 通过 const 定义的变量与 let 变量类似,但不能重新赋值: const PI 3.1415926535897…

Java中的运算符及其示例

Java中的运算符及其示例 运算符是指示编译器执行特定操作的符号。例如,“”运算符指示编译器执行加法,“>”运算符指示编译执行比较,“”用于赋值等等。在本指南中,我们将借助示例讨论java中的操作。 运算符和操作数&#…

钽掺杂锂镧锆氧(LLZTO)是优秀固态电解质 未来行业前景良好

钽掺杂锂镧锆氧(LLZTO)是优秀固态电解质 未来行业前景良好 钽掺杂锂镧锆氧,也称为锂镧锆钽氧,英文简称LLZTO,外观为白色或淡黄色结晶粉末状。LLZTO具有锂离子电导率高、对锂化学性质稳定等特点,是一种立方晶…

C++迈向精通:函数指针对象与函数对象

C:指针对象 C语言中的函数指针 在C语言中,我们见过如下的函数指针: int add(int a, int b) {return a b; }int main() {int a, b;int (*p)(int, int) add;scanf("%d%d", &a, &b);p(a, b);return 0; } 为了适应C中面向…

BPE (Byte-Pair Encoding) Tokenization

目录 一、Tokenization 的概念二、BPE 的概念三、BPE 的步骤 遇到看不明白的地方,欢迎在评论中留言呐,一起讨论,一起进步! 需掌握的前提知识: 本文参考: P24 台大资讯 2023秋季 深度学习之应用 &#xff5…

mybatis使用注解形式实现一对多、多对多查询(转载)

1、链接:一对一,一对多 2、多对多 原文 MyBatis的注解实现复杂映射开发 实现复杂关系映射之前我们可以在映射文件中通过配置来实现,使用注解开发后,我们可以使用Results注解,Result注解,One注解&#x…

Java学习笔记之基本数据类型转换

前言 本篇文章是基于我本人在初学JAVA阶段想记录的的学习笔记,如有错误,恳请指正。今天要干掉的是JAVA的基本数据类型转换。 笔记目录一览 前言一,基本数据类型复习二,基本介绍什么是自动类型转换? 三,任…

nvidia-smi命令详解

参考: https://zhuanlan.zhihu.com/p/664550672 NVIDAI-SMI Version 版本号; Driver Version 驱动版本号; CUDA Version 版本号; GPU 型号及序号; 风扇; 温度; Perf 性能状态; Pers…

达梦数据库的分区表

达梦数据库的分区表 达梦数据库(DM Database)提供了灵活和强大的分区表功能,以更高效地管理和操作大规模数据集。分区表通过将数据分割成更小、更易管理的部分,可以显著提高查询性能、缩短维护时间、并提高系统的总体可扩展性。 …

统计信号处理基础 习题解答10-15

题目 对随机变量的随机性的一个度量是它的熵(entropy),熵定义为: 如果,求这个熵,并说明它与PDF集中度的关系。观察到数据后,后验PDF的熵可以确定为: 且它应该比小。因此&#xff0c…