Go: 提案:规范:通用编程工具

创建于 2016-04-14  ·  816评论  ·  资料来源: golang/go

这个问题建议 Go 应该支持某种形式的泛型编程。
它具有 Go2 标签,因为对于 Go1.x,该语言或多或少已经完成。

伴随这个问题的是@ianlancetaylor提出的通用泛型提案,其中包括四个针对 Go 通用编程机制的具体有缺陷的提案。

此时的目的不是向 Go 添加泛型,而是向人们展示一个完整的提案会是什么样子。 我们希望这对将来提出类似语言更改的任何人有所帮助。

Go2 LanguageChange NeedsInvestigation Proposal generics

最有用的评论

让我先发制人地提醒大家我们的https://golang.org/wiki/NoMeToo政策。 表情符号派对在上面。

所有816条评论

CL https://golang.org/cl/22057提到了这个问题。

让我先发制人地提醒大家我们的https://golang.org/wiki/NoMeToo政策。 表情符号派对在上面。

Go Generics Discussions 的摘要,它试图提供来自不同地方的讨论的概述。 它还提供了一些示例,如何解决您希望使用泛型的问题。

链接提案中有两个“要求”可能会使实施复杂化并降低类型安全性:

  • 基于在实例化之前未知的类型定义泛型类型。
  • 不要求泛型类型或函数的定义与其用途之间有明确的关系。 也就是说,程序不应该明确说类型 T 实现了泛型 G。

这些要求似乎排除了例如类似于 Rust 特征系统的系统,其中泛型类型受特征边界的约束。 为什么需要这些?

在非常低的级别将泛型构建到标准库中变得很诱人,就像在 C++ std::basic_string 中一样, 标准::分配器>。 这有其好处——否则没有人会这样做——但它具有广泛且有时令人惊讶的效果,例如在难以理解的 C++ 错误消息中。

C++ 中的问题源于类型检查生成的代码。 在代码生成之前需要进行额外的类型检查。 C++概念提案通过允许泛型代码的作者指定泛型类型的要求来实现这一点。 这样,在代码生成和打印简单错误消息之前,编译可能会失败类型检查。 C++ 泛型(没有概念)的问题是泛型代码_是_泛型类型的规范。 这就是产生难以理解的错误消息的原因。

泛型代码不应该是泛型类型的规范。

@tamird这是 Go 接口类型的一个基本特征,您可以定义一个非接口类型 T,然后定义一个接口类型 I,以便 T 实现 I。请参阅https://golang.org/doc/faq#implements_interface 。 如果 Go 实现了一种泛型形式,而泛型类型 G 只能与明确表示“我可以用来实现 G”的类型 T 一起使用,那将是不一致的。

我对Rust不熟悉,但我不知道有什么语言需要T明确声明它可以用来实现G。你提到的两个要求并不意味着G不能对T强加要求,只是因为 I 对 T 提出了要求。这些要求只是意味着 G 和 T 可以独立编写。 这是泛型非常理想的特性,我无法想象放弃它。

@ianlancetaylor https://doc.rust-lang.org/book/traits.html解释了 Rust 的特征。 虽然我认为它们总体上是一个很好的模型,但它们不适合现在存在的 Go。

@sbunce我也认为概念是答案,你可以看到这个想法散布在最后一个提案之前的各种提案中。 但令人沮丧的是,概念最初是为 C++11 计划的,现在是 2016 年,它们仍然存在争议,而且还没有特别接近被包含在 C++ 语言中。

对于评估方法的任何指导,学术文献是否有价值?

我读过的关于该主题的唯一一篇论文是开发人员是否受益于泛型类型? (抱歉,您可能会用谷歌搜索 pdf 下载),其中有以下内容

因此,对实验的保守解释
是泛型类型可以被认为是一种权衡
积极的文档特征和
负扩展特性。 令人兴奋的部分
该研究表明,它显示了一种情况,即使用
(更强的)静态类型系统对
开发时间,同时预期的好处
fit - 类型错误修复时间的减少 - 没有出现。
我们认为这些任务可能有助于未来的实验
识别类型系统的影响。

我还看到https://github.com/golang/go/issues/15295还引用了轻量级、灵活的面向对象泛型

如果我们要依靠学术界来指导决定,我认为最好先进行文献综述,并且可能尽早决定是否对实证研究与依赖证据的研究进行不同的权衡。

请参阅: http ://dl.acm.org/citation.cfm?id=2738008 由 Barbara Liskov 撰写:

现代面向对象编程语言对泛型编程的支持很笨拙,并且缺乏理想的表达能力。 我们引入了一种表达通用性机制,增加了表达能力并加强了静态检查,同时在常见用例中保持轻量级和简单。 与类型类和概念一样,该机制允许现有类型对类型约束进行追溯建模。 为了表达能力,我们将模型公开为可以明确定义和选择以见证约束的命名结构; 然而,在通用性的常见用途中,类型隐含地见证约束,而无需额外的程序员努力。

我认为他们在那里所做的非常酷 - 如果这是不正确的停止地点,我很抱歉,但我无法在 /proposals 中找到评论的地方,我也没有在这里找到合适的问题。

拥有一个或多个实验性转译器可能会很有趣——从 Go 泛型源代码到 Go 1.xy 源代码编译器。
我的意思是 - 太多的谈话/论据-for-my-opinion,没有人正在编写_try_的源代码来实现_some kind_的Go泛型。

只是为了获得有关 Go 和泛型的知识和经验——看看哪些有效,哪些无效。
如果所有 Go 泛型解决方案都不是很好,那么; Go 没有泛型。

该提案是否还包括对二进制大小和内存占用的影响? 我希望每个具体值类型都会有代码重复,以便编译器优化对它们起作用。 我希望保证具体指针类型不会出现代码重复。

我提供了一个 Pugh 决策矩阵。 我的标准包括明确性影响(来源复杂性、规模)。 我还强制对标准进行排名以确定标准的权重。 当然,您自己的可能会有所不同。 我使用“接口”作为默认替代方案,并将其与“复制/粘贴”泛型、基于模板的泛型(我想到了 D 语言的工作原理之类的东西)以及我称之为运行时实例化风格的泛型进行了比较。 我敢肯定,这是一个巨大的过度简化。 尽管如此,它可能会引发一些关于如何评估选择的想法......这应该是我的谷歌表格的公共链接,在这里

联系@yizhouzhang@andrewcmyers ,这样他们就可以就Go 中的类如泛型表达他们的意见。 听起来这可能是一个很好的匹配:)

我们为 Genus 提出的泛型设计具有静态、模块化的类型检查,不需要预先声明类型实现某些接口,并且具有合理的性能。 如果您正在考虑 Go 的泛型,我肯定会看看它。 从我对 Go 的理解来看,这似乎很合适。

这是不需要 ACM 数字图书馆访问权限的论文的链接:
http://www.cs.cornell.edu/andru/papers/genus/

Genus 主页在这里: http ://www.cs.cornell.edu/projects/genus/

我们尚未公开发布编译器,但我们计划很快发布。

很高兴回答人们的任何问题。

@mandolyte的决策矩阵方面,Genus 得分为 17,并列第一。 不过,我会添加更多标准来评分。 例如,模块化类型检查很重要,就像上面提到的@sbunce等其他方法一样,但基于模板的方案缺少它。 Genus 论文的技术报告在第 34 页有一个更大的表格,比较了各种仿制药设计。

我刚刚浏览了整个Go Generics 总结文档,这是对之前讨论的有用总结。 在我看来,Genus 中的泛型机制不会受到 C++、Java 或 C# 所确定的问题的影响。 与 Java 不同,Genus 泛型被具体化,因此您可以在运行时找出类型。 您还可以在原始类型上进行实例化,并且您不会在您真正不想要的地方获得隐式装箱:T 的数组,其中 T 是原始类型。 类型系统最接近 Haskell 和 Rust——实际上更强大一些,但我认为也很直观。 Genus 目前不支持原始专业化 ala C#,但可以。 在大多数情况下,可以在链接时确定专业化,因此不需要真正的运行时代码生成。

CL https://golang.org/cl/22163提到了这个问题。

一种约束不需要添加新语言概念的泛型类型的方法: https://docs.google.com/document/d/1rX4huWffJ0y1ZjqEpPrDy-kk-m9zWfatgCluGRBQveQ/edit?usp=sharing。

Genus 看起来真的很酷,它显然是艺术的一个重要进步,但我不知道它会如何应用于 Go。 有没有人有关于它如何与 Go 类型系统/哲学集成的草图?

问题是围棋团队正在阻碍尝试。 标题清楚地说明了围棋队的意图。 如果这还不足以阻止所有接受者,那么 ian 提案中要求的如此广泛领域的特性清楚地表明,如果你想要泛型,那么他们就不需要你。 甚至尝试与 go 团队对话都是愚蠢的。 对于那些在 go 中寻找泛型的人,我说破坏语言。 开始新的旅程 - 许多人将跟随。 我已经看到在 forks 中完成了一些很棒的工作。 组织起来,围绕一个事业团结起来

如果有人想尝试基于 Genus 设计对 Go 进行泛型扩展,我们很乐意提供帮助。 我们对 Go 的了解还不足以产生与现有语言协调的设计。 我认为第一步将是一个稻草人设计提案,其中包含已制定的示例。

@andrewcmyers希望@ianlancetaylor能与您合作。 只是有一些例子来看看会有很大帮助。

我已经阅读了 Genus 论文。 就我的理解而言,它对 Java 来说似乎不错,但似乎并不适合 Go。

Go 的一个关键方面是,当您编写 Go 程序时,您编写的大部分内容都是代码。 这与 C++ 和 Java 不同,您编写的更多内容是类型。 Genus 似乎主要与类型有关:您编写约束和模型,而不是代码。 Go 的类型系统非常非常简单。 Genus 的类型系统要复杂得多。

追溯建模的想法,虽然显然对 Java 有用,但似乎根本不适合 Go。 人们已经使用适配器类型将现有类型与接口匹配; 使用泛型时不需要任何进一步的东西。

看到这些想法应用于 Go 会很有趣,但我对结果并不乐观。

我不是 Go 专家,但它的类型系统似乎并不比前泛型 Java 简单。 类型语法以一种不错的方式轻量级,但底层的复杂性似乎大致相同。

在 Genus 中,约束是类型,但模型代码。 模型是适配器,但它们在不添加实际包装层的情况下进行调整。 例如,当您想将整个对象数组调整为新接口时,这非常有用。 追溯建模使您可以将数组视为满足所需接口的对象数组。

如果它在类型理论意义上比(前泛型)Java 更复杂,我不会感到惊讶,即使它在实践中使用起来更简单。

撇开相对复杂性不谈,它们的差异足以让 Genus 无法 1:1 映射。 没有子类型似乎很大。

如果您有兴趣:

我提到的相关哲学/设计差异的最简短摘要包含在以下常见问题条目中:

与大多数语言不同,Go 规范对类型系统的相关属性非常简短和清晰,从https://golang.org/ref/spec#Constants开始,一直到标题为“块”的部分(所有这些少于 11 页)。

与 Java 和 C# 泛型不同,Genus 泛型机制不基于子类型。 另一方面,在我看来,Go 确实有子类型,但是结构子类型。 这也是 Genus 方法的一个很好的匹配,它具有结构性而不是依赖于预先声明的关系。

我不相信 Go 有结构子类型。

虽然基础类型相同的两种类型因此是相同的
可以互相替换, https://play.golang.org/p/cT15aQ-PFr

这不会扩展到共享公共字段子集的两种类型,
https://play.golang.org/p/KrC9_BDXuh。

2016 年 4 月 28 日星期四下午 1:09,Andrew Myers [email protected]
写道:

与 Java 和 C# 泛型不同,Genus 泛型机制不是基于
子类型。 另一方面,在我看来,Go 确实有子类型,
但结构子类型。 这也很适合 Genus 方法,
它具有结构风味而不是依赖于预先声明
关系。


您收到此消息是因为您订阅了此线程。
直接回复此邮件或在 GitHub 上查看
https://github.com/golang/go/issues/15292#issuecomment -215298127

谢谢,我误解了一些关于类型何时实现接口的语言。 实际上,在我看来,带有适度扩展的 Go 接口可以用作 Genus 样式的约束。

这正是我 ping 你的原因,genus 似乎比 Java/C# 之类的泛型更好。

关于专门研究接口类型有一些想法; 例如,_package templates_ 方法“proposals” 1 2就是它的例子。

tl;博士; 具有接口专业化的通用包如下所示:

package set
type E interface { Equal(other E) bool }
type Set struct { items []E }
func (s *Set) Add(item E) { ... }

版本 1. 具有包范围的专业化:

package main
import items set[[E: *Item]]

type Item struct { ... }
func (a *Item) Equal(b *Item) bool { ... }

var xs items.Set
xs.Add(&Item{})

版本 2. 声明范围专业化:

package main
import set

type Item struct { ... }
func (a *Item) Equal(b *Item) bool { ... }

var xs set.Set[[E: *Item]]
xs.Add(&Item{})

包作用域泛型将防止人们严重滥用泛型系统,因为使用仅限于基本算法和数据结构。 它基本上阻止了构建新的语言抽象和功能代码。

声明范围的专业化具有更多的可能性,但代价是更容易被滥用并且更冗长。 但是,功能代码是可能的,例如:

type E interface{}
func Reduce(zero E, items []E, fn func(a, b E) E) E { ... }

Reduce[[E: int]](0, []int{1,2,3,4}, func(a, b int)int { return a + b } )
// there are probably ways to have some aliases (or similar) to make it less verbose
alias ReduceInt Reduce[[E: int]]
func ReduceInt Reduce[[E: int]]

接口特化方法具有有趣的特性:

  • 已经存在的使用接口的包将是可特化的。 例如,我将能够调用sort.Sort[[Interface:MyItems]](...)并在具体类型而不是接口上进行排序(具有内联的潜在收益)。
  • 测试被简化了,我只需要确保通用代码与接口一起工作。
  • 很容易说明它是如何工作的。 即想象[[E: int]]int #$ 替换E的所有声明。

但是,跨包工作时存在冗长问题:

type op
import "set"

type E interface{}
func Union(a, b set.Set[[set.E: E]]) set.Set[[set.E: E]] {
    result := set.New[[set.E: E]]()
    ...
}

_当然,整个事情说起来比实现起来更简单。 在内部可能存在大量问题以及它如何工作的方式。_

_PS,对于那些抱怨泛型进展缓慢的抱怨者,我赞赏 Go 团队将更多时间花在对社区有更大好处的问题上,例如编译器/运行时错误、SSA、GC、http2._

@egonelbre您的观点是,包级泛型将防止“滥用”,这是我认为大多数人忽略的一个非常重要的观点。 再加上它们相对的语义和句法简单性(只有包和导入结构受到影响)使它们对 Go 非常有吸引力。

@andrewcymyers有趣的是,您认为 Go 接口作为 Genus 样式的约束工作。 我原以为他们仍然存在无法用它们表达多类型参数约束的问题。

然而,我刚刚意识到的一件事是,在 Go 中你可以编写一个内联接口。 因此,使用正确的语法,您可以将接口置于所有参数的范围内并捕获多参数约束:

type [V, E] Graph [V interface { Edges() E }, E interface { Endpoints() (V, V) }] ...

我认为接口作为约束的更大问题是方法在 Go 中不像在 Java 中那样普遍。 内置类型没有方法。 没有像 java.lang.Object 中那样的通用方法集。 除非他们特别需要,否则用户通常不会在其类型上定义 Equals 或 HashCode 等方法,因为这些方法不限定类型可用作映射键或任何需要相等的算法。

(Go 中的平等是一个有趣的故事。如果满足某些要求,该语言会给出您的类型“==”(参见 https://golang.org/ref/spec#Logical_operators,搜索“comparable”)。任何带有“ == 可以用作映射键。但是如果您的类型不值得“==”,那么您可以编写任何内容来使其用作映射键。)

因为方法并不普遍,并且因为没有简单的方法来表达内置类型的属性(比如它们使用的运算符),所以我建议使用代码本身作为通用约束机制。 请参阅上面我 4 月 18 日评论中的链接。 这个提议有它的问题,但一个很好的特性是通用数字代码仍然可以使用通常的运算符,而不是繁琐的方法调用。

另一种方法是向缺少方法的类型添加方法。 您可以在现有语言中以比 Java 更轻松的方式执行此操作:

类型 int
func (i Int) Less(j Int) bool { return i < j }

Int 类型“继承”了 int 的所有运算符和其他属性。 尽管您必须在两者之间进行转换才能将 Int 和 int 一起使用,这可能会很痛苦。

属模型可以在这里提供帮助。 但它们必须保持非常简单。 我认为@ianlancetaylor将 Go 描述为编写更多代码、更少类型的描述过于狭隘。 一般原则是 Go 厌恶复杂性。 我们关注 Java 和 C++,并决心永远不会去那里。 (没有冒犯的意思。)

因此,一个类似模型的功能的快速想法是:让用户编写类似上面的 Int 类型,并且在通用实例化中允许“int with Int”,这意味着使用类型 int 但将其视为 Int。 然后没有公开的语言结构称为模型,它的关键字、继承语义等等。 我对模型的理解不够深入,无法知道这是否可行,但它更符合 Go 的精神。

@jba我们当然同意避免复杂性的原则。 “尽可能简单,但不简单。” 基于这些理由,我可能会在 Go 中保留一些 Genus 功能,至少一开始是这样。

Genus 方法的优点之一是它可以流畅地处理内置类型。 回想一下,Java 中的原始类型没有方法,而 Genus 继承了这种行为。 相反,Genus 对待原始类型_好像_他们有一套相当大的方法来满足约束。 哈希表要求它的键可以被哈希和比较,但是所有的原始类型都满足这个约束。 因此,像Map[int, boolean]这样的类型实例化是完全合法的,无需大惊小怪。 无需区分两种类型的整数(int vs Int)来实现这一点。 但是,如果int没有为某些用途配备足够的操作,我们将使用几乎与上面使用Int完全相同的模型。

另一件值得一提的是 Genus 中“自然模型”的概念。 您通常不必声明模型以使用泛型类型:如果类型参数满足约束,则会自动生成自然模型。 我们的经验是,这是通常的情况; 通常不需要声明显式的命名模型。 但是如果需要一个模型——例如,如果你想以非标准的方式散列整数——那么语法类似于你建议的: Map[int with fancyHash, boolean] 。 我认为 Genus 在正常用例中在语法上很轻,但在需要时具有储备功能。

@egonelbre您在这里提出的建议看起来像Scala 支持的虚拟类型。 Kresten Krab Thorup 有一篇 ECOOP'97 论文,“Genericity in Java with virtual types”,探讨了这个方向。 我们还在我们的工作中开发了虚拟类型和虚拟类的机制(“J&:可扩展软件组合的嵌套交集”,OOPSLA'06)。

由于字面量初始化在 Go 中很普遍,我不得不想知道函数字面量会是什么样子。 我怀疑处理这个问题的代码主要存在于 Go 生成、修复和重命名中。也许它会激励某人:-)

// (通用) func 类型定义
类型 Sum64 func (X, Y) float64 {
返回 float64(X) + float64(Y)
}

// 实例化一个,位置
我:= 42
变量 j uint = 86
总和 := &Sum64{i, j}

// 通过命名参数类型实例化一个
总和 := &Sum64{ X: int, Y: uint}

// 现在使用它...
result := sum(i, j) // 结果是 128

伊恩的提议要求太多。 我们不可能一次开发所有功能,它会以未完成的状态存在数月之久。

与此同时,未完成的项目在完成之前不能称为官方 Go 语言,因为这将有可能导致生态系统分裂。

所以问题是如何计划这个。

该项目的很大一部分将是开发参考语料库。
开发实际的通用集合、算法和其他东西,我们都同意它们是惯用的,同时使用新的 go 2.0 特性

可能的语法?

// Module defining generic type
module list(type t)

type node struct {
    next *node
    data t
}
// Module using generic type:
import (
    intlist "module/path/to/list" (int)
    funclist "module/path/to/list" (func (int) int)
)

l := intlist.New()
l.Insert(5)

@md2perpe ,语法不是这个问题的难点。 事实上,它是迄今为止最简单的。 请参阅上面的讨论和链接文件。

@md2perpe我们已经讨论了将整个包(“模块”)参数化作为内部通用性的一种方式——它似乎确实是一种减少语法开销的方法。 但它还有其他问题; 例如,尚不清楚如何使用非包级别的类型对其进行参数化。 但这个想法可能仍然值得详细探索。

我想分享一个观点:在平行宇宙中,所有 Go 函数签名一直被限制为仅提及接口类型,而不是今天对泛型的需求,有一种方法可以避免与接口值相关联的间接性。 想想你将如何解决这个问题(不改变语言)。 我有一些想法。

@thwd那么库作者是否会继续使用接口,但没有今天需要的类型切换和类型断言。 库用户是否会简单地传递具体类型,就好像库会按原样使用类型一样......然后编译器会调和这两者? 如果它不能说明为什么? (例如在库中使用了模运算符,但用户提供了一部分内容。

我接近了吗? :-)

@mandolyte是的! 让我们交换电子邮件以免污染此线程。 您可以通过“me at thwd dot me”联系我。 其他任何可能感兴趣的阅读本文的人; 给我发一封电子邮件,我会把你添加到线程中。

对于type systemcollection library来说,这是一个很棒的功能。
一种潜在的语法:

type Element<T> struct {
    prev, next *Element<T>
    list *List<T>
    value T
}
type List<E> struct {
    root Element<E>
    len int
}

interface

type Collection<E> interface {
    Size() int
    Add(e E) bool
}

super typetype implement

func contain(l List<parent E>, e E) bool
<V> func (c Collection<child E>)Map(fn func(e E) V) Collection

以上在java中又名:

boolean contain(List<? super E>, E e)
<V> Collection Map(Function<? extend E, V> mapFunc);

@leaxoy 如前所述,语法不是这里的难点。 见上面的讨论。

请注意,接口的成本是令人难以置信的巨大。

请详细说明为什么您认为接口的成本“令人难以置信”
大。
它不应该比 C++ 的非专用虚拟调用差。

@minux我不能说性能成本,而是与代码质量有关。 interface{}无法在编译时验证,但泛型可以。 在我看来,在大多数情况下,这比使用interface{}的性能问题更重要。

@xoviat

这确实没有缺点,因为为此所需的处理不会减慢编译器的速度。

有(至少)两个缺点。

一是增加了链接器的工作量:如果两种类型的特化导致相同的底层机器代码,我们不想编译和链接该代码的两个副本。

另一个是参数化包的表现力不如参数化方法。 (有关详细信息,请参阅第一条评论中链接的提案。)

超类型是个好主意吗?

func getAddFunc (aType type) func(aType, aType) aType {
    return func(a, b aType) aType {
        return a+b
    }
}

超类型是个好主意吗?

您在这里描述的只是类型参数化ala C++(即模板)。 它不会以模块化方式进行类型检查,因为无法从给定的信息中知道类型aType具有 + 操作。 CLU、Haskell、Java、Genus 中的约束类型参数化是解决方案。

@golang101我有一个详细的建议。 我会发送一个 CL 将其添加到列表中,但它不太可能被采用。

CL https://golang.org/cl/38731提到了这个问题。

@andrewcmyers

它不会以模块化方式进行类型检查,因为无法从给定的信息中知道类型aType具有 + 操作。

当然有。 该约束隐含在函数的定义中,并且该形式的约束可以传播到getAddFunc的所有(传递)编译时调用者。

约束不是 Go _type_ 的一部分——也就是说,它不能在语言的运行时部分的类型系统中编码——但这并不意味着它不能以模块化的方式进行评估。

将我的提案添加为2016-09-compile-time-functions.md

我不期望它会被采用,但它至少可以作为一个有趣的参考点。

@bcmills我觉得编译时函数是一个强大的想法,除了对泛型的任何考虑。 例如,我编写了一个需要 popcount 的数独求解器。 为了加快速度,我预先计算了各种可能值的 popcounts 并将其存储为Go source 。 这是可以用go:generate做的事情。 但是如果有一个编译时函数,那么该查找表也可以在编译时计算,从而避免机器生成的代码必须提交到 repo。 一般来说,任何类型的可记忆数学函数都非常适合带有编译时函数的预制查找表。

更具推测性的是,人们可能还希望,例如,从规范源下载 protobuf 定义并在编译时使用它来构建类型。 但也许在编译时允许这样做太多了?

我觉得编译时函数既太强大又太弱:它们太灵活并且可能以奇怪的方式出错/减慢 C++ 模板的编译方式,但另一方面它们太静态且难以适应一流的功能。

对于第二部分,我看不出有一种方法可以制作类似“处理特定类型切片并返回一个元素的函数切片”,或者采用特殊语法[]func<T>([]T) T ,其中基本上在每种静态类型的函数式语言中都很容易做到。 真正需要的是能够采用参数类型的,而不是一些源代码级别的代码生成。

@bunsim

对于第二部分,我看不到一种方法可以制作诸如“处理特定类型切片并返回一个元素的函数切片”之类的东西,

如果您谈论的是单个类型参数,那么在我的提案中将这样写:

const func SliceOfSelectors(T gotype) gotype { return []func([]T)T (type) }

如果您在谈论混合类型参数和值参数,不,我的建议不允许这样做:编译时函数的一部分是能够对未装箱的值进行操作,以及运行时参数的类型我认为您所描述的几乎需要对值进行装箱。

是的,但在我看来,在保持类型安全的同时,应该允许那种需要装箱的事情,也许使用指示“装箱”的特殊语法。 添加“泛型”的很大一部分实际上是为了避免interface{}的类型不安全,即使interface{}的开销是不可避免的。 (也许只允许某些具有“已经”装箱的指针和接口类型的参数类型构造?Java 的Integer等装箱对象并不是一个坏主意,尽管值类型的切片很棘手)

我只是觉得编译时函数非常类似于 C++,对于像我这样期望 Go2 拥有基于健全类型理论而不是基于操纵编写的源代码片段的现代参数类型系统的人来说,这将是非常令人失望的在没有泛型的语言中。

@bcmills
您提出的建议不会是模块化的。 如果模块 A 使用模块 B,模块 B 使用模块 C,模块 C 使用模块 D,则对 D 中类型参数的使用方式的更改可能需要一直传播回 A,即使 A 的实现者不知道 D是在系统中。 模块系统提供的松散耦合会被削弱,软件会更加脆弱。 这是 C++ 模板的问题之一。

另一方面,如果类型签名确实捕获了对类型参数的要求,例如在 CLU、ML、Haskell 或 Genus 等语言中,则可以编译一个模块,而无需访问它所依赖的模块的内部结构。

@bunsim

添加“泛型”的很大一部分实际上是为了避免 interface{} 的类型不安全,即使 interface{} 的开销是不可避免的。

“无法避免”是相对的。 请注意,拳击的开销是 Russ 2009 年发布的第 3 点(https://research.swtch.com/generic)。

期望 Go2 拥有一个基于健全类型理论的现代参数类型系统,而不是基于操纵源代码片段的 hack

一个好的“声音类型理论”是描述性的,而不是规定性的。 我的建议特别借鉴了二阶 lambda 演算(沿着 System F 的行),其中gotype代表type类型,整个一阶类型系统被提升到第二个-order(“编译时”)类型。

它还与 CMU 的 Davies、Pfenning 等人的模态类型理论工作有关。 对于一些背景知识,我将从A Modal Analysis of Staged ComputationModal Types as Staging Specifications for Run-time Code Generation 开始

确实,我提议中的基本类型理论没有学术文献中那么正式,但这并不意味着它不存在。

@andrewcmyers

如果模块 A 使用模块 B,模块 B 使用模块 C,模块 C 使用模块 D,则对 D 中类型参数的使用方式的更改可能需要一直传播回 A,即使 A 的实现者不知道 D是在系统中。

这在今天的 Go 中已经是正确的:如果你仔细观察,你会注意到编译器为给定的 Go 包生成的目标文件包含有关影响导出 API 的传递依赖项部分的信息。

模块系统提供的松散耦合会被削弱,软件会更加脆弱。

我听说过同样的论点曾经主张在 Go API 中导出interface类型而不是具体类型,而相反的情况更常见:过早的抽象过度约束了类型并阻碍了 API 的扩展。 (对于一个这样的例子,请参阅#19584。)如果你想依赖这一论点,我认为你需要提供一些具体的例子。

这是 C++ 模板的问题之一。

在我看来,C++ 模板的主要问题是(没有特别的顺序):

  • 过多的句法歧义。
    一个。 类型名称和值名称之间的歧义。
    湾。 对运算符重载的支持过于广泛,导致从运算符使用中推断约束的能力减弱。
  • 过度依赖元编程的重载解决方案(或者,等效地,元编程支持的临时演变)。
    一个。 特别是 wrt 参考折叠规则。
  • SFINAE 原则的过于广泛的应用,导致非常难以传播的约束和类型定义中太多的隐式条件,导致非常困难的错误报告。
  • 过度使用标记粘贴和文本包含(C 预处理器)而不是 AST 替换和高阶编译工件(幸运的是,模块似乎至少部分解决了这些问题)。
  • C++ 编译器缺乏良好的引导语言,导致长期存在的编译器沿袭(例如 GCC 工具链)中的错误报告很差。
  • 由于将运算符集映射到不同名称的“概念”(而不是将运算符本身视为基本约束)而产生的名称加倍(有时是乘法)。

十年来,我断断续续地使用 C++ 进行编码,我很高兴详细讨论 C++ 的缺陷,但程序依赖关系是可传递的这一事实从未远离我的抱怨清单。

另一方面,是否需要更新一个 O(N) 依赖链只是为了向模块 A 中的类型添加一个方法并能够在模块 D 中使用它? 这就是经常让我慢下来的问题。 在参数化和松散耦合冲突的地方,我会选择参数化。

尽管如此,我仍然坚信元编程和参数多态性应该分开,而 C++ 对它们的混淆是 C++ 模板令人讨厌的根本原因。 简而言之,C++ 试图在本质上使用类固醇上的宏来实现类型理论的想法,这是非常有问题的,因为程序员喜欢将模板视为真正的参数多态性并且会受到意外行为的打击。 编译时函数是元编程和替换go generate的好主意,但我不认为它应该是进行通用编程的幸运方式。

“真正的”参数多态有助于松散耦合,不应与之冲突。 它还应该与类型系统的其余部分紧密集成; 例如,它可能应该集成到当前的接口系统中,以便接口类型的许多用法可以重写为:

func <T io.Reader> ReadAll(in T)

这应该避免接口开销(如 Rust 的用法),尽管在这种情况下它不是很有用。

一个更好的例子可能是sort包,你可以有类似的东西

func <T Comparable> Sort(slice []T)

其中Comparable只是类型可以实现的一个很好的旧接口。 然后可以在实现Comparable Sort而无需将它们装箱到接口类型中。

@bcmills在我看来,不受类型系统约束的传递依赖是您对 C++ 的一些抱怨的核心。 如果您控制模块 A、B、C 和 D,传递依赖就不是什么大问题。通常,您正在开发模块 A,并且可能只是微弱地意识到模块 D 在那里,反之,D 的开发人员可能不知道 A。如果模块 D 现在没有对 D 中可见的声明进行任何更改,就开始在类型参数上使用一些新运算符 - 或者只是将该类型参数用作具有自己的新模块 E 的类型参数隐式约束——这些约束将渗透到所有可能没有使用满足约束的类型参数的客户端。 没有什么告诉开发人员 D 他们正在吹它。 实际上,您已经获得了一种全局类型推断,其中包含调试的所有困难。

我相信我们在 Genus [ PLDI'15 ] 中采用的方法要好得多。 类型参数具有显式但轻量级的约束(我同意您关于支持操作约束的观点;CLU 早在 1977 年就展示了如何正确地做到这一点)。 属类型检查是完全模块化的。 通用代码可以只编译一次以优化代码空间,也可以专门用于特定类型的参数以获得良好的性能。

@andrewcmyers

如果模块 D 现在不对 D 中可见的声明进行任何更改,开始在类型参数上使用一些新运算符 […] [clients] 可能没有使用满足约束的类型参数。 没有什么告诉开发人员 D 他们正在吹它。

当然,但这对于 Go 中的许多隐式约束已经是正确的,独立于任何通用编程机制。

例如,一个函数可能会接收一个接口类型的参数,并最初按顺序调用它的方法。 如果该函数稍后更改为并发调用这些方法(通过产生额外的 goroutines),则约束“必须对并发使用是安全的”不会反映在类型系统中。

类似地,今天的 Go 类型系统没有指定对变量生命周期的约束: io.Writer的一些实现错误地假设它们可以保留对传入切片的引用并在以后读取它(例如,通过异步执行实际写入在后台 goroutine 中),但是如果Write的调用者尝试为后续的Write重用相同的支持切片,则会导致数据竞争。

或者使用类型开关的函数可能会采用不同路径的方法将其添加到开关中的类型之一。

或者,如果生成错误的函数改变了它报告该条件的方式,则检查特定错误代码的函数可能会中断。 (例如,参见 https://github.com/golang/go/issues/19647。)

或者,如果添加或删除错误的包装器,检查特定错误类型的函数可能会中断(就像 Go 1.5 中的标准net包中发生的那样)。

或者 API 中公开的通道上的缓冲可能会发生变化,从而引入死锁和/或竞争。

...等等。

Go 在这方面并不罕见:隐式约束在现实世界的程序中无处不在。


如果您尝试在显式注释中捕获所有相关约束,那么您最终会走向两个方向之一。

在一个方向上,您构建了一个复杂的、极其全面的依赖类型和注释系统,而这些注释最终概括了它们注释的大部分代码。 我希望你能清楚地看到,这个方向与 Go 语言的其余部分的设计完全不符:Go 倾向于规范的简单性和代码的简洁性,而不是全面的静态类型。

另一方面,显式注释将仅涵盖给定 API 的相关约束的子集。 现在注释提供了一种错误的安全感:代码仍然会由于隐式约束的变化而中断,但是显式约束的存在会误导开发人员认为任何“类型安全”的变化也能保持兼容性。


我不清楚为什么需要通过显式源代码注释来实现那种 API 稳定性:您所描述的那种 API 稳定性也可以通过源代码分析来实现(代码中的冗余更少)。 例如,您可以想象让api工具分析代码并输出比语言的正式类型系统所能表达的更丰富的约束集,并为guru工具提供能够查询任何给定 API 函数、方法或参数的计算约束集。

@bcmills你不是让完美成为善的敌人吗? 是的,存在难以在类型系统中捕获的隐式约束。 (并且良好的模块化设计会在可行的情况下避免引入此类隐式约束。)如果能够进行包罗万象的分析,可以静态检查您想要检查的所有属性,并为程序员提供清晰、无误导性的解释,那就太好了。正在犯错误。 即使最近在自动错误诊断和本地化方面取得了进展,我也没有屏住呼吸。 一方面,分析工具只能分析你给他们的代码。 开发人员并不总是可以访问可能与他们的代码相关联的所有代码。

那么在类型系统中哪里有容易捕获的约束,为什么不让程序员有能力把它们写下来呢? 我们拥有 40 年的静态约束类型参数编程经验。 这是一个简单、直观的静态注释,效果显着。

一旦您开始构建将软件模块分层的大型软件,您就会开始想要编写注释来解释这些隐式约束。 假设有一种很好的、​​可检查的方式来表达它们,为什么不让编译器参与进来,这样它可以帮助你呢?

我注意到您的一些其他隐式约束的示例涉及错误处理。 我认为我们对异常的轻量级静态检查 [ PLDI 2016 ] 将解决这些示例。

@andrewcmyers

那么在类型系统中哪里有容易捕获的约束,为什么不让程序员有能力把它们写下来呢?
[…]
一旦您开始构建将软件模块分层的大型软件,您就会开始想要编写注释来解释这些隐式约束。 假设有一种很好的、​​可检查的方式来表达它们,为什么不让编译器参与进来,这样它可以帮助你呢?

我实际上完全同意这一点,并且我经常在内存管理方面使用类似的论点。 (如果您无论如何都必须记录关于别名和数据保留的不变量,为什么不在编译时强制执行这些不变量?)

但我会把这个论点更进一步:反过来也成立! 如果您_不_需要为约束编写注释(因为在上下文中对使用代码的人来说是显而易见的),为什么需要为编译器编写该注释? 不管我个人的喜好如何,Go 对垃圾收集和零值的使用清楚地表明了“不要求程序员声明明显的不变量”的偏见。 Genus 风格的建模可能会表达许多将在注释中表达的约束,但它在省略也会在注释中省略的约束方面表现如何?

在我看来,Genus 风格的模型不仅仅是注释:它们实际上在某些情况下改变了代码的语义,它们不仅仅是约束它。 现在我们将有两种不同的机制——接口和类型模型——用于参数化行为。 这将代表 Go 语言的一个重大转变:随着时间的推移,我们已经发现了一些接口的最佳实践(例如“在消费者端定义接口”),而且这种体验是否会转化为如此完全不同的系统并不明显,甚至忽略 Go 1 的兼容性。

此外,Go 的优秀特性之一是它的规范可以在一个下午阅读(并在很大程度上理解)。 对我来说,可以将 Genus 风格的约束系统添加到 Go 语言中而不会使它变得非常复杂,这对我来说并不明显——我很想看到一个关于更改规范的具体建议。

这是“元编程”的一个有趣的数据点。 syncatomic包中的某些类型(即atomic.Valuesync.Map )支持CompareAndSwap方法会很好,但这些仅适用于碰巧具有可比性的类型。 atomic.Valuesync.Map API 的其余部分在没有这些方法的情况下仍然有用,因此对于该用例,我们要么需要像 SFINAE(或其他类型的条件定义 API)这样的东西,要么不得不放弃回到更复杂的类型层次结构。

我想放弃这种使用原住民音节的创造性语法思想

@bcmills你能解释一下这三点吗?

  1. 类型名称和值名称之间的歧义。
  2. 对运算符重载的过度支持
    3.过度依赖重载决议进行元编程

@mahdix当然。

  1. 类型名称和值名称之间的歧义。

这篇文章给出了很好的介绍。 为了解析 C++ 程序,您必须知道哪些名称是类型,哪些是值。 当您解析一个模板化的 C++ 程序时,您没有该信息可用于模板参数的成员。

对于复合文字,Go 中也出现了类似的问题,但歧义在于值和字段名称之间,而不是值和类型之间。 在这个 Go 代码中:

const a = someValue
x := T{a: b}

a是文字字段名称,还是用作映射键或数组索引的常量a

  1. 对运算符重载的过度支持

依赖于参数的查找是一个很好的起点。 C++ 中运算符的重载可以作为接收器类型上的方法或作为多个命名空间中的任何一个中的自由函数出现,解决这些重载的规则非常复杂。

有很多方法可以避免这种复杂性,但最简单的(就像 Go 当前所做的那样)是完全禁止运算符重载。

  1. 过度依赖重载解决方案进行元编程

<type_traits>库是一个很好的起点。 查看您友好社区libc++中的实现,了解重载解决方案如何发挥作用。

如果 Go 曾经支持元编程(即使那是非常值得怀疑的),我不希望它涉及重载决议作为保护条件定义的基本操作。

@bcmills
由于我从未使用过 C++,您能否阐明通过实现预定义的“接口”来实现运算符重载在复杂性方面的位置。 Python 和 Kotlin 就是这样的例子。

我认为 ADL 本身是 C++ 模板的一个大问题,几乎没有提到,因为它们迫使编译器将所有名称的解析延迟到实例化时间,并且可能导致非常微妙的错误,部分原因是“理想”和“ lazy" 编译器在这里的行为不同,标准允许这样做。 它支持运算符重载的事实并不是迄今为止最糟糕的部分。

这个建议是基于Templates的,一个系统做宏扩展会不会不够用? 我不是在谈论go generate或像 gotemplate 这样的项目。 我说的更多是这样的:

macro MacroFoo(stmt ast.Statement) {
    ....
}

宏可以减少样板和反射的使用。

我认为 C++ 是一个很好的例子,泛型不应该基于模板或宏。 特别是考虑到 Go 有像匿名函数这样的东西,除了作为优化之外,这些东西在编译时真的不能“实例化”。

@samadadi您可以在不说“你们这些人有什么问题”的情况下表达您的观点。 话虽如此,复杂性的论点已经被多次提出。

Go 并不是第一种试图通过省略对参数多态性(泛型)的支持来实现简单性的语言,尽管该功能在过去 40 年中变得越来越重要——根据我的经验,它是第二学期编程课程的主要内容。

语言中没有该功能的问题在于,程序员最终会求助于更糟糕的解决方法。 例如,Go 程序员经常编写代码模板,这些代码模板经过宏扩展以生成各种所需类型的“真实”代码。 但是真正的编程语言是你输入的,而不是编译器看到的。 所以这个策略实际上意味着你正在使用一种(不再是标准的)语言,它具有 C++ 模板的所有脆弱性和代码膨胀。

https://blog.golang.org/toward-go2所述,我们需要提供“经验报告”,以便确定需求和设计目标。 你能花几分钟记录下你观察到的宏观案例吗?

请将此错误保持在主题和民事上。 再次, https://golang.org/wiki/NoMeToo。 如果您有独特和建设性的信息要添加,请仅发表评论。

@mandolyte在网络上很容易找到详细的解释,提倡代码生成作为(部分)泛型的替代品:
https://appliedgo.net/generics/
https://www.calhoun.io/using-code-generation-to-survive-without-generics-in-go/
http://blog.ralch.com/tutorial/golang-code-generation-and-generics/

显然有很多人采用这种方法。

@andrewcmyers ,使用代码生成 BUT 时有一些限制和方便的注意事项。
一般来说 - 如果你认为这种方法是最好的/足够好的,我认为在 go 工具链中允许一些类似的生成的努力将是一种祝福。

  • 在这种情况下,编译器优化可能是一个挑战,但运行时将保持一致,并且可以保留代码维护、用户体验(简单性...)、标准最佳实践和统一代码标准。
    此外 - 所有工具链都将保持不变,除了调试工具(分析器、步进调试器等)会看到不是由开发人员编写的代码行,但这有点像在调试时进入 ASM 代码 - 只是它是一个可读的代码:)。

缺点 - go tool chain 中这种方法没有先例(据我所知)。

总结一下——考虑将代码生成作为构建过程的一部分,它不应该太复杂,相当安全,运行时优化,可以保持语言的简单性和非常小的变化。

恕我直言:它的妥协很容易实现,价格低廉。

需要明确的是,我不认为宏风格的代码生成,无论是使用 gen、cpp、gofmt -r 还是其他宏/模板工具完成,即使标准化也是解决泛型问题的好方法。 它与 C++ 模板存在相同的问题:代码膨胀、缺乏模块化类型检查和难以调试。 当您开始根据其他通用代码构建通用代码时,情况会变得更糟,这是很自然的。 在我看来,优势是有限的:它可以让 Go 编译器编写者的生活相对简单,并且它确实可以生成高效的代码——除非存在指令缓存压力,这是现代软件中常见的情况!

我认为重点在于代码生成用于替代
泛型,因此泛型应该寻求解决大多数这些用例。

2017 年 7 月 26 日星期三 22:41 Andrew Myers, notifications @github.com 写道:

需要明确的是,我不考虑宏样式的代码生成,是否完成
使用 gen、cpp、gofmt -r 或其他宏/模板工具,成为一个好的
即使标准化也能解决泛型问题。 它具有相同的
作为 C++ 模板的问题:代码膨胀、缺乏模块化类型检查,以及
调试困难。 当你开始时它会变得更糟,这是自然的,建筑
就其他通用代码而言的通用代码。 在我看来,优点是
有限:它会让 Go 编译器编写者的生活相对简单
它确实产生了高效的代码——除非有指令缓存
压力,现代软件中的常见情况!


您收到此消息是因为您发表了评论。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/15292#issuecomment-318242016或静音
线程
https://github.com/notifications/unsubscribe-auth/AT4HVb2SPMpe5dlEDUQeadIRKPaB74zoks5sR_jSgaJpZM4IG-xv
.

毫无疑问,代码生成不是真正的解决方案,即使包含一些语言支持以使外观和感觉成为“语言的一部分”

我的观点是它非常划算。

顺便说一句,如果您查看一些代码生成替代品,您可以很容易地看到如果语言为它们提供了更好的工具,它们如何变得更具可读性、更快,并且缺少一些错误的概念(例如迭代指针数组与值)为了这。

也许这是短期内解决的更好途径,感觉不像是补丁:
在考虑“最好的泛型支持也将是惯用的”(我相信上面的一些实现需要数年才能完成完全集成)之前,实现一些无论如何都需要的“语言”支持的功能集(比如内置结构深拷贝)将使这些代码生成解决方案更有用。

在阅读了@bcmills@ianlancetaylor的泛型提案后,我做了以下观察:

编译时函数和第一类类型

我喜欢编译时评估的想法,但我没有看到将其限制为纯函数的好处。 该提案引入了内置gotype ,但将其使用限制为 const 函数和函数范围内定义的任何数据类型。 从库用户的角度来看,实例化仅限于像“New”这样的构造函数,并导致像这样的函数签名:

const func New(K, V gotype, hashfn Hashfn(K), eqfn Eqfn(K)) func()*Hashmap(K, V, hashfn, eqfn)

此处的返回类型不能拆分为函数类型,因为我们仅限于纯函数。 此外,签名在签名本身中定义了两个新的“类型”(K 和 V),这意味着为了解析单个参数,我们必须解析整个参数列表。 这对于编译器来说很好,但我想知道是否会增加包的公共 API 的复杂性。

Go 中的类型参数

参数化类型允许通用编程的大多数用例,例如定义通用数据结构和对不同数据类型的操作的能力。 该提案详尽列出了产生更好的编译错误、更快的编译时间和更小的二进制文件所需的类型检查器的增强功能。

在“类型检查器”部分下,该提案还列出了一些有用的类型限制以加快进程,如“可索引”、“可比较”、“可调用”、“复合”等……我不明白的是为什么不允许用户指定自己的类型限制? 该提案指出

在参数化函数中如何使用参数化类型没有任何限制。

但是,如果标识符有更多与之相关的约束,那不会有帮助编译器的效果吗? 考虑:

HashMap[Anything,Anything] // Compiler must always compare the implementation and usages to make sure this is valid.

对比

HashMap[Comparable,Anything] // Compiler can first filter out instantiations for incomparable types before running an exhaustive check.

将类型约束与类型参数分开并允许用户定义约束也可以提高可读性,使通用包更易于理解。 有趣的是,如果这些规则由用户明确定义,那么提案末尾列出的关于类型推断规则复杂性的缺陷实际上可以得到缓解。

@smasher164

我喜欢编译时评估的想法,但我没有看到将其限制为纯函数的好处。

好处是它使单独编译成为可能。 如果编译时函数可以修改全局状态,则编译器必须使该状态可用,或者以链接器可以在链接时对它们进行排序的方式记录对其的编辑。 如果编译时函数可以修改本地状态,那么我们需要一些方法来跟踪哪个状态是本地的还是全局的。 两者都增加了复杂性,而且两者都不能提供足够的好处来抵消它。

@smasher164

我不明白的是为什么不允许用户指定自己的类型限制?

该提案中的类型限制对应于语言语法中的操作。 这减少了新特性的表面积:不需要为约束类型指定额外的语法,因为所有的语法约束都可以从使用中推断出来。

如果标识符有更多与之相关的约束,那不会有帮助编译器的效果吗?

该语言应该为其用户设计,而不是为编译器编写者设计。

不需要为约束类型指定额外的语法,因为所有的语法约束都可以从使用中推断出来。

这是 C++ 失败的路线。 它需要全局程序分析来识别相关用法。 程序员无法以模块化方式推理代码,错误消息冗长且难以理解。

指定所需的操作可以如此简单和轻量级。 例如,参见 CLU (1977)。

@andrewcmyers

它需要全局程序分析来识别相关用法。 程序员不能以模块化方式推理代码,

那是使用“模块化”的特定定义,我认为它不像您想象的那样普遍。 根据 2013 年的提议,每个函数或类型都将具有一组明确的约束,这些约束是从导入的包中自下而上推断的,与非参数函数的运行时(和运行时约束)派生自下而上的方式完全相同。从今天的调用链中上升。

您大概可以使用guru或类似工具查询推断的约束,并且它可以使用导出的包元数据中的本地信息来回答这些查询。

并且错误消息冗长且难以理解。

我们有几个示例(GCC 和 MSVC)证明天真生成的错误消息是难以理解的。 我认为假设隐式约束的错误消息本质上是不好的,这有点牵强。

我认为推断约束的最大缺点是它们可以很容易地以一种在不完全理解它的情况下引入约束的方式使用类型。 在最好的情况下,这只是意味着您的用户可能会遇到意外的编译时故障,但在最坏的情况下,这意味着您可以通过无意中引入新的约束来破坏消费者的包。 明确指定的约束可以避免这种情况。

我个人也不觉得显式约束与现有的 Go 方法不一致,因为接口是显式的运行时类型约束,尽管它们的表达能力有限。

我们有几个示例(GCC 和 MSVC)证明天真生成的错误消息是难以理解的。 我认为假设隐式约束的错误消息本质上是不好的,这有点牵强。

非本地类型推断的编译器列表 - 这是您建议的 - 导致错误消息的编译器列表比这长得多。 它包括 SML、OCaml 和 GHC,其中已经付出了很多努力来改进它们的错误消息,并且至少有一些显式的模块结构可以提供帮助。 您可能会做得更好,如果您提出的方案能够针对良好的错误消息提出算法,那么您将获得一份不错的出版物。 作为该算法的起点,您可能会发现我们关于错误定位的 POPL 2014 和 PLDI 2015 论文很有用。 它们或多或少是最先进的。

因为所有的句法约束都可以从用法中推断出来。

这不会限制可类型检查的通用程序的广度吗? 例如,请注意 type-params 提案没有指定“Iterable”约束。 在当前语言中,这将对应于切片或通道,但复合类型(例如链表)不一定满足这些要求。 定义一个类似的接口

type Iterable[T] interface {
    Next() T
}

有助于链表的情况,但现在必须扩展内置切片和通道类型以满足此接口。

对于用户、包作者和编译器实现者来说,“我接受所有类型的集合,即 Iterables、切片或通道”的约束似乎是双赢的局面。 我要说明的一点是,约束是语法有效程序的超集,从语言的角度来看,有些可能没有意义,但仅从 API 的角度来看。

该语言应该为其用户设计,而不是为编译器编写者设计。

我同意,但也许我应该换个说法。 提高编译器效率可能是用户定义约束的副作用。 主要的好处是可读性,因为无论如何用户都比编译器更了解他们的 API 行为。 这里的权衡是通用程序必须稍微更明确地说明它们接受的内容。

如果不是

type Iterable[T] interface {
    Next() T
}

我们将“接口”的概念从“约束”中分离出来。 那么我们可能有

type T generic

type Iterable class {
    Next() T
}

其中“类”表示 Haskell 风格的类型类,而不是 Java 风格的类。

将“类型类”与“接口”分开可能有助于消除这两个想法的一些非正交性。 然后Sortable (忽略 sort.Interface)可能看起来像:

type T generic

type Comparable class {
    Less(a, b T) bool
}

type Sortable class {
    Next() Comparable
}

这是@andrewcmyersGenus中“类型类和概念”部分的一些反馈及其对 Go 的适用性。

本节解决类型类和概念的限制,说明

首先,必须唯一见证约束满足

我不确定我是否理解这个限制。 将约束绑定到单独的标识符不会阻止它对于给定类型是唯一的吗? 在我看来,Genus 中的“where”子句本质上是从给定约束构造类型/约束,但这似乎类似于从给定类型实例化变量。 这种方式的约束类似于kind

这是适用于 Go 的约束定义的显着简化:

kind Any interface{} // accepts any type that satisfies interface{}.
type T Any // Declare a type of Any kind. Also binds it to an identifier.
kind Eq T == T // accepts any type for which equality is defined.

因此,地图声明将显示为:

type Map[K Eq, V Any] struct {
}

在 Genus 中,它可能看起来像:

type Map[K, V] where Eq[K], Any[V] struct {
}

在现有的 Type-Params 提案中,它看起来像:

type Map[K,V] struct {
}

我想我们都同意,允许约束利用现有的类型系统既可以消除语言特性之间的重叠,又可以使新特性易于理解。

其次,他们的模型定义了如何适应单一类型,而在具有子类型的语言中,每个适应的类型通常代表它的所有子类型。

这种限制似乎与 Go 不太相关,因为该语言已经在命名/未命名类型和重叠接口之间具有良好的转换规则。

给定的示例提出了模型作为解决方案,这对于 Go 来说似乎是一个有用但不是必需的特性。 例如,如果一个库期望一个类型实现 http.Handler,并且用户想要根据上下文不同的行为,那么编写适配器很简单:

type handleFunc func(http.ResponseWriter, *http.Request)
func (f handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { f(w,r) }

事实上,这就是标准库所做的。

@smasher164

首先,必须唯一见证约束满足
我不确定我是否理解这个限制。 将约束绑定到单独的标识符不会阻止 > 它对于给定类型是唯一的吗?

这个想法是,在 Genus 中,与 Haskell 不同,您可以通过不止一种方式满足相同类型的相同约束。 例如,如果你有一个HashSet[T] ,你可以写HashSet[String]以通常的方式散列字符串,但HashSet[String with CaseInsens]来散列和比较字符串与CaseInsens模型,它可能以不区分大小写的方式处理字符串。 Genus 实际上区分了这两种类型。 这对 Go 来说可能有点过头了。 即使类型系统不跟踪它,能够覆盖类型提供的默认操作似乎仍然很重要。

kind Any interface{} // 接受任何满足 interface{} 的类型。
type T Any // 声明任何类型的类型。 还将它绑定到一个标识符。
kind Eq T == T // 接受定义了相等性的任何类型。
类型 Map[K Eq, V Any] struct { ...
}

Genus 中的道德等价物是:

constraint Any[T] {}
// Just use Any as if it were a type
constraint Eq[K] {
   boolean equals(K);
}
class Map[K, V] where Eq[K] { ... }

在 Familia 我们只会写:

interface Eq {
    boolean equals(This);
}
class Map[K where Eq, V] { ... }

编辑:撤回这一点以支持基于反射的解决方案,如 #4146 中所述。我在下面描述的基于泛型的解决方案在组合数量上呈线性增长。 虽然基于反射的解决方案总是存在性能障碍,但它可以在运行时自我优化,因此无论组合的数量如何,障碍都是恒定的。

这不是提案,而是设计提案时要考虑的潜在用例。

今天的 Go 代码中有两件事很常见

  • 包装接口值以提供附加功能(为框架包装http.ResponseWriter
  • 具有有时接口值具有的可选方法(例如Temporary() bool上的net.Error

这些都是好的和有用的,但它们不会混合。 一旦你包装了一个接口,你就失去了访问任何未在包装类型上定义的方法的能力。 也就是说,给定

type MyError struct {
  error
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.error)
}

如果您在该结构中包装错误,则会隐藏原始错误上的任何其他方法。

如果您不将错误包装在结构中,则无法提供额外的上下文。

假设接受的通用提案允许您定义类似以下的内容(我试图故意使其丑陋的任意语法,因此没有人会关注它)

type MyError generic_over[E which_is_a_type_satisfying error] struct {
  E
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.E)
}

通过利用嵌入,我们可以嵌入任何满足错误接口的具体类型,并包装它并访问它的其他方法。 不幸的是,这只会让我们走到那里。

我们真正需要的是获取错误接口的任意值并嵌入其动态类型。

这立即引起了两个担忧

  • 该类型必须在运行时创建(无论如何可能需要反射)
  • 如果错误值为 nil,类型创建将不得不恐慌

如果这些想法还没有让您感到厌烦,您还需要一种机制来“跨越”接口到其动态类型,或者通过泛型参数列表中的注释来说明“始终实例化接口值的动态类型" 或者通过一些只能在类型实例化期间调用的魔术函数来拆箱接口,以便正确拼接其类型和值。

没有它,您只是在错误类型本身而不是接口的动态类型上实例化MyError

假设我们有一个神奇的unbox函数可以提取并(以某种方式)应用信息:

func wrap(ec extraContext, err error) error {
  if err == nil {
    return nil
  }
  return MyError{
    E: unbox(err),
    extraContext: ec,
  }
}

现在假设我们有一个非零错误err ,其动态类型为*net.DNSError 。 那么这个

wrapped := wrap(getExtraContext(), err)
//wrapped 's dynamic type is a MyStruct embedding E=*net.DNSError
_, ok := wrapped.(net.Error)
fmt.Println(ok)

将打印true 。 但是,如果err的动态类型是*os.PathError ,它会打印出 false。

鉴于演示中使用的钝语法,我希望所提议的语义是清晰的。

我也希望有一个更好的方法来解决这个问题,用更少的机制和仪式,但我认为上面的方法是可行的。

@jimmyfrasche如果我理解你想要什么,它是一个无包装的适应机制。 您希望能够扩展类型提供的操作集,而无需将其包装在隐藏原始类型的另一个对象中。 这是 Genus 提供的功能。

@andrewcmyers没有。

Go 中的结构允许嵌入。 如果您将一个没有名称但具有类型的字段添加到结构中,它会做两件事:它创建一个与类型同名的字段,并且它允许透明地分派到该类型的任何方法。 这听起来非常像继承,但事实并非如此。 如果你有一个类型 T 有一个方法 Foo() 那么下面是等价的

type S struct {
  T
}

type S struct {
  T T
}
func (s S) Foo() {
  s.T.Foo()
}

(当 Foo 被称为它的“this”时,它的类型总是 T)。

您还可以在结构中嵌入接口。 这为结构提供了接口契约中的所有方法(尽管您需要为隐式字段分配一些动态值,否则将导致相当于空指针异常的恐慌)

Go 具有根据类型方法定义契约的接口。 满足契约的任何类型的值都可以装箱在该接口的值中。 接口的值是指向内部类型清单(动态类型)的指针和指向该动态类型的值(动态值)的指针。 您可以对接口值进行类型断言,以 (a) 如果断言其非接口类型,则获取动态值;或者 (b) 如果断言动态值也满足的不同接口,则获取新的接口值。 通常使用后者对对象进行“功能测试”以查看它是否支持可选方法。 要重用前面的示例,一些错误具有“Temporary() bool”方法,因此您可以查看是否有任何错误是临时的:

func isTemp(err error) bool {
  if t, ok := err.(interface{ Temporary() bool}); ok {
    return t.Temporary()
  }
  return false
}

将一种类型包装在另一种类型中以提供额外功能也很常见。 这适用于非接口类型。 当你包装一个接口时,虽然你也隐藏了你不知道的方法,并且你不能用“功能测试”类型断言来恢复它们:被包装的类型只公开接口的必需方法,即使它有可选的方法. 考虑:

type A struct {}
func (A) Foo()
func (A) Bar()

type I interface {
  Foo()
}

type B struct {
  I
}

var i I = B{A{}}

你不能在i Bar甚至知道它存在,除非你知道 i 的动态类型是 B 所以你可以打开它并在 I 字段中键入 assert .

这会导致真正的问题,尤其是处理错误或 Reader 等常见接口。

如果有办法将动态类型和值从接口中取出(以某种安全、受控的方式),您可以用它参数化一个新类型,将嵌入字段设置为该值,然后返回一个新接口。 然后你得到一个满足原始接口的值,有你想要添加的任何增强功能,但是原始动态类型的其余方法仍然需要进行功能测试。

@jimmyfrasche确实。 Genus 允许你做的是使用一种类型来满足“接口”合同而不用装箱。 该值仍然具有其原始类型和原始操作。 此外,程序可以指定该类型应该使用哪些操作来满足契约——默认情况下,它们是该类型提供的操作,但如果该类型没有必要的操作,程序可以提供新的操作。 它还可以替换该类型将使用的操作。

@jimmyfrasche @andrewcmyers对于该用例,另请参阅https://github.com/golang/go/issues/4146#issuecomment -318200547。

@jimmyfrasche对我来说,听起来这里的关键问题是获取变量的动态类型/值。 撇开嵌入不谈,一个简化的例子是

type MyError generic_over[E which_is_a_type_satisfying error] struct {
  e E
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.e)
}

分配给e的值需要具有类似*net.DNSError的动态(或具体)类型,它实现error 。 以下是未来语言更改可能解决此问题的几种可能方式:

  1. 有一个类似于unbox的神奇函数,可以揭示变量的动态值。 这适用于任何不具体的类型,例如联合。
  2. 如果语言更改支持类型变量,请提供获取变量动态类型的方法。 有了类型信息,我们就可以自己编写unbox函数了。 例如,
func unbox(v T1) T2 {
    t := dynTypeOf(v)
    return v.(t)
}

wrap可以像以前一样写成,也可以写成

func wrap(ec extraContext, err error) error {
  if err == nil {
    return nil
  }
  t := dynTypeOf(err)
  return MyError{
    e: v.(t),
    extraContext: ec,
  }
}
  1. 如果语言更改支持类型约束,这里有一个替代想法:
type E1 which_is_a_type_satisfying error
type E2 which_is_a_type_satisfying error

func wrap(ec extraContext, err E1) E2 {
  if err == nil {
    return nil
  }
  return MyError{
    e: err,
    extraContext: ec,
  }
}

在此示例中,我们接受任何实现错误的类型的值。 任何期望error的 $#$ wrap $#$ 用户都会收到一个。 但是, MyError e的类型和传入的err的类型是一样的,不限于接口类型。 如果一个人想要与 2 相同的行为,

var iface error = ...
wrap(getExtraContext(), unbox(iface))

由于似乎没有其他人做过,我想指出https://blog.golang.org/toward-go2 所要求的非常明显的泛型“经验报告”。

第一个是内置的map类型:

m := make(map[string]string)

接下来是内置的chan类型:

c := make(chan bool)

最后,标准库充满了interface{}替代品,泛型可以更安全地工作:

  • heap.Interface (https://golang.org/pkg/container/heap/#Interface)
  • list.Element (https://golang.org/pkg/container/list/#Element)
  • ring.Ring (https://golang.org/pkg/container/ring/#Ring)
  • sync.Pool (https://golang.org/pkg/sync/#Pool)
  • 即将推出的sync.Map (https://tip.golang.org/pkg/sync/#Map)
  • atomic.Value (https://golang.org/pkg/sync/atomic/#Value)

可能还有其他我想念的。 关键是,以上每一个都是我希望泛型有用的地方。

(注意:我在这里不包括sort.Sort ,因为它是一个很好的例子,说明了如何使用接口而不是泛型。)

http://www.yinwang.org/blog-cn/2014/04/18/golang
我认为泛型很重要。否则,无法处理类似的类型。有时接口无法解决问题。

简单的语法和类型系统是 Go 的重要优点。 如果添加泛型,语言将像 Scala 或 Haskell 一样变得一团糟。 此功能还将吸引伪学术爱好者,他们最终会将社区价值观从“让我们完成”转变为“让我们谈谈 CS 理论和数学”。 避免泛型,这是一条通往深渊的道路。

@bxqgit请保持文明。 没有必要侮辱任何人。

至于未来会带来什么,我们拭目以待,但我确实知道,虽然在我 98% 的时间里我不需要泛型,但每当我需要它们时,我希望我能使用它们。 它们如何被使用与它们如何被错误使用是一个不同的讨论。 教育用户应该是这个过程的一部分。

@bxqgit
在某些情况下需要泛型,例如泛型数据结构(Trees、Stacks、Queues ......)或泛型函数(Map、Filter、Reduce ......),这些都是不可避免的,使用接口而不是泛型这些情况只会为代码编写者和代码阅读者增加巨大的复杂性,并且还会对运行时的代码效率产生不良影响,因此添加到语言泛型中应该比尝试使用接口和反射来编写复杂的代码更合理和低效的代码。

@bxqgit添加泛型并不一定会增加语言的复杂性,这也可以通过简单的语法来实现。 正如@riwogo所说,使用泛型,您正在添加一个变量编译时间类型约束,这对数据结构非常有用。

目前go中的接口系统非常好用,但是当你需要一个通用的list实现的时候就很糟糕了使用实际类型编译时间,使约束变得不必要。

另外,请记住,背后的人使用所谓的“CS 理论和数学”开发语言,也是“正在完成这项工作”的人。

另外,请记住,背后的人使用所谓的“CS 理论和数学”开发语言,也是“正在完成这项工作”的人。

就我个人而言,我在 Go 语言设计中没有看到太多的 CS 理论和数学。 这是一种相当原始的语言,在我看来这很好。 您所说的那些人也决定避免使用泛型并完成工作。 如果它工作得很好,为什么要改变任何东西? 一般来说,我认为不断发展和扩展语言的语法是一种不好的做法。 它只会增加复杂性,导致 Haskell 和 Scala 的混乱。

模板很复杂,但泛型很简单

查看 sort 包中的函数 SortInts、SortFloats、SortStrings。 或 SearchInts、SearchFloats、SearchStrings。 或者 io/ioutil 包中 byName 的 Len、Less 和 Swap 方法。 纯样板复制。

存在复制和追加功能是因为它们使切片更有用。 泛型意味着这些函数是不必要的。 泛型可以为地图和通道编写类似的函数,更不用说用户创建的数据类型了。 诚然,切片是最重要的复合数据类型,这就是需要这些函数的原因,但其他数据类型仍然有用。

我对通用应用程序泛型投了反对票,对更多的内置泛型函数(如适用于多种基本类型的appendcopy )投了反对票。 也许可以为集合类型添加sortsearch

对于我的应用程序,唯一缺少的类型是无序集(https://github.com/golang/go/issues/7088),我希望将其作为内置类型,以便获得像slice这样的通用类型map 。 将工作放入编译器(对每种基本类型和一组选定的struct类型进行基准测试,然后调整以获得最佳性能)并在应用程序代码中保留额外的注释。

smap内置而不是sync.Map也请。 根据我使用interface{}进行运行时类型安全的经验,这是一个设计缺陷。 编译时类型检查是使用 Go 的主要原因。

@pciet

根据我使用 interface{} 来保证运行时类型安全的经验,这是一个设计缺陷。

你能写一个小的(类型安全的)包装器吗?
https://play.golang.org/p/tG6hd-j5yx

@pierrre该包装器比reflect.TypeOf(item).AssignableTo(type)检查更好。 但是用map + sync.Mutexsync.RWMutex编写自己的类型与没有sync.Map所需的类型断言的复杂性相同。

我的同步映射用于互斥体的全局映射,它旁边有一个var myMapLock = sync.RWMutex{} ,而不是创建一个类型。 这可能更干净。 一个通用的内置类型对我来说听起来不错,但需要我做不到的工作,我更喜欢我的方法而不是类型断言。

我怀疑许多 Go 程序员似乎对泛型产生了负面的本能反应,因为他们对泛型的主要接触是通过 C++ 模板。 这是不幸的,因为 C++ 从第一天起就出现了可悲的泛型错误,并且从那以后一直在加剧这个错误。 Go 的泛型可能更简单,更不容易出错。

看到 Go 通过添加内置参数化类型变得越来越复杂,这将令人失望的。 最好只为程序员添加语言支持以编写自己的参数化类型。 然后可以将特殊类型作为库提供,而不是使核心语言混乱。

@andrewcmyers “Go 的泛型可能更简单,更不容易出错。” --- 就像 C# 中的泛型。

令人失望的是,通过添加内置参数化类型,Go 变得越来越复杂。

尽管对这个问题进行了猜测,但我认为这极不可能发生。

参数化类型的复杂性度量的指数是方差。
Go 的类型(接口除外)是不变的,这可以而且应该是
遵守规则。

一种机械的、编译器辅助的“类型复制粘贴”泛型实现
将以真正 Go 的底层方式解决 99% 的问题
浅薄和不意外的原则。

顺便说一句,已经讨论了这个和其他几十个可行的想法
之前,有些甚至最终以良好、可行的方法告终。 在这
点,我对他们都消失了
无声无息的进入虚空。

2017 年 11 月 28 日 23:54,“Andrew Myers” [email protected]写道:

我怀疑许多 Go 对仿制药的负面反应
程序员的出现似乎是因为他们对泛型的主要接触是
通过 C++ 模板。 这是不幸的,因为 C++ 悲惨地得到了泛型
从第一天开始就错了,从那以后就一直在加剧这个错误。 泛型
Go 可能更简单,更不容易出错。

令人失望的是,通过添加 Go 变得越来越复杂
内置参数化类型。 最好只添加语言
支持程序员编写自己的参数化类型。 然后
特殊类型可以作为库提供而不是混乱
核心语言。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/15292#issuecomment-347691444或静音
线程
https://github.com/notifications/unsubscribe-auth/AJZ_jPsQd2qBbn9NI1wZeT-O2JpyraTMks5s7I81gaJpZM4IG-xv
.

是的,您可以拥有没有模板的泛型。 模板是高级参数多态性的一种形式,主要用于元编程设施。

@ianlancetaylor Rust 允许程序在现有类型Q $ 上实现特征T $ ,前提是他们的 crate 定义TQ

只是一个想法:我想知道 Simon Peyton Jones(是的,Haskell 的名气)和/或 Rust 开发人员是否能够提供帮助。 Rust 和 Haskell 可能拥有任何生产语言中最先进的两种类型系统,Go 应该向它们学习。

还有Phillip Wadler ,他在Generic Java上工作,最终导致了 Java 今天的泛型实现。

@tarcieri我认为Java 的泛型不是很好,但它们经过了实战考验。

@DemiMarie 幸运的是,我们有 Andrew Myers 在这里投球。

根据我的个人经验,我认为对不同语言和不同类型系统有很多了解的人对检查想法很有帮助。 但是首先要产生这些想法,我们需要的是非常熟悉 Go 的人,它现在是如何工作的,以及它在未来如何合理地工作。 Go 被设计成一种简单的语言。 从比 Go 复杂得多的 Haskell 或 Rust 等语言中导入想法不太适合。 总的来说,那些还没有写出合理数量的 Go 代码的人的想法不太适合; 并不是说这些想法本身就不好,只是它们不能很好地适应语言的其余部分。

例如,理解 Go 已经部分支持使用接口类型的泛型编程并且已经(几乎)完全支持使用反射包是很重要的。 尽管出于各种原因,这两种泛型编程方法都不能令人满意,但 Go 中的任何泛型提议都必须与它们进行良好的交互,同时解决它们的缺点。

事实上,当我在这里的时候,我曾考虑过使用接口进行泛型编程,并提出了三个不能令人满意的原因。

  1. 接口要求所有操作都表示为方法。 这使得为​​内置类型(例如通道类型)编写接口变得很痛苦。 所有通道类型都支持<-操作符进行发送和接收操作,使用SendReceive方法编写接口很容易,但是为了分配通道值对于该接口类型,您必须编写样板SendReceive方法。 对于每种不同的通道类型,这些样板方法看起来完全相同,这很乏味。

  2. 接口是动态类型的,因此组合不同静态类型值的错误只会在运行时捕获,而不是编译时。 例如,使用SendReceive方法将两个通道合并为单个通道的Merge函数将要求两个通道具有相同类型的元素,但是检查只能在运行时进行。

  3. 接口总是装箱的。 例如,如果不将这些其他类型放入接口值中,就无法使用接口来聚合一对其他类型,这需要额外的内存分配和指针追踪。

我很高兴就 Go 的泛型提案进行 kibitz。 也许最近康奈尔大学对仿制药的研究越来越多,这似乎与 Go 可能做的事情有关:

http://www.cs.cornell.edu/andru/papers/familia/ (Zhang & Myers, OOPSLA'17)
http://io.livecode.ch/learn/namin/unsound (Amin & Tate, OOPSLA'16)
http://www.cs.cornell.edu/projects/genus/ (张等人,PLDI '15)
https://www.cs.cornell.edu/~ross/publications/shapes/shapes-pldi14.pdf (格林曼,穆尔博克和泰特,PLDI '14)

在对无序集类型的 map 与 slice 进行基准测试时,我为每个类型编写了单独的单元测试,但是对于接口类型,我可以将这两个测试列表合并为一个:

type Item interface {
    Equal(Item) bool
}

type Set interface {
    Add(Item) Set
    Remove(Item) Set
    Combine(...Set) Set
    Reduce() Set
    Has(Item) bool
    Equal(Set) bool
    Diff(Set) Set
}

测试删除项目:

type RemoveCase struct {
    Set
    Item
    Out Set
}

func TestRemove(t *testing.T) {
    for i, c := range RemoveCases {
        if c.Out.Equal(c.Set.Remove(c.Item)) == false {
            t.Fatalf("%v failed", i)
        }
    }
}

通过这种方式,我可以毫无困难地将之前单独的案例组合成一个案例:

var RemoveCases = []RemoveCase{
    {
        Set: MapPathSet{
            &Path{{0, 0}}:         {},
            &Path{{0, 1}, {1, 1}}: {},
        },
        Item: Path{{0, 0}},
        Out: MapPathSet{
            &Path{{0, 1}, {1, 1}}: {},
        },
    },
    {
        Set: SlicePathSet{
            {{0, 0}},
            {{0, 1}, {1, 1}},
        },
        Item: Path{{0, 0}},
        Out: SlicePathSet{
            {{0, 1}, {1, 1}},
        },
    },
}

对于每种具体类型,我必须定义接口方法。 例如:

func (the MapPathSet) Remove(an Item) Set {
    return MapDelete(the, an.(Path))
}
func (the SlicePathSet) Remove(an Item) Set {
    return SliceDelete(the, an.(Path))
}

这些通用测试可以使用建议的编译时类型检查:

type Item generic {
    Equal(Item) bool
}
func (the SlicePathSet) Remove(an Item) Set {
    return SliceDelete(the, an)
}

来源: https ://github.com/pciet/pathsetbenchmark

再想一想,对于这样的测试,编译时类型检查似乎是不可能的,因为您必须运行程序才能知道类型是否传递给相应的接口方法。

那么作为一个接口并且在具体使用时编译器添加了一个不可见的类型断言的“通用”类型呢?

@andrewcmyers “Familia”论文很有趣(而且超出了我的想象)。 一个关键概念是继承。 对于像 Go 这样依赖组合而不是继承的语言,概念将如何变化?

谢谢。 继承部分不适用于 Go —— 如果你只对 Go 的泛型感兴趣,你可以在论文的第 4 节之后停止阅读。 那篇论文与 Go 相关的主要内容是,它展示了如何以现在用于 Go 的方式使用接口以及作为泛型抽象类型的约束。 这意味着您无需在语言中添加全新的结构即可获得 Haskell 类型类的强大功能。

@andrewcmyers你能举例说明这在 Go 中的样子吗?

那篇论文与 Go 相关的主要内容是,它展示了如何以现在用于 Go 的方式使用接口以及作为泛型抽象类型的约束。

我的理解是 Go 接口定义了一个类型的约束(例如,“可以使用'type Comparable interface'来比较这种类型的相等性,因为它满足具有 Eq 方法”)。 我不确定我理解你所说的类型约束是什么意思。

我对 Haskell 不熟悉,但阅读快速概述让我猜测适合 Go 接口的类型将适合该类型类。 你能解释一下 Haskell 类型类有什么不同吗?

Familia 和 Go 之间的具体比较会很有趣。 感谢您分享您的论文。

Go 接口可以被视为通过结构子类型描述类型的约束。 但是,该类型约束本身的表达能力不足以捕获泛型编程所需的约束。 例如,您不能在 Familia 论文中表达名为Eq的类型约束。

关于 Go 中更多通用编程工具的动机的一些想法:

所以这是我的通用测试列表,实际上不需要添加任何东西到语言中。 在我看来,我提出的泛型类型并不能满足 Go 直接理解的目标,它与普遍接受的编程术语没有太大关系,并且在那里进行类型断言并不难看,因为对失败的恐慌是美好的。 我对 Go 的通用编程工具已经满足了我的需要。

但是 sync.Map 是一个不同的用例。 标准库中需要成熟的通用同步映射实现,而不仅仅是具有映射和互斥锁的结构。 对于类型处理,我们可以用另一种类型包装它,该类型设置非接口{}类型并进行类型断言,或者我们可以在内部添加反射检查,以便第一个之后的项目必须匹配相同的类型。 两者都有运行时检查,包装需要为每种使用类型重写每个方法,但它为输入添加了编译时类型检查并隐藏了输出类型断言,并且通过内部检查,我们仍然必须进行输出类型断言。 无论哪种方式,我们都是在没有实际使用接口的情况下进行接口转换; interface{} 是对语言的一种破解,对于新的 Go 程序员来说并不清楚。 虽然 json.Marshal 在我看来是一个很好的设计(包括丑陋但合理的结构标签)。

我要补充一点,因为 sync.Map 理想情况下在标准库中,它应该为简单结构性能更高的测量用例交换实现。 不同步的映射是 Go 并发编程中常见的早期陷阱,标准库修复应该可以正常工作。

常规映射只有编译时类型检查,不需要任何这种脚手架。 我认为 sync.Map 应该是相同的,或者不应该在 Go 2 的标准库中。

我建议将 sync.Map 添加到内置类型列表中,并为将来的类似需求做同样的事情。 但我的理解是给 Go 程序员一种方法来做到这一点,而不必在编译器上工作,并且通过开源验收挑战是这个讨论背后的想法。 在我看来,修复 sync.Map 是一个真实的案例,它部分地定义了这个泛型提案应该是什么。

如果您将sync.Map添加为内置,那么您能走多远? 你对每个容器都进行特殊处理吗?
sync.Map 不是唯一的容器,在某些情况下,有些容器比其他容器更好。

@Azareal@chowey在八月份列出了这些:

最后,标准库充满了 interface{} 替代方案,泛型可以更安全地工作:

• heap.Interface (https://golang.org/pkg/container/heap/#Interface)
• list.Element (https://golang.org/pkg/container/list/#Element)
• ring.Ring (https://golang.org/pkg/container/ring/#Ring)
• sync.Pool (https://golang.org/pkg/sync/#Pool)
• 即将推出的sync.Map (https://tip.golang.org/pkg/sync/#Map)
• atomic.Value (https://golang.org/pkg/sync/atomic/#Value)

可能还有其他我想念的。 关键是,以上每一个都是我希望泛型有用的地方。

而且我想要可以比较相等的类型的无序集。

我希望根据基准测试在运行时中为每种类型的变量实现投入大量工作,以便通常使用的最佳实现是可能的。

我想知道 Go 1 是否有合理的替代实现可以为这些没有 interface{} 和没有泛型的标准库类型实现相同的目标。

golang 接口和 haskell 类型类克服了两件事(非常棒!):

1.)(类型约束)他们用一个标签分组不同的类型,接口名称
2.)(调度)它们通过接口实现为给定的一组函数提供不同类型的调度

但,

1.) 有时您只需要匿名组,例如一组 int、float64 和 string。 你应该如何命名这样的接口,NumericandString?

2.) 很多时候,您不想对每种类型的接口进行不同的分派,而是只为所有列出的接口类型提供一种方法(也许可以使用接口的默认方法

3.) 很多时候,您不想为一个组枚举所有可能的类型。 相反,您采取懒惰的方式,说我希望所有类型 T 实现一些接口 A,然后编译器在您编辑的所有源文件和所有用于在编译时生成适当函数的库中搜索所有类型。

尽管最后一点在 go via 接口多态中是可能的,但它的缺点是涉及强制转换的运行时多态以及如何限制函数的参数输入以包含实现多个接口或多个接口之一的类型。 可行的方法是引入扩展其他接口的新接口(通过接口嵌套)以实现类似但没有最佳实践的东西。

顺便一提。
我承认那些说 go 已经具有多态性的人,因此 go 不再是像 C 这样的简单语言。它是一种高级系统编程语言。 那么为什么不扩展 go 提供的多态性呢。

这是我今天为通用无序集类型启动的一个库: https ://github.com/pciet/unordered

这为编译时类型安全提供了类型包装器模式(感谢@pierrre)的文档和测试示例,并且还为运行时类型安全提供了反射检查。

仿制药有什么需求? 我之前对标准库泛型类型的负面态度主要集中在 interface{} 的使用上; 我的投诉可以通过 interface{} 的包特定类型(如 pciet/unordered 中的type Item interface{} )来解决,该类型记录了预期的不可表达的约束。

当现在只有文档可以让我们到达那里时,我认为不需要添加语言功能。 在提供通用设施的标准库中已经有大量经过实战测试的代码(参见 https://github.com/golang/go/issues/23077)。

您的代码在运行时进行类型检查(从这个角度来看,它绝不比interface{}更好,如果不是更糟的话)。 使用泛型,您可以拥有带有编译时类型检查的集合类型。

@zerkms运行时检查可以通过设置 asserting = false 来关闭(这不会出现在标准库中),编译时检查有一个使用模式,无论如何类型检查只查看接口结构(使用接口比类型检查增加了更多的费用)。 如果接口没有执行,那么您将不得不编写自己的类型。

您说性能最大化的通用代码是关键需求。 它不适用于我的用例,但也许标准库可以变得更快,也许其他人需要这样的东西。

可以通过设置 asserting = false 来关闭运行时检查

那么没有什么能保证正确性

您说性能最大化的通用代码是关键需求。

我没那么说过。 类型安全将是一个很大的问题。 您的解决方案仍然受到interface{}感染。

但也许标准库可以变得更快,也许其他人需要这样的东西。

可能是,如果核心开发团队乐于按需快速实施我需要的任何东西。

@pciet

当现在只有文档可以让我们到达那里时,我认为不需要添加语言功能。

你这么说,但使用切片形式的通用语言特性和 make 函数没有问题。

当现在只有文档可以让我们到达那里时,我认为不需要添加语言功能。

那为什么还要使用静态类型语言呢? 您可以使用像 Python 这样的动态类型语言,并依靠文档来确保将正确的数据类型发送到您的 API。

我认为 Go 的优点之一是可以通过编译器强制执行一些约束以防止将来出现错误。 这些设施可以扩展(使用泛型支持)以强制执行一些其他约束,以防止将来出现更多错误。

你这么说,但使用切片形式的通用语言特性和 make 函数没有问题。

我是说现有的特性让我们达到了一个很好的平衡点,它确实有通用的编程解决方案,并且应该有充分的真实理由来改变 Go 1 类型系统。 不是改变会如何改进语言,而是人们现在面临的问题,例如在 fmt 和数据库标准库包中为 interface{} 维护大量运行时类型切换,这将得到修复。

那为什么还要使用静态类型语言呢? 您可以使用像 Python 这样的动态类型语言,并依靠文档来确保将正确的数据类型发送到您的 API。

我听说过使用 Python 而不是静态类型语言和组织编写系统的建议。

大多数使用标准库的 Go 程序员使用的类型在没有文档或不查看实现的情况下无法完全描述。 具有参数子类型的类型或具有应用约束的通用类型仅以编程方式修复这些情况的子集,并且会生成标准库中已经完成的大量工作。

在求和类型的提案中,我建议了接口类型切换的构建功能,其中当分配给接口的可能值与任何包含的接口类型切换案例不匹配时,函数或方法中使用的接口会发出构建错误。

采用接口的函数/方法可能会在构建时拒绝某些类型,因为它没有默认情况,也没有类型的情况。 如果该功能可以实现,这似乎是一个合理的通用编程添加。

如果 Go 接口可以捕获实现者的类型,那么可能会有一种与当前 Go 语法完全兼容的泛型形式——泛型的单参数形式(演示)。

@dc0d对于通用容器类型,我相信该功能添加了编译时类型检查,而不需要包装器类型: https ://gist.github.com/pciet/36a9dcbe99f6fb71f5fc2d3c455971e5

@pciet你是对的。 在提供的代码中,第 4 号示例指出,类型是为切片和通道(和数组)捕获的。 但不适用于地图,因为只有一个类型参数:实现者。 而且由于映射需要两个类型参数,因此需要包装器接口。

顺便说一句,作为一种思路,我必须强调该代码的演示目的。 我不是语言设计师。 这只是 Go 中泛型实现的一种假设性思考方式:

  • 与当前的 Go 兼容
  • 简单(单个泛型类型参数,_feels_ 与其他 OO 中的 _this_ 类似,指当前实现者)

在希望最小化影响同时最大化重要用例和表达灵活性的背景下讨论通用性和所有可能的用例是一个非常复杂的分析。 不确定我们中的任何人是否能够将其提炼成一组简短的原则,即生成本质。 我正在努力。 无论如何,这里是我_cursory_阅读这个线程的一些初步想法......

@adg写道:

伴随这个问题的是@ianlancetaylor提出的通用泛型提案,其中包括四个针对 Go 通用编程机制的具体有缺陷的提案。

Afaics,摘录如下的链接部分未能说明当前接口缺乏通用性的情况,_“没有办法编写一个方法,该方法采用调用者提供的类型 T 的接口,对于任何 T,并返回一个值同类型T。”_。

没有办法用一个方法编写一个接口,该方法接受一个类型为 T 的参数,对于任何 T,并返回一个相同类型的值。

那么调用站点类型的代码如何检查它的类型 T 作为结果值呢? 例如,上述接口可能有一个用于构建类型 T 的工厂方法。这就是为什么我们需要对类型 T 的接口进行参数化。

接口不仅仅是类型; 它们也是价值观。 不使用接口值就无法使用接口类型,并且接口值并不总是有效的。

同意,由于接口目前无法在其操作的类型 T 上显式参数化,因此程序员无法访问类型 T。

所以这就是函数定义站点上类型类requires where的事情这些接口字典可以在编译时自动单态化,以便在运行时不会将字典指针(用于接口)传递给函数(我认为 Go 编译器目前适用于接口的单态化?)。 上面引用中的“值”是指输入类型 T,而不是类型 T 实现的接口类型的方法字典。

如果我们随后允许数据类型上的类型参数(例如struct ),那么上面所说的类型 T 本身可以被参数化,所以我们真的有一个类型T<U> 。 需要保留U知识的此类类型的工厂称为高级类型 (HKT)

泛型允许类型安全的多态容器。

参见下面讨论的 _heterogeneous_ 容器问题。 因此,多态是指容器的值类型的通用性(例如集合的元素类型),但也存在我们是否可以在容器中同时放入多个值类型使它们异构的问题。


@tamird写道:

这些要求似乎排除了例如类似于 Rust 特征系统的系统,其中泛型类型受特征边界的约束。

Rust 的特征界限本质上是类型类界限。

@alex写道:

锈的特征。 虽然我认为它们总体上是一个很好的模型,但它们不适合现在存在的 Go。

为什么你认为他们不合适? 也许您正在考虑使用运行时调度的特征对象,因此性能不如单态? 但是这些可以与类型类边界通用性原则分开考虑(参见我在下面对异构容器/集合的讨论)。 Afaics,Go 的接口已经是 trait-like bounds 并实现了类型类的目标,即后期将字典绑定到调用站点的数据类型,而不是早期绑定的 OOP 的反模式(即使仍处于编译阶段)时间)字典到数据类型(在实例化/构造时)。 类型类可以(至少部分提高自由度)解决OOP 无法解决的表达式问题

@jimmyfrasche写道:

  • https://golang.org/doc/faq#covariant_types

我同意上面的链接,类型类确实不是子类型,也没有表达任何继承关系。 并且同意不要将“泛型”(作为比参数多态性更普遍的重用或模块化概念)与继承混为一谈,就像子类化所做的那样。

但是我还想指出,如果语言支持联合和交集,继承层次结构(又名子类​​型化)在分配到(函数输入)和从(函数输出)时是不可避免的1 ,因为例如int ν string可以接受来自intstring的分配,但都不能接受来自int ν string的分配。 如果没有联合 afaik,提供静态类型异构容器/集合的唯一替代方法是子类化或存在界多态性(在 Rust 中又称为特征对象,在 Haskell 中称为存在量化)。 上面的链接包含关于存在主义和联合之间权衡的讨论。 Afaik,现在在 Go 中执行异构容器/集合的唯一方法是将所有类型包含到一个空的interface{}中,这会丢弃输入信息,我认为需要强制转换和运行时类型检查,这是2打败了静态类型的要点。

要避免的“反模式”是子类化,也就是虚拟继承(参见“EDIT#2”关于隐式包含和相等等问题)。

1无论它们在结构上还是名义上都匹配,因为子类型化是由于基于比较集的 Liskov 替换原则以及函数输入与返回值相反的赋值方向,例如structinterface的类型参数

2绝对主义将不适用,因为我们无法对无限不确定性的宇宙进行类型检查。 所以据我了解,这个线程是关于选择一个最佳(“最佳位置”)限制,以说明对通用性问题的打字水平。

@andrewcmyers写道:

与 Java 和 C# 泛型不同,Genus 泛型机制不基于子类型。

继承和子类化(不是结构子类型化)是您不想从 Java、Scala、Ceylon 和 C++ 复制的最糟糕的反模式(与 C++ 模板的问题无关)。

@thwd写道:

参数化类型的复杂性度量的指数是方差。 Go 的类型(接口除外)是不变的,这可以而且应该保持规则。

具有不变性的子类型避开了协方差的复杂性。 不变性还改善了子类化的一些问题(例如RectangleSquare ),但不能改善其他问题(例如隐含的包含、相等等)。

@bxqgit写道:

简单的语法和类型系统是 Go 的重要优点。 如果添加泛型,语言将像 Scala 或 Haskell 一样变得一团糟。

请注意,Scala 尝试将 OOP、子类化、FP、泛型模块、HKT 和类型类(通过implicit )全部合并到一个 PL 中。 也许仅类型类就足够了。

Haskell 不一定因为类型类泛型而迟钝,但更有可能是因为它在每个地方都强制执行纯函数,并使用单子范畴理论来模拟受控的命令式效果。

因此,我认为将这些 PL 的迟钝和复杂性与例如 Rust 中的类型类联系起来是不正确的。 并且我们不要将 Rust 的生命周期 + 专有可变性借用抽象归咎于类型类。

Afaics,在 Go_ 中的 _Type 参数的语义部分中,@ ianlancetaylor遇到的问题是一个概念化问题,因为他(afaics)显然在不知不觉中重新发明了类型类

我们可以合并SortableSlicePSortableSlice以实现两全其美吗? 不完全的; 无法编写支持具有Less方法的类型或内置类型的参数化函数。 问题是SortableSlice.Less不能为没有Less方法的类型实例化,并且没有办法只为某些类型实例化方法,而不能为其他类型实例化方法。

$#$ []T $#$ 的Less方法上的类型类绑定的requires Less[T]子句(即使编译器隐式推断)在T而不是[]T 。 每个TLess[T]类型类的实现(其中包含方法Less方法)将在方法的函数体中提供实现或分配<内置函数作为实现。 然而我相信这需要 HKT U[T]如果Sortable[U]的方法需要一个类型参数U代表实现类型,例如[]T 。 Afair @keean另一种构建排序的方法,该排序为不需要 HKT 的值类型T使用单独的类型类。

请注意[]T的那些方法可能正在实现Sortable[U]类型类,其中U[]T

(撇开技术不谈:似乎我们可以合并SortableSlicePSortableSlice ,方法是通过某种机制只为某些类型参数实例化一个方法,而不为其他类型参数实例化一个方法。然而,结果将是牺牲编译-时间类型安全,因为使用错误的类型会导致运行时恐慌。在 Go 中,已经可以使用接口类型和方法以及类型断言来选择运行时的行为。不需要提供另一种使用类型参数的方法来做到这一点.)

对于静态已知的T ,调用站点上绑定的类型类的选择在编译时解析。 如果需要异构动态调度,请参阅我在上一篇文章中解释的选项。

我希望@keean能抽出时间来这里帮助解释类型类,因为他更专业,并帮助我学习了这些概念。 我的解释中可能有一些错误。

对于那些已经阅读过我之前的帖子的人来说,请注意我在发布它大约 10 小时后(经过一些睡眠)对其进行了广泛的编辑,以希望使关于异构容器的观点更加连贯。


Cycles部分似乎不正确。 $# structS[T]{e}实例的运行时构造与调用的泛型函数的实现选择无关。 他大概认为编译器不知道它是否专门针对参数类型专门实现泛型函数,但所有这些类型在编译时都是已知的。

也许可以通过研究@keean 将不同类型的连通图概念作为统一算法的节点来简化类型检查部分规范。 由边连接的任何不同类型必须具有全等类型,并为通过赋值或源代码中其他方式连接的任何类型创建边。 如果有联合和交叉(来自我之前的帖子),那么必须考虑分配的方向(不知何故? )。 每个不同的未知类型都以Top的最小上限 (LUB) 和Bottom的最大下限 (GLB) 开始,然后约束可以改变这些界限。 连接类型必须具有兼容的边界。 约束都应该是类型类边界。

实施中:

例如,始终可以通过为每个实例化生成函数的新副本来实现参数化函数,其中通过将类型参数替换为类型参数来创建新函数。

我相信正确的技术术语是monomorphisation

这种方法将以相当多的额外编译时间和增加代码大小为代价产生最有效的执行时间。 对于小到可以内联的参数化函数,这可能是一个不错的选择,但在大多数其他情况下,这将是一个糟糕的权衡。

分析会告诉程序员哪些函数最能从单态化中受益。 也许 Java Hotspot 优化器会在运行时进行单态优化?

@egonelbre写道:

Go Generics Discussions 的摘要,它试图提供来自不同地方的讨论的概述。

概述部分似乎暗示 Java 对容器中实例的装箱引用的普遍使用是与 C++ 的模板单态化截然相反的唯一设计轴。 但是类型类边界(也可以用 C++ 模板实现,但始终是单态的)应用于函数而不是容器类型参数。 因此,afaics 的概述缺少类型类的设计轴,我们可以在其中选择是否对每个类型类有界函数进行单态化。 使用类型类,我们总是让程序员更快(更少样板),并且可以在使编译器/执行更快/更慢和代码膨胀更大/更少之间获得更精确的平衡。 根据我之前的帖子,如果要单态化的函数的选择是分析器驱动的(自动或更有可能通过注释),那么最佳选择可能是。

在问题:通用数据结构部分:

缺点

  • 通用结构倾向于从所有用途中积累特征,从而导致编译时间增加或代码膨胀或需要更智能的链接器。

对于类型类,这要么不是真的,要么不是问题,因为只需要为提供给使用这些接口的函数的数据类型实现接口。 类型类是关于实现到接口的后期绑定,不像 OOP 将每个数据类型绑定到class实现的方法。

同样,并非所有方法都需要放在一个接口中。 绑定到函数声明的类型类上的requires子句(即使编译器隐式推断)可以混合匹配所需的接口。

  • 通用结构和对其进行操作的 API 往往比专门构建的 API 更抽象,这会给调用者带来认知负担

我认为显着改善这种担忧的一个反驳论点是,学习无限数量的基本相同的通用算​​法的特殊情况重新实现的认知负担是无限的。 然而,学习抽象的通用 API 是有限的。

  • 深度优化是非常非通用的和特定于上下文的,因此在通用算法中更难优化它们。

这不是一个有效的骗局。 80/20 规则说不要为分析时不需要的代码添加无限复杂性(例如过早优化)。 程序员可以在 20% 的情况下自由优化,而剩下的 80% 则由通用 API 的有限复杂性和认知负荷来处理。

我们真正了解的是语言的规律性和通用 API 的帮助,而不是伤害它。 这些缺点确实没有正确概念化。

替代解决方案:

  • 使用更简单的结构而不是复杂的结构

    • 例如使用 map[int]struct{} 而不是 Set

Rob Pike(我也看到他在视频中指出了这一点)似乎忽略了通用容器不足以生成通用函数这一点。 我们需要T中的map[T] ,这样我们就可以在函数中为输入、输出和我们自己的struct传递通用数据类型。 仅在容器类型参数上的泛型完全不足以表达泛型 API,并且泛型 API 需要有限的复杂性和认知负载以及在语言生态系统中获得规律性。 此外,我还没有看到非泛型代码所需的重构级别提高(因此不易重构的模块的可组合性降低),这就是我在第一篇文章中提到的表达式问题。

通用方法部分:

包模板
这是 Modula-3、OCaml、SML(所谓的“函子”)和 Ada 使用的一种方法。 整个包是通用的,而不是为特化指定一个单独的类型。 您可以通过在导入时修复类型参数来专门化包。

我可能弄错了,但这似乎不太正确。 ML 仿函数(不要与 FP 仿函数混淆)也可以返回保持类型参数化的输出。 否则将无法在其他泛型函数中使用算法,因此泛型模块将无法重用(通过将具体类型导入)其他泛型模块。 这似乎是一种过度简化的尝试,然后完全忽略了泛型、模块重用等的重点。

相反,我的理解是,该包(又名模块)类型参数化能够将类型参数应用于structinterfacefunc的分组。

更复杂的类型系统
这是 Haskell 和 Rust 采用的方法。
[…]
缺点:

  • 难以适应更简单的语言(https://groups.google.com/d/msg/golang-nuts/smT_0BhHfBs/MWwGlB-n40kJ)

在链接文档中引用@ianlancetaylor

如果您相信这一点,那么值得指出的是
Go 运行时中的 map 和 slice 代码在某种意义上不是通用的
使用类型多态性。 它在某种意义上是通用的
输入反射信息以查看如何移动和比较类型
价值观。 所以我们有证据证明它是可以接受的
通过编写使用类型的非多态代码在 Go 中“通用”代码
有效地反射信息,然后将该代码包装在
编译时类型安全的样板(在地图和切片的情况下)
这个样板当然是由编译器提供的)。

这就是从添加了泛型的 Go 超集转换而来的编译器将输出为 Go 代码的内容。 但是包装不会基于诸如包装之类的描述,因为那将缺乏我已经提到的可组合性。 要点是没有捷径可以通向一个好的可组合泛型类型系统。 要么我们做对了,要么什么都不做,因为添加一些不是真正泛型的不可组合的hack 最终会产生拼凑的半成品泛型和不规则的极端情况以及制作Go生态系统代码的变通方法的集群惯性无法理解。

这也是事实,大多数编写大型复杂 Go 程序的人都有
没有发现对仿制药的重大需求。 到目前为止,更像是
一个恼人的疣——需要写三行样板文件
每种类型都要排序——而不是编写有用的主要障碍
代码。

是的,这一直是我心中的想法之一,即是否可以使用完整的类型类系统。 如果你所有的库都基于它,那么显然它可能是一个美丽的和谐,但如果我们正在考虑现有 Go hacks 的通用性,那么对于很多项目来说,获得的额外协同作用可能会很低?

但是,如果来自 typeclass 语法的转译器模拟了 Go 可以对泛型建模的现有手动方式(编辑:我刚刚读到@andrewcmyers声明是合理的),这可能会不那么繁重并找到有用的协同作用。 例如,我意识到可以使用interface来模拟两个参数类型类,它在模拟元组的struct上实现,或者@jba提到了在上下文中使用内联interface的想法. 显然struct是结构上的而不是名义上的类型,除非给定一个名称type ? 此外,我确认了一个interface的方法可以输入另一个interface ,因此在我在之前的帖子中写过的排序示例中,可能可以从 HKT 转换。 但是当我不那么困的时候,我需要多考虑一下。

我认为可以公平地说大多数 Go 团队不喜欢 C++
模板,其中分层了一种图灵完备的语言
另一种图灵完备语言,使得这两种语言具有
完全不同的语法,两种语言的程序都是
以非常不同的方式编写。 C++ 模板起到警示作用
故事,因为复杂的实现已经遍及整个
标准库,导致 C++ 错误消息成为
惊奇和惊奇。 这不是 Go 永远遵循的路径。

我怀疑有人会不同意! 单态化的好处与图灵完备的泛型元编程引擎的缺点是正交的。

顺便说一句,在我看来,C++ 模板的设计错误与生成(而不是应用)ML 函子缺陷的生成本质相同。 适用最小功率原则。


@ianlancetaylor写道:

令人失望的是,通过添加内置参数化类型,Go 变得越来越复杂。

尽管对这个问题进行了猜测,但我认为这极不可能发生。

但愿如此。 我坚信 Go 应该要么添加一个连贯的泛型系统,要么只是接受它永远不会有泛型。

我认为转译器的分叉更有可能发生,部分原因是我有资金来实施它并且有兴趣这样做。 但我仍在分析情况。

虽然这会破坏生态系统,但至少 Go 可以保持纯粹的极简主义原则。 因此,为了避免破坏生态系统并允许我希望进行一些其他创新,我可能不会将其设为超集并将其命名为零

@pciet写道:

我对通用应用程序泛型投了反对票,对更多的内置泛型函数(如适用于多种基本类型的appendcopy )投了反对票。 也许可以为集合类型添加sortsearch

扩大这种惯性可能会阻止全面的泛型特性进入 Go。 那些想要仿制药的人可能会离开去寻找更绿色的牧场。 @andrewcmyers重申了这一点:

看到 Go 通过添加内置参数化类型变得越来越复杂,这会令人失望。 最好只为程序员添加语言支持以编写自己的参数化类型。

@shelby3

Afaik,现在在 Go 中执行异构容器/集合的唯一方法是将所有类型归入一个空接口{},这会丢弃输入信息,我认为需要强制转换和运行时类型检查,这有点违背了静态类型。

有关 Go 中 interface{} 集合的静态类型检查,请参阅上面评论中的包装器模式。

要点是没有捷径可以通向一个好的可组合泛型类型系统。 要么我们做对了,要么什么都不做,因为添加了一些不是真正泛型的不可组合的hack……

你能再解释一下吗? 对于具有定义所包含项的必要通用行为的接口的集合类型情况,编写函数似乎是合理的。

@pciet这段代码实际上是在做@shelby3描述和考虑反模式的确切事情。 引用你之前的话:

这为编译时类型安全提供了类型包装器模式(感谢@pierrre)的文档和测试示例,并且还为运行时类型安全提供了反射检查。

您正在获取缺少类型信息的代码,并且在逐个类型的基础上,使用反射添加强制转换和运行时类型检查。 这正是@shelby3所抱怨的。 我倾向于将这种方法称为“手动单态化”,这正是我认为最好交给编译器的那种乏味的家务活。

这种方法有许多缺点:

  • 需要逐个类型的包装器,手动或go generate类工具维护
  • (如果是手工而不是工具)有机会在样板文件中犯错误,直到运行时才会被发现
  • 需要动态调度而不是静态调度,这既慢又占用更多内存
  • 使用运行时反射而不是编译时类型断言,这也很慢
  • 不可组合:完全作用于具体类型,没有机会在类型上使用类类型类(甚至类接口)边界,除非您为每个还想抽象的非空接口手动处理另一层间接层

你能再解释一下吗? 对于具有定义所包含项的必要通用行为的接口的集合类型情况,编写函数似乎是合理的。

现在,无论您想使用绑定来代替具体类型还是除了具体类型之外,您都必须为每个接口类型编写相同的类型检查样板。 它只是进一步加剧了您必须编写的静态类型包装器的(可能是组合的)爆炸。

还有一些想法,据我所知,今天根本无法在 Go 的类型系统中表达,例如对接口组合的限制。 想象一下我们有:

type Foo interface {
    ...
}

type Bar interface {
    ...
}

我们如何使用纯静态类型检查来表达我们想要一个同时实现FooBar的类型? 据我所知,这在 Go 中是不可能的(没有诉诸可能失败的运行时检查,避开静态类型安全)。

使用基于类型类的泛型系统,我们可以将其表示为:

func baz<T Foo + Bar>(t T) {
    ...
}

@tarcieri

我们如何使用纯静态类型检查来表达我们想要一个同时实现 Foo 和 Bar 的类型?

就像这样:

type T interface {
    Foo
    Bar
}

func baz(t T) { ... }

@sbinet整洁,直到

就我个人而言,我认为运行时反射是一个错误功能,但这只是我……如果有人感兴趣,我可以解释为什么。

我认为任何实现任何类型的泛型的人都应该先阅读 Stepanov 的“编程元素”几遍。 它将避免很多 Not Invented Here 问题和重新发明轮子。 阅读后应该清楚为什么“C++ 概念”和“Haskell 类型类”是做泛型的正确方法。

我看到这个问题似乎又活跃了
这是一个稻草人求婚游乐场
https://go-li.github.io/test.html
只需从此处粘贴演示程序
https://github.com/go-li/demo

非常感谢您对这个单参数的评价
函数泛型。

我们维护被黑的 gccgo 和
这个项目没有你是不可能的,所以我们
想回馈。

我们也期待您采用任何仿制药,继续努力!

@anlhord关于这个的实现细节在哪里? 哪里可以读到语法? 执行什么? 什么没有实施? 此实现的规范是什么? 它的优点和缺点是什么?

游乐场链接包含最糟糕的例子:

package main

import "fmt"

func main() {
    fmt.Println("Hello, playground")
}

该代码没有告诉我如何使用它以及我可以测试什么。

如果您可以改进这些事情,这将有助于更好地了解您的提案是什么以及它与以前的提案相比如何/看看这里提出的其他观点如何适用或不适用于它。

希望这可以帮助您了解您的评论存在的问题。

@joho写道:

对于评估方法的任何指导,学术文献是否有价值?

我读过的唯一一篇关于该主题的论文是开发人员是否受益于泛型类型? (抱歉,您可能会用谷歌搜索 pdf 下载),其中有以下内容

因此,对实验的保守解释
是泛型类型可以被认为是一种权衡
积极的文档特征和
负扩展特性。

我认为 OOP 和子类化(例如 Java 和 C++ 中的类)都不会认真考虑,因为 Go 已经有一个类似于类型类的interface (没有显式的T泛型类型参数),Java 是引用为不可复制的内容,并且因为许多人认为它们是反模式。 Upthread 我已经链接到其中的一些论点。 如果有人感兴趣,我们可以更深入地进行分析

我还没有研究过更新的研究,例如提到的属系统 upthread 。 由于对 Scala 的抱怨,我对试图混合如此多范式(例如子类化、多重继承、OOP、特征线性化、 implicit 、类型类、抽象类型等)的“厨房水槽”系统持谨慎态度在实践中有如此多的极端情况,尽管 Scala 3(又名 Dotty 和 DOT 演算)可能会有所改善。 我很好奇他们的比较表是否与实验性 Scala 3 或当前版本的 Scala 进行比较?

因此,就经过验证的通用系统而言,剩下的是 ML 函子和 Haskell 类型类,与 OOP+子类化相比,它们显着提高了可扩展性和灵活性。

写了一些关于@keean的私人讨论,以及关于 ML 仿函数模块与类型类的讨论。 亮点似乎是:

  • typeclasses _model an algebra_ (但没有检查公理)并仅以一种方式为每个接口实现每种数据类型。 从而使编译器能够隐式选择实现,而无需在调用站点进行注释。

  • 应用函子具有引用透明性,而生成函子在每个实例化时创建一个新实例,这意味着它们不是初始化顺序不变的。

  • ML 仿函数比类型类更强大/更灵活,但这是以更多注释和可能更多极端案例交互为代价的。 根据@keean ,他们需要依赖类型(用于关联类型),这是更复杂的类型系统。 @keean认为Stepanov将泛型表达为代数加类型类足够强大和灵活,因此这似乎是最先进的、经过充分证明(在 Haskell 和现在在 Rust 中)泛型的最佳选择。 但是,公理不是由类型类强制执行的。

  • 我建议为具有类型类的异构容器添加联合以沿表达式问题的另一个轴扩展,尽管这需要不可变性或复制(仅适用于采用异构可扩展性的情况),已知具有O(log n)与无拘无束的可变迫切性相比,

@larsth写道:

拥有一个或多个实验性转译器可能会很有趣——从 Go 泛型源代码到 Go 1.xy 源代码编译器。

PS 我怀疑 Go 是否会采用如此复杂的类型系统,但我正在考虑将转译器转换为现有的 Go 语法,正如我在之前的帖子中提到的(参见底部的编辑)。 我想要一个健壮的通用系统以及那些非常理想的 Go 特性。 Go 上的类型类泛型似乎是我想要的。

@bcmills写了他关于泛型编译时函数的提议:

我听说过同样的论点曾经主张在 Go API 中导出interface类型而不是具体类型,而相反的情况更常见:过早的抽象过度约束了类型并阻碍了 API 的扩展。 (对于一个这样的例子,请参阅#19584。)如果你想依赖这一论点,我认为你需要提供一些具体的例子。

确实,类型系统抽象必然会放弃一些自由度,有时我们会以“不安全”(即违反静态检查抽象)摆脱这些约束,但这必须与具有简洁注释的不变量的模块化解耦。

在为通用性设计系统时,我们可能希望将增加生态系统的规律性和可预测性作为主要目标之一,特别是如果考虑到 Go 的核心理念(例如,普通程序员是优先事项)。

适用最小功率原则。 必须权衡“隐藏在”通用性编译时函数中的不变量的功能/灵活性与它们对例如生态系统中源代码的可读性造成损害的能力(其中模块化解耦非常重要,因为读者不会由于隐含的传递依赖关系,不必阅读可能无限量的代码,以理解给定的模块/包!)。 如果不遵守它们的代数,类型类实现实例的隐式解析就会出现这个问题

当然,但这对于 Go 中的许多隐式约束已经是正确的,独立于任何通用编程机制。

例如,一个函数可能会接收一个接口类型的参数,并最初按顺序调用它的方法。 如果该函数稍后更改为并发调用这些方法(通过产生额外的 goroutines),则约束“必须对并发使用是安全的”不会反映在类型系统中。

但是 afaik Go 并没有尝试设计一个抽象来模块化这些效果。 Rust 有这样一个抽象(顺便说一句,我认为这对于某些/大多数用例来说是过度的 pita/tsuris/limiting,我主张更简单的单线程模型抽象,但不幸的是 Go 不支持将所有生成的 goroutine 限制在同一个线程) . 并且 Haskell 需要对效果进行单子控制,因为它强制执行纯函数以实现引用透明性


@alercah写道:

我认为推断约束的最大缺点是它们可以很容易地以一种在不完全理解它的情况下引入约束的方式使用类型。 在最好的情况下,这只是意味着您的用户可能会遇到意外的编译时故障,但在最坏的情况下,这意味着您可以通过无意中引入新的约束来破坏消费者的包。 明确指定的约束可以避免这种情况。

同意。 因为类型的不变量没有显式注释,所以能够偷偷地破坏其他模块中的代码是非常阴险的。


@andrewcmyers写道:

需要明确的是,我不认为宏风格的代码生成,无论是使用 gen、cpp、gofmt -r 还是其他宏/模板工具完成,即使标准化也是解决泛型问题的好方法。 它与 C++ 模板存在相同的问题:代码膨胀、缺乏模块化类型检查和难以调试。 当您开始根据其他通用代码构建通用代码时,情况会变得更糟,这是很自然的。 在我看来,优势是有限的:它可以让 Go 编译器编写者的生活相对简单,并且它确实可以生成高效的代码——除非存在指令缓存压力,这是现代软件中常见的情况!

@keean似乎同意你的看法。

@shelby3感谢您的评论。 您下次可以直接在文档本身中进行评论/编辑吗? 更容易跟踪需要修复的地方,更容易确保所有笔记都得到正确的响应。

概述部分似乎暗示 Java 对实例的装箱引用的普遍使用.​​.....

添加了注释以明确说明,这并不是一个完整的列表。 它主要是为了让人们了解不同权衡的要点。 不同方法的完整列表如下。

通用结构倾向于从所有用途中积累特征,从而导致编译时间增加或代码膨胀或需要更智能的链接器。
对于类型类,这要么不是真的,要么不是问题,因为只需要为提供给使用这些接口的函数的数据类型实现接口。 类型类是关于实现到接口的后期绑定,不像 OOP 将每个数据类型绑定到它的类实现方法。

该声明是关于从长远来看通用数据结构会发生什么。 换句话说,一个通用的数据结构通常最终会收集所有不同的用途——而不是为了不同的目的而拥有多个较小的实现。 举个例子,看看https://www.scala-lang.org/api/2.12.3/scala/collection/immutable/List.html。

需要注意的是,仅仅“机械设计”和“尽可能多的灵活性”不足以创建一个好的“泛型解决方案”。 它还需要很好的说明,应该如何使用和避免什么,并考虑人们最终如何使用它。

通用结构和对其进行操作的 API 往往比专门构建的 API 更抽象......

我认为可以显着改善这种担忧的一个反驳论点是,学习无限数量的基本相同的通用算​​法的特殊情况重新实现的认知负担是无限的……

添加了关于许多类似 API 的认知负荷的注释。

特殊情况的重新实现在实践中并不是无限的。 您只会看到固定数量的专业化。

这不是一个有效的骗局。

您可能不同意某些观点,我在某种程度上不同意其中的一些观点,但我确实理解他们的观点并尝试理解人们日常面临的问题。 该文件的目的是收集不同的意见,而不是判断“某事对某人来说有多烦人”。

然而,该文件确实对“可追溯到现实世界问题的问题”采取了立场,因为论坛中的抽象和便利化问题往往会在没有建立任何理解的情况下陷入毫无意义的喋喋不休。

我们真正了解的是语言的规律性和通用 API 的帮助,而不是伤害它。

当然,在实践中,您可能仅在不到 1% 的情况下需要这种优化方式。

替代解决方案:

替代解决方案并不意味着可以替代泛型。 但是,更确切地说,是针对不同类型问题的潜在解决方案列表。

包模板

我可能弄错了,但这似乎不太正确。 ML 仿函数(不要与 FP 仿函数混淆)也可以返回保持类型参数化的输出。

您能否提供更清晰的措辞,并在必要时分为两种不同的方法?

@egonelbre也感谢您的回复,这样我就可以知道我需要在哪些方面进一步澄清我的想法。

您下次可以直接在文档本身中进行评论/编辑吗?

道歉希望我能遵守,但我从未使用过 Google Doc 的讨论功能,没有时间学习它,我也希望能够链接到我在 Github 上的讨论以供将来参考。

举个例子,看看 https://www.scala-lang.org/api/2.12.3/scala/collection/immutable/List.html。

Scala 集合库的设计受到了许多人的批评,其中包括他们的一位前关键团队成员发布到 LtU 的评论具有代表性。 请注意,我将以下内容添加到此线程中的先前帖子之一以解决此问题:

由于对 Scala 的抱怨,我对试图混合如此多范式(例如子类化、多重继承、OOP、特征线性化、 implicit 、类型类、抽象类型等)的“厨房水槽”系统持谨慎态度在实践中有如此多的极端情况,尽管 Scala 3(又名 Dotty 和 DOT 演算)可能会改善这种情况。

我认为 Scala 的集合库不能代表为 PL 创建的库,其中只有用于多态的类型类。 公平地说,Scala 集合采用了反模式继承,这导致了复杂的层次结构,再加上implicit帮助器,例如CanBuildFrom ,这大大增加了复杂性预算。 而且我认为,如果坚持@keean 的观点,即 Stepanov 的 _Elements of Programming_是一个代数,则可以创建一个优雅的集合库。 这是我看到的基于仿函数 (FP) 的集合库(即不复制 Haskell )的第一个替代方案,也是基于数学的。 我想在实践中看到这一点,这是我与他合作/讨论新 PL 设计的原因之一。 截至目前,我计划将该语言最初转换为 Go(尽管多年来我一直试图找到一种避免这样做的方法)。 因此,希望我们能够很快进行试验,看看效果如何。

我的看法是,Go 社区/哲学宁愿等着看什么在实践中有效,然后在经过验证后采用它,而不是匆忙通过失败的实验来污染语言。 因为正如您重申的那样,所有这些抽象的主张都不是那么有建设性(除了 PL 设计理论家)。 此外,由委员会设计一个连贯的泛型系统可能是不合理的。

它还需要很好的说明,应该如何使用和避免什么,并考虑人们最终如何使用它。

而且我认为这将有助于不将程序员可用的这么多不同的范式混合在同一种语言中。 显然没有必要( @keean ,我需要证明这一说法)。 我认为我们都认为复杂性预算是有限的,而您从 PL 中遗漏的内容与所包含的功能一样重要。

然而,该文件确实对“可追溯到现实世界问题的问题”采取了立场,因为论坛中的抽象和便利化问题往往会在没有建立任何理解的情况下陷入毫无意义的喋喋不休。

同意。 而且每个人也很难遵循抽象的观点。 魔鬼在细节和实际结果中。

当然,在实践中,您可能仅在不到 1% 的情况下需要这种优化方式。

Go 已经有interface用于通用性,因此可以处理调用站点提供的接口实例不需要类型T的参数多态性的情况。

我想在某个地方读到过,也许是在线程上,Go 的标准库实际上在优化使用最新的习语时遇到了不一致的问题。 我不知道这是不是真的,因为我还没有使用 Go 的经验。 我要说的是,所选择的泛型范式会感染所有的库。 所以是的,到目前为止,您可以声称只有 1% 的代码需要它,因为惯用语中已经存在避免需要泛型的惯性。

你可能是对的。 我也对我将使用任何特定语言功能的程度持怀疑态度。 我认为通过实验来找出答案是我将继续进行的方式。 PL 设计是一个迭代过程,那么问题是与发展的惯性作斗争,使得迭代过程变得困难。 所以我猜 Rob Pike 在视频中是正确的,他建议编写为程序编写代码的程序(意味着去编写生成工具和转译器)来试验和测试想法。

当我们可以证明一组特定的功能在实践中优于目前在 Go 中的那些功能(并且希望也很受欢迎),那么我们可能会看到一些关于将它们添加到 Go 中的共识形式。 我鼓励其他人也创建可以转换为 Go 的实验系统。

您能否提供更清晰的措辞,并在必要时分为两种不同的方法?

对于那些想要阻止在 Go 中放置一些过于简单的模板功能并声称这是泛型的人,我表示赞同。 IOW,我认为一个正常运行的泛型系统不会最终成为不良惯性,这与对泛型进行过度简单化设计的愿望根本不相容。 Afaik,一个泛型系统需要一个经过深思熟虑和经过充分验证的整体设计。 与@larsth所写的内容相呼应,我鼓励那些提出认真建议的人首先构建一个转译器(或在 gccgo 前端的一个分支中实施),然后对该建议进行试验,以便我们都能更好地理解它的局限性。 我被鼓励阅读@ianlancetaylor认为不会将不良惯性污染添加到 Go 的上行线程。 至于我对包级别参数化提案的具体抱怨,我对谁提出的建议,请考虑是否制作一个我们都可以使用的编译器,然后我们都可以谈论我们喜欢和不喜欢什么的例子不喜欢它。 否则,我们会互相交谈,因为也许我什至无法正确理解抽象描述的提案。 我一定不明白这个提议,因为我不明白参数化包如何在另一个也被参数化的包中重用。 IOW,如果一个包带有参数,那么它也需要用参数实例化其他包。 但似乎该提案表明实例化参数化包的唯一方法是使用具体类型,而不是类型参数。

道歉这么啰嗦。 想确保我没有被误解。

@shelby3啊,然后我误解了最初的投诉。 首先我要明确,“泛型方法”中的部分不是具体的建议。 它们是一种方法,或者换句话说,是一种可能在具体泛型方法中采取的更大的设计决策。 然而,这些分组受到现有实现或具体/非正式建议的强烈推动。 此外,我怀疑该列表中至少还缺少 5 个大创意。

对于“包模板”方法,它有两种变体(请参阅文档中的链接讨论):

  1. 基于“接口”的通用包,
  2. 明确的通用包。

对于 1. 它不需要通用包做任何特别的事情——例如当前的container/ring将变得可用于专业化。 想象这里的“专业化”是用具体类型替换包中接口的所有实例(并忽略循环导入)。 当该包本身特化另一个包时,它可以使用“接口”作为特化——因此,这种使用也将被特化。

对于 2. 你可以用两种方式看待它们。 一个是每次导入时的递归具体特化——类似于模板/宏,绝不会有“部分应用的包”。 当然,从功能方面也可以看出,泛型包是带有参数的部分,然后您将其专门化。

所以,是的,您可以在另一个中使用一个参数化包。

呼应@larsth所写的内容,我鼓励那些有认真提议的人首先构建一个转译器(或在 gccgo 前端的一个分支中实现),然后对该提议进行试验,以便我们都能更好地理解它的局限性。

我知道这并没有明确针对这种方法,但它确实有 4 个不同的原型来测试这个想法。 当然,它们不是完整的转译器,但它们足以测试一些想法。 即我不确定是否有人实现了“使用另一个参数化包”的情况。

参数化包听起来很像 ML 模块(ML 函子是参数可以是其他包)。 有两种方式可以“应用”或“生成”。 应用函子就像一个值或类型别名。 必须构造生成函子,并且每个实例都是不同的。 考虑这一点的另一种方法是,要应用的包必须是纯的(即在包级别没有可变变量)。 如果在包级别存在状态,则它必须是生成的,因为需要初始化该状态,并且重要的是您实际上将生成包的哪个“实例”作为参数传递给其他包,而其他包又必须是生成的。 例如 Ada 包是生成的。

生成包方法的问题在于它创建了大量样板文件,您在其中使用参数实例化包。 您可以查看 Ada 泛型以了解它的外观。

类型类通过仅基于函数中使用的类型隐式选择类型类来避免这种样板。 您还可以将类型类视为具有多个调度的约束重载,其中重载解析几乎总是在编译时静态发生,但多态递归和存在类型除外(它们本质上是您无法排除的变体,您只能使用变体确认的接口)。

应用函子就像一个值或类型别名。 必须构造生成函子,并且每个实例都是不同的。 考虑这一点的另一种方法是,要应用的包必须是纯的(即在包级别没有可变变量)。 如果在包级别存在状态,则它必须是生成的,因为需要初始化该状态,并且重要的是您实际上将生成包的哪个“实例”作为参数传递给其他包,而其他包又必须是生成的。 例如 Ada 包是生成的。

感谢您提供确切的术语,我需要考虑如何将这些想法整合到文档中。

此外,我看不出为什么你不能有一个“生成包的自动类型别名”的原因——在某种意义上介于“应用函子”和“生成函子”方法之间。 显然,当包确实包含某种形式的状态时,调试和理解会变得复杂。

生成包方法的问题在于它创建了大量样板文件,您在其中使用参数实例化包。 您可以查看 Ada 泛型以了解它的外观。

据我所知,它创建的样板文件比 C++ 模板少,但比类型类多。 你有一个很好的 Ada 真实世界程序来演示这个问题吗? _(在现实世界中,我的意思是某人正在/正在生产中使用的代码。)_

当然,看看我的 Ada 棋盘: https ://github.com/keean/Go-Board-Ada/blob/master/go.adb

尽管这是对生产的一个相当松散的定义,但代码已经过优化,性能与 C++ 版本及其开源一样好,而且算法已经经过数年的改进。 您也可以查看 C++ 版本: https ://github.com/keean/Go-Board/blob/master/go.cpp

这表明(我认为)Ada 泛型是比 C++ 模板更简洁的解决方案(但这并不难),另一方面,由于返回引用的限制,很难快速访问 Ada 中的数据结构.

如果您想查看命令式语言的包泛型系统,我认为 Ada 是最好看的。 很遗憾,他们决定采用多范式并将所有 OO 的东西添加到 Ada。 Ada 是一种增强的 Pascal,而 Pascal 是一种小巧而优雅的语言。 Pascal 加 Ada 泛型仍然是一门相当小的语言,但在我看来会好得多。 因为 Ada 的重点转移到了 OO 方法上,寻找好的文档和如何用泛型做同样事情的例子似乎很难找到。

虽然我认为类型类有一些优势,但我可以接受 Ada 风格的泛型,但有几个问题阻碍了我更广泛地使用 Ada,我认为它会错误地获取值/对象(我认为很少有语言能做到这一点, 'C' 是唯一的一个),很难使用指针(访问变量)和创建安全指针抽象,并且它不提供使用具有运行时多态性的包的方法(它提供了一个对象模型为此,但它添加了一个全新的范例,而不是试图找到一种使用包的运行时多态性的方法)。

运行时多态性的解决方案是使包成为一流的,因此包签名的实例可以作为函数参数传递,不幸的是,这需要依赖类型(请参阅 Scala 的依赖对象类型所做的工作,以清理它们所造成的混乱他们原来的类型系统)。

所以我认为包泛型可以工作,但是 Ada 花了几十年的时间来处理所有的边缘情况,所以我会研究一个生产泛型系统,看看在生产中使用了哪些改进。 然而,Ada 仍然存在不足,因为这些包不是一流的,并且不能用于运行时多态,这需要解决。

@keean写道

就我个人而言,我认为运行时反射是一个错误功能,但这只是我……如果有人感兴趣,我可以解释为什么。

类型擦除启用“免费定理”,具有实际意义。 可写(甚至可能由于与命令式代码的传递关系而可读?)运行时反射使得不可能保证任何代码中的引用透明性,因此某些编译器优化是不可能的,并且类型安全的 monad 也是不可能的。 我意识到 Rust 甚至还没有不可变特性。 OTOH,反射可以实现其他优化,如果它们不能被静态类型化,这些优化是不可能的。

我也说过upthread:

这就是从添加了泛型的 Go 超集转换而来的编译器将输出为 Go 代码的内容。 但是包装不会基于诸如包装之类的描述,因为那将缺乏我已经提到的可组合性。 要点是没有捷径可以通向一个好的可组合泛型类型系统。 要么我们做对了,要么什么都不做,因为添加一些不是真正泛型的不可组合的hack 最终会产生拼凑的半成品泛型和不规则的极端情况以及制作Go生态系统代码的变通方法的集群惯性无法理解。


@keean写道:

[…] 要使一个包适用,它必须是纯的(即在包级别没有可变变量)

并且不能使用不纯函数来初始化不可变变量。

@egonelbre写道:

所以,是的,您可以在另一个中使用一个参数化包。

我显然想到的是“一流的参数化包”和@keean随后提到的相应的运行时(又名动态)多态性,因为我假设参数化包是代替类型类或 OOP 提出的。

编辑:但是“一流”模块有两种可能的含义:作为一流值的模块,例如在Successor MLMixML中的模块,与作为一流值的模块在 1ML 中具有一流的类型,以及必要的权衡在它们之间的模块递归(即混合)中。

@keean写道:

运行时多态性的解决方案是使包成为一流的,因此包签名的实例可以作为函数参数传递,不幸的是,这需要依赖类型(请参阅 Scala 的依赖对象类型所做的工作,以清理它们所造成的混乱他们原来的类型系统)。

依赖类型是什么意思? (编辑:我现在假设他的意思是“不依赖于值”的类型,即“结果类型取决于 [runtime?] 参数['s type] 的函数”)当然不依赖于例如int的值F-ing Modules证明了“依赖”类型对于在系统 F ω中建模 ML 模块并不是绝对必要的。 如果我假设@rossberg重新制定打字模型以删除所有单态化要求,我是否过于简化了?

生成包方法的问题在于它会创建大量样板 […]
类型类通过仅基于函数中使用的类型隐式选择类型类来避免这种样板。

是不是也有适用的 ML 函子的样板? 没有已知的类型类和 ML 仿函数(模块)的统一,它保持简洁而不引入必要的限制来防止参见)类型类实现实例的全局唯一性标准的固有反模块化。

类型类只能以一种方式实现每种类型,否则需要newtype包装器样板来克服限制。 这是实现算法的多种方法的另一个示例。 Afaics, @keean在他的 typeclass排序示例中解决了这个限制,方法是使用显式选择的Relation覆盖隐式选择,并使用包装data类型在值类型上一般命名不同的关系,但我怀疑这种策略是否适用于所有模块化变体。 然而,一个更通用的解决方案(它可以帮助改善全局唯一性的模块化问题,可能与孤立限制相结合,通过对可能孤立的实现采用非默认值来改进孤立解决方案的提议版本控制)可能是有一个额外类型参数隐式在所有类型类interface上,当未指定时默认为正常的隐式匹配,但当指定时(或未指定时不匹配任何其他2 )然后选择具有相同值的实现在其以逗号分隔的自定义值列表中(因此这比命名特定的implement实例更通用,模块化匹配)。 逗号分隔的列表使得实现可以在多个自由度上进行区分,例如,如果它具有两个正交的特化。 可以在函数声明或调用站点指定所需的非默认专用化。 在呼叫站点,例如f<non-default>(…)

那么,如果我们有类型类,为什么还需要参数化模块呢? Afaics 仅用于(← 重要的点击链接)替换,因为为此目的重用类型类并不适合,例如,我们希望包模块能够跨越多个文件,并且我们希望能够隐式打开的内容将模块纳入范围而无需额外的样板文件因此,也许使用 _syntactical-only_ 仅替换(不是第一类)包参数化是合理的第一步,它可以解决模块级通用性,同时如果稍后为函数级添加类型类,则保持对功能的兼容性和不重叠通用性。 例如类型化的还是只是语法(也称为“预处理器”)替换,这是一个问题。 如果是类型化的,那么模块会复制类型类的功能,从最小化 PL 的重叠范式/概念的角度来看,以及由于重叠的相互作用(例如尝试提供 ML 函子和类型类时的潜在极端情况),这都是不可取的)。 类型化模块更加模块化,因为对模块内任何封装实现的修改不会修改导出的签名不会导致模块的使用者变得不兼容(除了前面提到的类型类重叠实现实例的反模块化问题)。 我有兴趣阅读@keean 对此的看法。

[…] 除了多态递归和存在类型(它们本质上是你不能强制转换的变体,你只能使用变体确认的接口)。

帮助其他读者。 通过“多态递归”,我认为是指更高级别的类型,例如在运行时设置的参数化回调,其中编译器无法单态化回调函数的主体,因为它在编译时是未知的。 正如我之前提到的,存在类型等同于 Rust 的 trait 对象,这是在表达式问题中获得异构容器的一种方法,而不是class子类化虚拟继承,但对表达式中的扩展不开放作为具有不可变数据结构的联合或复制具有O(log n) 性能成本的3的问题。

1在上面的例子中不需要 HKT,因为SET不需要elem类型是set的泛型类型的类型参数,即它不是set<elem>

2然而,如果存在多个非默认实现且没有默认实现,则选择将是不明确的,因此编译器应该会产生错误。

3注意使用不可变数据结构进行变异不一定需要复制整个数据结构,如果数据结构足够智能以隔离历史记录(例如单链表)。

实现func pick(a CollectionOfT, count uint) []T将是泛型的一个很好的示例应用程序(来自 https://github.com/golang/go/issues/23717):

// pick returns a slice (len = n) of pseudorandomly chosen elements 
// in unspecified order from c which is an array, slice, or map.
for i, e := range pick(c, n) {

这里的 interface{} 方法很复杂。

我曾多次评论过这个问题,C++ 模板方法的主要问题之一是它依赖于重载解析作为编译时元编程的机制。

Herb Sutter 似乎得出了同样的结论:现在有一个有趣的提议,用于 C++ 中的编译时编程

它与 Go reflect包和我之前关于Go 编译时函数的提议有一些共同点。

你好。
我已经为 Go 的约束写了一个泛型提案。 你可以在这里阅读。 也许它可以作为 15292 的文档添加。它主要是关于约束的,并且读作是对Go 中 Taylors 类型参数的修正。
它旨在作为在 Go 中执行“类型安全”泛型的可行(我相信)方式的示例,希望它可以为这个讨论添加一些东西。
请注意,虽然我已经阅读了(大部分)这个很长的帖子,但我并没有关注其中的所有链接,所以其他人可能已经提出了类似的建议。 如果是这样,我道歉。

br。 铬。

语法bikeshedding:

constraint[T] Array {
    :[#]T
}

可能

type [T] Array constraint {
    _ [...]T
}

看起来更像是去我这里。 :-)

这里有几个元素。

一件事是将:替换为_并将#替换为...
我想如果你愿意,你可以这样做。

另一件事是将constraint[T] Array替换为type[T] Array constraint
这似乎表明约束是类型,我认为这是不正确的。 形式上,约束是所有类型集合上的一个_谓词_,即。 从类型集合到集合 { true , false } 的映射。
或者,如果您愿意,您可以将约束视为简单的_一组_类型。
它不是 _a_ 类型。

br。 铬。

为什么constraint不只是一个interface

type [T io.Writer] List struct { 
    element T; 
    next *List[T];
}

使用以下提议,接口作为约束会更有用:#23796,这反过来也会给提议本身带来一些好处。

此外,如果 sum 类型的提议以某种形式被接受(#19412),那么应该使用它们来约束类型。

虽然我相信约束关键字,但应该添加一些类似的东西,以免重复大的约束并防止由于心不在焉而出错。

最后,对于bikeshedding部分,我认为约束应该列在定义的末尾,以避免过度拥挤(rust似乎在这里有一个好主意):

// similar to the map[T]... syntax
// also no constraint
type List[T] struct {
    element T
    next *List[T]
}

// with constraint
type List[T] struct {
    element T
    next *List[T]
} where T is io.Writer | encoding.BinaryMarshaler

type BigConstraint constraint {
     io.Writer
     SomeFunc() int
     AnotherFunc()
     AField int64
     StringField string
}


// with predefined constraint
type List[T, U] struct {
    element T
    val U
    next *List[T, U]
} where T is BigConstraint | encoding.BinaryMarshaler,
    U is io.Reader

@urandom :我认为 go 隐式实现接口而不是显式实现接口的一大优势。 我认为此评论中的@surlykke提议在精神上更接近其他 Go 语法。

@surlykke如果提案对其中任何一个有答案,我深表歉意。

泛型的一个用途是允许内置样式函数。 您将如何使用此实现应用程序级 len? 每个允许的输入的内存布局都是不同的,那么这比接口更好吗?

前面描述的“pick”有一个类似的问题,即对 map 的索引与对 slice 的索引是不同的。 在地图情况下,如果先转换为切片,则可以使用相同的拾取代码,但这是如何完成的?

集合是另一种用途:

// An unordered collection of comparable items.
type [T Comparable] Set []T

func (a Set) Diff(from Set) Set {
    // the implementation is the same as one with
    //     type Comparable interface { Equal(Comparable) bool }
    //     type Set []Comparable
}

// compile error
d := Set[int]{1, 2}.Diff(Set[string]{“abc”, “def”})

// Go 1, easier to read but runtime error
d := Set{1, 2}.Diff(Set{“abc”, “def”})

对于集合类型的情况,我不相信这是对 Go 1 泛型的巨大胜利,因为存在可读性权衡。

我同意类型参数必须具有某种形式的约束。 否则我们将重复 C++ 模板的错误。 问题是,约束的表现力应该如何?

一方面,我们可以只使用接口。 但正如您所指出的,许多有用的模式无法以这种方式捕获。

然后是您的想法和类似的想法,它们试图划分出一组有用的约束并提供新的语法来表达它们。 除了添加更多语法的问题之外,还不清楚在哪里停止。 正如您所指出的,您的提案包含了许多模式,但绝不是全部。

另一个极端是我在这个文档中提出的想法。 它使用 Go 代码本身作为约束语言。 您几乎可以通过这种方式捕获任何约束,并且不需要新的语法。

@jba
这有点冗长。 也许如果 Go 有 lambda 语法,它会更容易接受。 另一方面,它试图解决的最大问题似乎是检查一个类型是否支持某种运算符。 如果 Go 只为各种运算符预定义接口,可能会更容易:

func equal[T](x, y T) bool
    where T is runtime.Equitable {
    return x == y
}

func copyable[T](x, y []T) int {
    return copy(x, y)
}

或类似的东西。

如果问题出在扩展内置函数上,那么问题可能出在语言创建适配器类型的方式上。 例如,与 sort.Interface 相关的膨胀不是https://github.com/golang/go/issues/16721和 sort.Slice 背后的全部原因吗?
查看https://github.com/golang/go/issues/21670#issuecomment -325739411, @Sajmani拥有接口文字的想法可能是类型参数轻松使用内置函数所必需的要素。
看下面Iterator的定义:

type [T] Iterator interface {
    Next() (elem T, done bool)
}

如果print是一个简单地遍历列表并打印其内容的函数,那么下面的示例使用接口文字为print构造一个令人满意的接口。

func SliceIterator(slice []T) Iterator {
    i := 0
    return Iterator{
        Next: func() (elem int, done bool) {
            v := slice[i]
            if i+1 == len(slice) {
                return v, true
            }
            i++
            return v, false
        },
    }
}

func main() {
    arr := []int{1,2,3,4,5}
    // SliceIterator works for an arbitrary slice
    print(SliceIterator(arr))
}

如果他们全局声明其唯一职责是满足接口的类型,则已经可以做到这一点。 然而,这种从函数到方法的转换使得接口(以及因此的“约束”)更容易满足。 我们不会使用简单的适配器(如排序中的“widgetsByName”)污染顶级声明。
用户定义的类型显然也可以利用此功能,如以下 LinkedList 示例所示:

type ListNode struct {
    v string
    next *ListNode
}
func (l *ListNode) Iterator() Iterator {
    ptr := l
    return Iterator{
        Next: func() (elem int, done bool) {
            v := ptr.v
            if ptr.next == nil {
                return v, true
            }
            ptr = ptr.next
            return v, false
        },
    }
}

@geovanisouza92 :我所描述的约束比接口(字段、运算符)更具表现力。 我确实曾短暂地考虑过扩展接口而不是引入约束,但我认为这对 Go 的现有元素来说过于侵入性了。

@pciet我不太确定“应用程序级别”是什么意思。 Go 有一个内置的len函数,它可以应用于数组、指向数组的指针、切片、字符串和通道,因此,在我的建议中,如果类型参数被限制为其中之一,因为它是底层类型, len可能适用于它。

@pciet关于您的示例,带有Comparable约束/接口。 请注意,如果您定义(接口变体):

type Comparable interface { Equal(Comparable) bool }
type Set []Comparable

然后,您可以将任何实现Comparable的内容放入Set 。 将其与以下内容进行比较:

constraint [T] Comparable { Equal(t T) bool }
type [T Comparable[T]] Set []T
...
type FooSet Set[Foo] // Where Foo satisfies constraint Comparable

您只能将Foo类型的值放入FooSet 。 那是更强的类型安全性。

@urandom同样,我不喜欢:

type MyConstraint constraint {....}

因为我不相信一个约束是一种类型。 另外,我绝对不允许:

var myVar MyConstraint

这对我来说毫无意义。 约束不是类型的另一个迹象。

@urandom Onbikeshedding:我相信约束应该在类型参数旁边声明。 考虑一个普通函数,定义如下:

func MyFunc(i) {
     if (i>0) fmt.Println("It's positive")
} with i being an integer

您无法从左到右阅读此内容。 相反,您首先阅读func MyFunc(i)以确定它是一个函数定义。 然后你必须跳到最后弄清楚i是什么,然后回到函数体。 不理想,IMO。 而且我看不出通用定义应该有什么不同。
但显然,这个讨论与关于 Go 是否应该有约束或泛型的讨论是正交的。

@surlykke
我很好,它不是一种类型。 最重要的是它们有一个名称,以便它们可以被多种类型引用。

对于函数,如果我们遵循 rust 语法,它将是:

func MyFunc[I](i I) int64
     where I is being an integer {
   return 42
}

因此它不会隐藏函数名称或其参数之类的内容,并且您无需到函数体的末尾查看泛型类型的约束是什么

@surlykke后人,您能否找到可以将您的提案添加到的位置:
https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4

这是一个“编译”所有提案的好地方。

我向大家提出的另一个问题是如何处理泛型类型的不同实例化的专门化。 在type-params提案中,这样做的方法是为每个实例化类型生成相同的模板化函数,将类型参数替换为类型名称。 为了对不同类型具有单独的功能,请在类型参数上执行类型切换。

可以安全地假设当编译器在类型参数上看到类型开关时,它可以为每个断言生成单独的实现吗? 或者这是否涉及优化,因为断言结构中的嵌套类型参数可能会为代码生成创建参数方面?

编译时函数提案中,因为我们知道这些声明是在编译时生成的,所以类型切换不会带来任何运行时成本。

一个实际的场景:如果我们考虑math/bits包的情况,执行一个类型断言来为每个uintXX调用OnesCount $ 将超过拥有一个高效的位操作库的意义。 但是,如果类型断言被转换为以下

func OnesCount(x T) int {
    switch x.(type) {
    case uint:
        // separate uint functionality...
    case uint8:
        // separate uint8 functionality...
    case uint16:
        // separate uint16 functionality...
    case uint32:
        // separate uint32 functionality...
    case uint64:
        // separate uint64 functionality...
    }
}

打电话给

var x uint8 = 255
bits.OnesCount(x)

然后将调用以下生成的函数(名称在这里并不重要):

func $OnesCount_uint8(x uint8) {
    // separate uint8 functionality...
}

@jba这是一个有趣的提议,但对我来说,它主要强调了参数函数本身的定义通常足以定义其约束的事实。

如果您打算使用“函数中使用的运算符”作为约束,那么编写包含第一个中使用的运算符子集的第二个函数会给您带来什么好处?

@bcmills其中一个是规范,另一个是实现。 这与静态类型的优势相同:您可以更早地发现错误。

如果实现是规范,à la C++ 模板,那么对实现的任何更改都可能破坏依赖项。 这可能直到很久以后才被发现,当依赖项重新编译时,发现者没有上下文来理解错误消息。 使用同一个包中的规范,您可以在本地检测破损。

@mandolyte我不太确定在哪里添加它-也许是“泛型方法”下的一个名为“带约束的泛型”的段落?
该文档似乎没有包含太多关于约束类型参数的内容,因此如果您添加了一个段落来提及我的建议,那么其他约束方法也可以在其中列出。

@surlykke对文档的一般方法是做出感觉正确的更改,我将尝试接受、合并和组织它与文档的其余部分。 我在这里添加了一个部分。 随意添加我错过的东西。

@egonelbre非常好。 谢谢!

@jba
我喜欢你的建议,但我认为这对 golang 来说太重了。 它让我想起了很多 c++ 中的模板。 我认为主要问题是你可以用它编写非常复杂的代码。
要确定两个通用接口实例是否重叠,因为受约束的类型集重叠将是一项艰巨的任务,导致编译时间变慢。 代码生成也是如此。

我认为建议的约束对于 go 来说更轻量级。 据我所知,约束又名类型类可以与语言的类型系统正交地实现。

我必须强烈同意我们不应该使用来自函数体的隐式约束。 它们被广泛认为是 C++ 模板最重要的错误功能之一:

  • 约束不容易看到。 虽然 godoc 理论上可以将所有约束枚举到文档中,但它们在源代码中是不可见的,除非是隐式的。
  • 因此,可能会意外包含一个附加约束,该约束仅在您尝试以非预期方式使用该函数时才可见。 通过要求明确说明约束,程序员必须确切地知道他们正在引入什么约束。
  • 它使关于允许哪些类型的约束的决定更加临时。 例如,我可以定义以下函数吗? 这里对 T、U 和 V 的实际限制是什么? 如果我们要求程序员明确指定约束,那么我们在允许的约束类型上是保守的(让我们缓慢而有意识地扩展它)。 如果我们仍然尝试保守,我们如何为这样的函数提供错误消息? “错误:无法将 uv() 分配给 T,因为它施加了非法约束”?
func[T, U, V] Foo(u U, v V) {
  var t T = u.v(V) + 1;
}
  • 在其他泛型函数中调用泛型函数会使上述情况变得更糟,因为您现在需要查看所有被调用者的约束以了解您正在编写或读取的函数的约束。
  • 调试可能非常困难,因为错误消息要么不能提供足够的信息来找到约束的来源,要么必须泄露函数的内部细节。 例如,如果F对类型T $ 有一些要求,并且F的作者试图找出该要求的来源,他们希望编译器提醒他们究竟是哪个语句引起了约束(特别是如果它来自通用被调用者)。 但是F的用户不想要该信息,实际上,如果它包含在错误消息中,那么我们会在其用户的错误消息中泄露F的实现细节,这是糟糕的用户体验。

@alercah

例如,我可以定义以下函数吗?

func[T, U, V] Foo(u U, v V) {
  var t T = u.v(V) + 1;
}

u.v(V)是语法错误,因为V是一种类型,而变量t未使用。

但是,您可以定义此函数,这可能是您想要的:

func[T, U, V] Foo(u U, v V) {
    var _ T = u.v(v) + 1;
}

这里对 T、U 和 V 的实际限制是什么?

  • V类型不受约束。
  • 类型U必须有一个方法v接受单个参数或可从V分配的某种类型的可变参数,因为u.v是使用单个参数调用的V类型。

    • U.v可以是函数类型的字段,但可以说这应该暗示一个方法; 见#23796。

  • U.v返回的类型必须是数字,因为它添加了常量1
  • U.v的返回类型必须可分配给T ,因为u.v(…) + 1已分配给T类型的变量。
  • T类型必须是数字,因为U.v的返回类型是数字并且可以分配给T

(顺便说一句:您可以争辩说UV应该具有“可复制”约束,因为这些类型的参数是按值传递的,但是现有的非泛型类型系统不强制执行那个约束也是。这是一个单独的提案的问题。)

如果我们要求程序员明确指定约束,那么我们在允许的约束类型上是保守的(让我们缓慢而有意识地扩展它)。

是的,这是真的:但是无论这些约束是否是隐式的,忽略一个约束都是一个严重的缺陷。 IMO,约束更重要的作用是解决歧义。 例如,在上述约束中,编译器必须准备好将u.v实例化为单参数或可变参数方法。

最有趣的歧义发生在文字上,我们需要在结构类型和复合类型之间消除歧义:

func[T] Foo() (t T) {
    x := 42;
    t = T{x: "some string"}  // Is x an index, or a field name?
    _ = x
}

如果我们仍然尝试保守,我们如何为这样的函数提供错误消息? “错误:无法将 uv() 分配给 T,因为它施加了非法约束”?

我不太确定你在问什么,因为我没有看到这个例子有冲突的约束。 “非法约束”是什么意思?

调试可能非常困难,因为错误消息要么不能提供足够的信息来找到约束的来源,要么必须泄露函数的内部细节。

并非每个相关约束都可以由类型系统表示(另请参见 https://github.com/golang/go/issues/22876#issuecomment-347035323)。 一些约束是由运行时恐慌强制执行的; 有些是由竞赛检测器强制执行的; 最危险的限制只是记录在案,根本没有检测到。

在某种程度上,所有这些“泄露内部细节”。 (另见 https://xkcd.com/1172/。)

例如,如果 [...] F 的作者试图找出该要求的来源,他们希望编译器准确地提醒他们哪个语句引起了约束(特别是如果它来自通用被调用者)。 但是 F 的用户不想要这些信息[.]

或许? 这就是 API 作者在诸如 Haskell 和 ML 等类型推断语言中使用类型注释的方式,但它通常也会导致深度参数(“高阶”)类型的兔子洞。

例如,假设你有这个功能:

func [F, Arg, Result] InvokeAsync(f F, x Arg) (<-chan Result) {
    c := make(chan result, 1)
    go func() { c <- f(x) }()
    return c
}

你如何表达对类型Arg的显式约束? 它们取决于F的具体实例化。 最近的许多约束提议似乎都缺少这种依赖性。

不。 uv(V) 是语法错误,因为 V 是一种类型,变量 t 未使用。

但是,您可以定义此函数,这可能是您想要的:

是的,这是我的意图,我很抱歉。

T类型必须是数字,因为U.v的返回类型是数字并且可以分配给T

我们真的应该认为这是一个约束吗? 它可以从其他约束中推导出来,但是将其称为不同的约束或多或少有用吗? 隐式约束以显式约束没有的方式提出这个问题。

是的,这是真的:但是无论这些约束是否是隐式的,忽略一个约束都是一个严重的缺陷。 IMO,约束更重要的作用是解决歧义。 例如,在上述约束中,编译器必须准备好将 uv 实例化为单参数或可变参数方法。

我的意思是语言中的“我们允许的限制”。 有了显式约束,我们更容易决定我们愿意允许用户编写什么样的约束,而不是仅仅说约束是“无论什么东西都能编译”。 例如,我上面的示例Foo实际上涉及与TUV分开的隐式附加类型,因为我们必须考虑返回类型u.v 。 这种类型在f的声明中没有明确地被引用; 它必须具有的属性是完全隐含的。 同样,我们是否愿意允许排名较高的( forall )类型? 我无法想出一个例子,但我也无法说服自己你不能隐式地编写更高级别的类型绑定。

另一个例子是我们是否应该允许函数利用重载语法。 如果一个隐式约束函数对某些泛型Tt执行 $#$ for i := range t $#$ ,则如果T是任何数组、切片、通道、或地图。 但是语义完全不同,特别是如果T是通道类型。 例如,如果t == nil (只要T是一个数组就可能发生),那么迭代要么什么都不做,因为 nil 切片或映射中没有元素,要么永远阻塞因为这就是在nil频道上收到的内容。 这是一个等待发生的大脚枪。 同样在做m[i] = ... ; 如果我打算将m用作地图,我将需要防止它实际上是一个切片,否则代码可能会因超出范围的分配而恐慌。

事实上,我认为这有助于反对隐式约束的另一个论点:API 作者可能编写人工语句只是为了添加约束。 例如for _, _ := range t { break }阻止通道,同时仍然允许映射、切片和数组; x = append(x)强制x具有切片类型。 var _ = make(T, 0)允许切片、映射和通道,但不允许数组。 将有一本关于如何隐式添加约束的食谱书,以便有人无法使用您尚未编写正确代码的类型调用您的函数。 除非我也知道键类型,否则我什至想不出一种方法来编写只为映射类型编译的代码。 而且我认为这根本不是假设。 对于大多数应用程序来说,地图和切片的行为完全不同

我不太确定你在问什么,因为我没有看到这个例子有冲突的约束。 “非法约束”是什么意思?

我的意思是语言不允许的约束,例如如果语言决定不允许更高级别的约束。

并非每个相关约束都可以由类型系统表示(另请参见#22876(评论))。 一些约束是由运行时恐慌强制执行的; 有些是由竞赛检测器强制执行的; 最危险的限制只是记录在案,根本没有检测到。

在某种程度上,所有这些“泄露内部细节”。 (另见 https://xkcd.com/1172/。)

我真的不明白#22876 是如何出现的; 那是试图使用类型系统来表达一种不同的约束。 即使使用任意复杂的类型系统,我们也无法表达对值或程序的某些约束,这将始终是正确的。 但我们在这里只讨论类型的约束。 编译器需要能够回答“我可以用类型T实例化这个泛型吗?”这个问题。 这意味着它必须理解约束,无论它们是隐式的还是显式的。 (请注意,某些语言,如 C++ 和 Rust,一般不能决定这个问题,因为它可能依赖于任意计算,因此会转移到停机问题,但它们仍然表达了需要满足的约束。)

我的意思更像是“以下示例应该给出什么错误消息?”

func [U] DirectlyConstrained(U t) {
    t.DoSomething();
}
func [T] IndirectlyConstrained(T t) {
    DirectlyConstrainted(t);
}
func Illegal() {
    IndirectlyConstrained(4);
}

我们可以说Error: cannot call IndirectlyConstrained with [T = int]; T must have a method with signature func (T t) DoSomething() 。 此错误消息对IndirectlyConstrained的用户很有帮助,因为它清楚地列出了它们缺少的约束。 但它没有向试图调试为什么IndirectlyConstrained具有该约束的人提供任何信息,如果它是一个大函数,这是一个很大的可用性问题。 我们可以添加Note: this constraint exists on T because IndirectlyConstrained calls DirectlyConstrained with [U = T] on line N ,但现在我们泄露了IndirectlyConstrained的实现细节。 除此之外,我们还没有解释为什么IndirectlyConstrained有约束,所以我们要再添加一个Note: this constraint exists on U because DirectlyConstrained calls t.DoSomething() on line M吗? 如果隐式约束来自调用堆栈下四层的某些被调用者怎么办?

此外,对于未明确列为参数的类型,我们如何格式化此错误消息? 例如,如果在上面的示例中, IndirectlyConstrained调用DirectlyConstrained(t.U()) 。 我们甚至如何引用类型? 在这种情况下,我们可以说the type of t.U() ,但值不一定是单个表达式的结果; 它可以建立在多个语句上。 然后我们要么需要合成一个具有正确类型的表达式以放入错误消息中,一个从未出现在代码中的错误消息,要么我们需要找到一些其他方式来引用它,这对用户来说不太清楚违反约束的可怜的来电者。

你如何表达对类型 Arg 的显式约束? 它们依赖于 F 的具体实例化。最近的许多约束提议似乎都缺少这种依赖关系。

删除F并将f的类型设为func (Arg) Result 。 是的,它忽略了可变参数函数,但 Go 的其余部分也忽略了。 可以单独完成将可变参数funcs分配给兼容签名的提议。

对于我们真正需要高阶类型边界的情况,将它们包含在泛型 v1 中可能有意义,也可能没有意义。 显式约束确实迫使我们明确决定是否要支持高阶类型以及如何支持。 我认为到目前为止缺乏考虑是 Go 目前无法引用内置类型的属性的一个症状。 这是一个普遍的开放性问题,即任何泛型系统如何允许函数对所有数字类型或所有整数类型都具有泛型,并且大多数提案都没有过多关注这一点。

请在您的下一个项目中评估我的泛型实现
http://go-li.github.io/

我们可以说Error: cannot call IndirectlyConstrained with [T = int]; T must have a method with signature func (T t) DoSomething() 。 此错误消息 [...] 没有向尝试调试IndirectlyConstrained为何具有该约束的人提供任何信息,如果它是一个大函数,这是一个很大的可用性问题。

我想指出您在这里所做的一个重要假设:来自go build的错误消息是程序员可以用来诊断问题的_only_工具。

打个比方:如果你在运行时遇到error ,你有几个调试选项。 错误本身只包含一个简单的消息,它可能足以或可能不足以描述错误。 但这不是您可以获得的唯一信息:例如,您还有程序发出的任何日志语句,如果它是一个非常棘手的错误,您可以将其加载到交互式调试器中。

也就是说,运行时调试是一个交互过程。 那么我们为什么要为编译时错误假设非交互式调试呢⸮ 作为一种替代方法,我们可以教guru工具有关类型约束的知识。 然后,编译器的输出将类似于:

somefile.go:123: Argument `4` to DirectlyConstrained has type `int`,
    but DirectlyConstrained requires a type `T` with method `DoSomething()`.
    (For more detail, run `guru contraints path/to/somefile.go:#1033`.)

这为通用包的用户提供了调试即时调用站点所需的信息,但_也_为包维护者(重要的是,他们的编辑环境!)提供了进一步调查的路径。

我们可以添加Note: this constraint exists on T because IndirectlyConstrained calls DirectlyConstrained with [U = T] on line N ,但现在我们泄露了IndirectlyConstrained的实现细节。

是的,这就是我所说的信息泄露的意思。 您已经可以使用guru describe来查看实现。 您可以使用调试器查看正在运行的程序,不仅可以查看堆栈,还可以进入任意低级函数。

我绝对同意我们应该隐藏可能不相关的信息_默认情况下_,但这并不意味着我们必须绝对隐藏它。

如果隐式约束函数对i := range t的某些泛型类型T的 $#$ t $#$ 执行,则如果T是任何数组、切片、通道,则语法有效,或地图。 但是语义是完全不同的,特别是如果T是一个通道类型。

我认为这是类型约束更有说服力的论据,但这并不要求显式约束像某些人所提议的那样冗长。 为了消除呼叫站点的歧义,将类型参数限制为更接近reflect.Kind似乎就足够了。 我们不需要描述从代码中已经清楚的操作; 相反,我们只需要说“ T是一个切片类型”之类的话。 这导致了一组更简单的约束:

  • 受索引操作影响的类型需要标记为线性或关联,
  • range操作影响的类型需要标记为 nil-empty 或 nil-blocking,
  • 具有文字的类型需要标记为具有字段或索引,并且
  • (也许)具有数字运算的类型需要标记为定点或浮点。

这导致了一个更窄的约束语言,可能是这样的:

TypeConstraint = "sliceable" | "map" | "chan" | "struct" | "integer" | "float" | "type"

例如:

func[T:integer, U, V] Foo(u U, v V) {
    var _ T = u.v(v) + 1;
}
func [S:sliceable, T] append(s S, x ...T) S {
    dst := s
    if cap(s) - len(s) < len(x) {
        dst = make(S, len(s), nextSizeClass(cap(s)))
        copy(dst, s)
    }
    copy(dst[len(s):cap(s)], x)
    return dst[:len(s)+len(x)]
}

我觉得我们通过引入类型别名向自定义泛型迈出了一大步。
类型别名使超类型(类型的类型)成为可能。
我们可以在使用中将类型视为值。

为了使解释更简单,我们可以添加一个新的代码元素genre
体裁与类型之间的关系就像类型与价值之间的关系。
换句话说,流派意味着类型的类型。

每种类型,除了结构和接口和函数类型,都对应一个预先声明的类型。

  • 布尔
  • 细绳
  • Int8,Uint8,Int16,Uint16,Int32,Uint32,Int64,Uint64,Int,Uint,Uintptr
  • 浮点 32、浮点 64
  • 复杂64、复杂128
  • 数组、切片、映射、通道、指针、UnsafePointer

还有一些其他预先声明的流派,例如 Comaprable、Numeric、Interger、Float、Complex、Container 等。我们可以使用Type*表示所有类型的流派。

所有内置流派的名称都以大写字母开头。

每个结构和接口以及函数类型对应一个流派。

我们还可以声明自定义类型:

genre Addable = Numeric | String
genre Orderable = Interger | Float | String
genre Validator = func(int) bool // each parameter and result type must be a specified type.
genre HaveFieldsAndMethods = {
    width  int // we must use a specific type to define the fields.
    height int // we can't use a genre to define the fields.
    Load(v []byte) error // each parameter and result type must be a specified type.
    DoSomthing()
}
genre GenreFromStruct = aStructType // declare a genre from a struct type
genre GenreFromInterface = anInterfaceType // declare a genre from an interface type
genre GenreFromStructInterface = aStructType + anInterfaceType
genre ComparableStruct = HaveFieldsAndMethods & Comprable
genre UncomparableStruct = HaveFieldsAndMethods &^ Comprable

为了使以下解释保持一致,需要一个流派修饰符。
流派修饰符由Const表示。 例如:

  • Const Integer是一种类型(不同于Integer ),它的实例必须是一个常量值,其类型必须是一个整数。 但是,常量值可以被视为一种特殊类型。
  • Const func(int) bool是一种流派(不同于func(int) bool ),它的实例必须是一个 decared 函数值。 但是,函数声明可以被视为一种特殊类型。

(修改器解决方案有些棘手,也许还有其他更好的设计解决方案。)

好的,让我们继续。
我们需要另一个概念。 为它取个好名字并不容易,
我们就叫它crate吧。
一般来说, crates 和流派之间的关系就像函数和类型之间的关系。
crate 可以将类型作为参数并返回类型。

一个 crate 声明(假设以下代码在lib包中声明):

crate Example [T Float, S {width, height T}, N Const Integer] [*, *, *] {
    type MyArray [N]T

    func Add(a, b T) T {
        return a+b
    }

    type M struct {
        x T
        y S
    }

    func (m *M) Area() T {
        m.DoSomthing()
        return m.y.width * m.y.height
    }

    func (m *M) Perimeter() T {
        return 2 * Add(m.y.width, m.y.height)
    }

    export M, Add, MyArray
}

使用上面的箱子。

import "lib"

// We can use AddFunc as a normal delcared function.
// Its genre is "Const func (a, b T) T"
type Rect, AddFunc, Array = lib.Example[float32, struct{x, y float32}, 100]

func demo() {
    var r Rect
    a, p = r.Area(), r.Perimeter()
    _ = AddFunc(a, p)
}

我的想法吸收了上面显示的其他人的许多想法。
他们现在还很不成熟。
我把它们贴在这里只是因为我觉得它们很有趣,
我不想再改进它了。
通过修复想法中的漏洞,许多脑细胞被杀死。
我希望这些想法能给其他地鼠带来一些启发。

你所说的“流派”其实就是“种类”,在
函数式编程社区。 你所说的板条箱是受限制的
一种 ML 函子。

2018 年 4 月 4 日,星期三,下午 12:41 dotaheor [email protected]写道:

我觉得我们通过引入自定义泛型迈出了一大步
键入别名。
类型别名使超类型(类型的类型)成为可能。
我们可以在使用中将类型视为值。

为了使解释更简单,我们可以添加一个新的代码元素,genre。
体裁与类型之间的关系就像类型之间的关系
和价值观。
换句话说,流派意味着类型的类型。

每种类型,除了结构和接口和函数类型,
对应于预先声明的类型。

  • 布尔
  • 细绳
  • Int8,Uint8,Int16,Uint16,Int32,Uint32,Int64,Uint64,Int,Uint,
    uintptr
    & 浮点 32、浮点 64
  • 复杂64、复杂128
  • 数组、切片、映射、通道、指针、UnsafePointer

还有一些其他预先声明的类型,例如 Comaprable、Numeric、
Interger、Float、Complex、Container 等。我们可以使用 Type 或 * 表示
所有类型的流派。

所有内置流派的名称都以大写字母开头。

每个结构和接口以及函数类型对应一个流派。

我们还可以声明自定义类型:

类型可添加 = 数字 | 细绳
流派可订购 = 整数 | 浮动 | 细绳
Genre Validator = func(int) bool // 每个参数和结果类型都必须是指定的类型。
流派 HaveFieldsAndMethods = {
width int // 我们必须使用特定类型来定义字段。
height int // 我们不能使用流派来定义字段。
Load(v []byte) error // 每个参数和结果类型都必须是指定的类型。
DoSomthing()
}
GenreFromStruct = aStructType // 从结构类型声明一个流派
GenreFromInterface = anInterfaceType // 从接口类型声明一个流派
流派 GenreFromStructInterface = aStructType | 接口类型

为了使以下解释保持一致,需要一个流派修饰符。
类型修饰符由 Const 表示。 例如:

  • Const Integer 是一种类型,它的实例必须是一个常量值
    哪个类型必须是整数。
    但是,常量值可以被视为一种特殊类型。
  • const func(int) bool 是一个流派,它的实例必须是一个 delcared
    函数值。
    但是,函数声明可以被视为一种特殊类型。

(修改器解决方案有些棘手,也许还有其他更好的设计
解决方案。)

好的,让我们继续。
我们需要另一个概念。 为它取个好名字并不容易,
我们就叫它箱子吧。
一般来说,箱子和流派之间的关系就像关系
函数和类型之间。
crate 可以将类型作为参数并返回类型。

一个 crate 声明(假设在 lib 中声明了以下代码
包裹):

crate 示例 [T Float, S {width, height T}, N Const Integer] [*, *, *] {
键入 MyArray [N]T

函数添加(a,b T)T {
返回 a+b
}

// 一个 crate-scope 类型。 只能在板条箱中使用。

// M 是流派 G 的一种类型
类型 M 结构 {
x T

}

func (m *M) 面积() T {
m.DoSomthing()
返回我的宽度 * 我的高度
}

func (m *M) 周长() T {
返回 2 * 添加(我的宽度,我的高度)
}

导出 M、添加、MyArray
}

使用上面的箱子。

导入“库”

// 我们可以将 AddFunc 用作普通的 delcared 函数。
类型 Rect, AddFunc, Array = lib.Example(float32, struct{x, y float32})

函数演示(){
var r 矩形
a, p = r.Area(), r.Perimeter()
_ = AddFunc(a, p)
}

我的想法吸收了上面显示的其他人的许多想法。
他们现在还很不成熟。
我把它们贴在这里只是因为我觉得它们很有趣,
我不想再改进它了。
通过修复想法中的漏洞,许多脑细胞被杀死。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/15292#issuecomment-378665695或静音
线程
https://github.com/notifications/unsubscribe-auth/AGGWB78BrjN0BxRfroH-jRNy4mCXgSwCks5tlPfMgaJpZM4IG-xv
.

我觉得种类和流派之间存在一些差异。

顺便说一句,如果一个 crate 只返回一种类型,我们可以直接将它的调用用作一个类型。

package lib

// export a type
crate List [T *] * {
    type List struct {
        ...
    }

    export List
}

用它:

import "lib"

var l lib.List[int]

会有一些“类型扣除”规则,就像当前系统中的“类型扣除”一样。

@dotaheor@DemiMarie是正确的。 您的“类型”概念听起来与类型理论中的“种类”完全一样。 (您的提议恰好需要一个子类规则,但这并不少见。)

您提案中的genre关键字将新种类定义为现有种类的超种类。 crate关键字定义了带有“crate 签名”的对象,它不是Type的子类。

作为一个正式的系统,您的建议似乎是这样的:

板条箱 ::= χ | ⋯
输入 ::= τ | χ | int | bool | ⋯ | func(τ) | func(τ) τ | []τ | χ[τ₁, …]

CrateSig ::= [κ₁, …] ⇒ [κₙ, …]
种类 ::= κ | exactly τ | kindOf κ | Map | Chan | ⋯ | Const κ | Type | 板条箱签名

滥用一些类型论符号:

  • 将“⊢”读作“包含”。
  • 将“ k1k2 ”读作“ k1k2的子类”。
  • 读“:”为“是善良的”。

然后规则看起来像:

τ : exactly τ
exactly τkindOf exactly τ
kindOf exactly τType

τ : κ₁κ₁κ₂τ : κ₂

τ₁ : Typeτ₂ : TypekindOf exactly map[τ₁]τ₂Map
MapType

κ₁κ₂Const κ₁Const κ₂

[…]
(等等,对于所有内置类型)


类型定义赋予种类,并且底层种类折叠为内置类型的种类:

type τ₁ τ₂τ₂ : κτ₁ : kindOf κ

kindOf kindOf κkindOf κ
kindOf MapMap
[…]


genre定义了新的子类型关系:
genre κ = κ₁ | κ₂κ₁κ
genre κ = κ₁ | κ₂κ₂κ

(您可以根据|定义Numeric等。)

genre κ = κ₁ & κ₂ ∧ ( κ₃κ₁ ) ∧ ( κ₃κ₂ ) ⊢ κ₃κ


crate 扩展规则类似:
type τₙ, … = χ[τ₁, …] ∧ ( χ : [κ₁, …] ⇒ [κₙ, …] ) ∧ ( τ₁ : κ₁ ) ∧ ⊢ τₙ : κₙ

当然,这一切都只是在谈论种类。 如果你想把它变成一个类型系统,你还需要类型规则。 🙂


因此,您所描述的是一种非常容易理解的参数化形式。 这很好,因为它很容易理解,但令人失望的是它无助于解决 Go 引入的独特问题。

Go 引入的真正有趣和棘手的问题主要围绕动态类型检查。 类型参数应该如何与类型断言和反射交互?

(例如,是否可以使用参数类型的方法定义接口?如果可以,如果您在运行时使用新参数类型断言该接口的值会发生什么?)

在相关的说明中,是否讨论过如何使代码在内置和用户定义的类型上通用? 比如制作可以处理大整数和原始整数的代码?

在相关的说明中,是否讨论过如何使代码在内置和用户定义的类型上通用? 比如制作可以处理大整数和原始整数的代码?

基于类型类的机制,例如在 Genus 和 Familia 中,可以有效地做到这一点。 有关详细信息,请参阅我们的 PLDI 2015 论文。

@DemiMarie
我认为“流派”==“特征集”。

[编辑]
也许traits是一个更好的关键字。
我们可以查看每一种也是一个特征集。

大多数特征仅针对单一类型定义。
但是更复杂的特征可能会定义两种类型之间的关系。

[编辑 2]
假设有两个 trait set A 和 B,我们可以做如下操作:

A + B: union set
A - B: difference set
A & B: intersection set

参数类型的特征集必须是相应参数类型的超集(特征集)。
结果类型的特征集必须是相应结果类型的子集(特征集)。

(恕我直言)

我仍然认为重新绑定类型别名是向 Go 添加泛型的方法。 它不需要对语言进行巨大的改变。 以这种方式概括的包仍然可以在 Go 1.x 中使用。 并且不需要添加约束,因为可以通过将类型别名的默认类型设置为已经满足这些约束的东西来实现。 重新绑定类型别名最重要的方面是,内置的复合类型(切片、映射和通道)不需要更改和泛化。

@dc0d

类型别名应该如何替换泛型?

@sighoya重新绑定类型别名可以替换泛型(不仅仅是类型别名)。 让我们假设一个包引入了一些包级类型别名,例如:

package likedlist

type T = interface{}

type LinkedList struct {
    // ...
}

如果提供了类型别名重新绑定(和编译器工具),则可以使用此包为不同的具体类型创建链表,而不是空接口:

package main

import (
    "likedlist"
)

type intLL = likedlist.LinkedList(likedlist.T = int)
type stringLL = likedlist.LinkedList(likedlist.T = string)

func main() {}

如果我们这样使用别名,则以下方式更干净。

// pkg.go
package pkg

type ListNode struct {
    prev, next *ListNode
    element    ?Element
}

func Add(x, y ?T) ?T {
    return x+y
}



// main.go
package main

import "pkg"

type intList = pkg.ListNode[Element=int]
func stringAdd = pkg.Add[T=string]

func main() {
}

@dc0d以及这将如何实现? 代码很好,但它并没有说明它实际上是如何工作的。 而且,看看泛型提案的历史,对于 Go 来说,它非常重要,而不仅仅是它的外观和感觉。

@dotaheor这与 Go 1.x 不兼容。

@creker我已经实现了一个工具(名为goreuse ),它使用这种技术生成代码,并作为类型别名重新绑定的概念而诞生。

可以在这里找到。 有一个 15 分钟的视频来解释该工具。

@dc0d所以它的工作方式有点像生成专门实现的 C++ 模板。 我不认为它会被接受,因为 Go 团队(坦率地说,我和这里的许多其他人)似乎反对任何类似于 C++ 模板的东西。 它增加了二进制文件,减慢了编译速度,可能无法产生有意义的错误。 而且,最重要的是,它与 Go 支持的仅二进制包不兼容。 这就是 C++ 选择在头文件中编写模板的原因。

@creker

所以它的工作方式有点像 C++ 模板,为每种使用的类型生成专门的实现。

我不知道(我写任何 C++ 已经有 16 年了)。 但从你的解释看来是这样。 但是我不确定它们是否相同或如何相同。

我不认为它会被接受,因为 Go 团队(坦率地说,我和这里的许多其他人)似乎反对任何类似于 C++ 模板的东西。

当然,这里的每个人都有充分的理由根据他们的优先事项来选择他们的偏好。 首先是与 Go 1.x 的兼容性。

它增加了二进制文件,

它可能。

减慢编译速度,

我非常怀疑这一点(因为它可以通过goreuse体验到)。

而且,最重要的是,它与 Go 支持的仅二进制包不兼容。

我不确定。 其他实现泛型的方法是否支持这一点?

可能无法产生有意义的错误。

这可能有点麻烦。 它仍然发生在编译时,并且可以在很大程度上使用一些工具进行补偿。 此外,如果作为包的类型参数的类型别名是一个接口,那么可以简单地检查它是否可以从具体提供的类型中分配。 尽管像intstring和结构这样的原始类型的问题仍然存在。

@dc0d

我想了一下。
除此之外,它是在接口内部建立的,您示例中的“T”

type T=interface{}

被视为可变类型变量,但它应该是特定类型的别名,即对类型的 const 引用。
您想要的是 T 类型,但这意味着引入泛型。

@sighoya我不确定我是否理解你所说的。

它内部建立在接口上

不对。 正如我在原始评论中所描述的,可以使用满足约束的特定类型。 例如类型参数类型别名可以声明为:

type T = int

只有具有+运算符(或-* ;取决于该运算符是否在包的主体中使用)的类型才能用作类型值位于该类型参数中。

所以不仅仅是接口可以用作类型参数的占位符。

但这意味着引入泛型。

这是在 Go 语言本身中引入/实现泛型的一种方式。

@dc0d

为了提供多态性,您将使用 interface{} 因为这允许稍后将 T 设置为任何类型。

设置 'type T=Int' 不会有太大的收获。

如果您会说“类型 T”首先是未声明/未定义的,可以稍后设置,那么您就有了泛型之类的东西。

它的问题是'T'包含模块/包范围并且不是任何函数或结构的本地(好的,可能是可以从外部访问的结构中的嵌套类型声明)。

为什么不写呢?:

fun<type T>(t T)

或者

fun[type T](t T)

此外,我们需要一些类型推断机制来在调用没有类型参数特化的泛型函数或结构时推断出正确的类型。

@dc0d写道

只有具有 + 运算符(或 - 或 *; 取决于该运算符是否在包的主体中使用)的类型才能用作位于该类型参数中的类型值。

你能详细说明一下吗?

@sighoya

为了提供多态性,您将使用 interface{} 因为这允许稍后将 T 设置为任何类型。

当重新绑定类型别名时,多态性不是通过具有兼容类型来实现的。 唯一实际的约束是通用包的主体。 它们必须在机械上兼容。

你能详细说明一下吗?

例如,如果包级别类型参数类型别名定义如下:

package genericadd

type T = int

func Add(a, b T) T { return a + b }

然后几乎所有数字类型都可以分配给T ,例如:

package main

import (
    "genericadd"
)

var add = genericadd.Add(
    T = float64
)

func main() {
    var (
        a, b float64
    )

    println(add(a, b))
}

@dc0d

但是我不确定它们是否相同或如何相同。

从某种意义上说,它们是相同的,它们的工作方式与我所看到的几乎相同。 对于每个类模板实例化编译器,如果它是第一次看到使用类模板及其参数列表的特定组合,它就会生成一个唯一的实现。 这会增加二进制大小,因为您现在拥有同一个类模板的多个实现。 减慢编译速度,因为编译器现在需要生成这些实现并进行各种检查。 在 C++ 的情况下,编译时间的增加可能是巨大的。 您的玩具示例很快,但 C++ 的也很快。

我不确定。 其他实现泛型的方法是否支持这一点?

其他语言对此没有问题。 尤其是我最熟悉的 C#。 但它使用 Go 团队完全排除的运行时代码生成。 Java 也可以,但至少可以说它们的实现并不是最好的。 据我了解,一些 ianlancetaylor 提案可以处理仅二进制包。

我唯一不明白的是是否必须支持仅二进制包。 我认为提案中没有明确提及它们。 我并不真正关心它们,但它仍然是一种语言功能。

只是为了测试我的理解......考虑这个复制/粘贴算法的回购[这里]。 除非你想使用“int”,否则不能直接使用代码。 它必须被复制、粘贴和修改才能工作。 通过修改,我的意思是“int”的每个实例都必须更改为您真正需要的任何类型。

类型别名方法将修改一次,例如 T,然后插入一行“type T int”。 然后编译器需要将 T 重新绑定到其他东西,比如 float64。

所以:
a)我认为除非您实际使用此技术,否则编译器不会减速。 所以这是你的选择。
b)给定新的 vgo 东西,可以使用相同代码的多个版本......这意味着必须有某种方法将使用的源隐藏在视线之外,那么编译器肯定可以跟踪是否有两个使用相同的重新绑定并避免重复。 所以我认为代码膨胀与当前的复制/粘贴技术相同。

在我看来,在类型别名和即将到来的 vgo 之间,这种泛型方法的基础几乎是完整的......

提案[此处]中列出了一些“未知数”。 因此,将其充实一点会很好。

@mandolyte ,您可以通过将专用类型包装在一些通用容器中来添加另一层间接性。 这样你的实现可以保持不变。 然后编译器会做所有的事情。 我认为 Ian 的类型参数提案就是这样工作的。

我认为用户需要在类型擦除和单态化之间做出选择。
后者是 Rust 提供零成本抽象的原因。 去也应该。

2018 年 4 月 9 日星期一上午 8:32 Antonenko Artem [email protected]
写道:

@mandolyte https://github.com/mandolyte你可以添加另一个级别
通过在一些通用容器中包装特殊类型来实现间接寻址。 那
您的实现可以保持不变的方式。 然后编译器将完成所有
魔法。 我认为 Ian 的类型参数提案就是这样工作的。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/15292#issuecomment-379735199或静音
线程
https://github.com/notifications/unsubscribe-auth/AGGWB1v9h5kWmuHCBuoewTTSX751OHgrks5tm1TsgaJpZM4IG-xv
.

在我看来,在关于模块化和性能之间权衡的讨论中存在一个可以理解的混淆。 C++ 技术对每种类型的通用代码进行重新类型检查和实例化,这不利于模块化,不利于二进制分发,并且由于代码膨胀,不利于性能。 这种方法的好处是它会自动将生成的代码专门用于所使用的类型,这在所使用的类型是原始类型(如int )时特别有用。 Java 同构地翻译通用代码,但在性能上付出了代价,特别是当代码使用类型T[]时。

幸运的是,有几种方法可以解决这个问题,而无需 C++ 的非模块化和完整的运行时代码生成:

  1. 为原始类型生成专门的实例化。 这可以自动完成,也可以通过程序员指令完成。 需要一些调度来访问正确的实例化,但可以折叠到同质翻译已经需要的调度中。 这与 C# 的工作方式类似,但不需要完整的运行时代码生成; 在运行时可能需要一些额外的支持,以便在代码加载时设置调度表。
  2. 使用单个泛型实现,其中当 T 被实例化为原始类型时,T 的数组实际上表示为原始类型的数组。 我们在 PolyJ、Genus 和 Familia 中使用的这种方法相对于 Java 方法大大提高了性能,尽管它不如完全专业化的实现快。

@dc0d

当重新绑定类型别名时,多态性不是通过具有兼容类型来实现的。 唯一实际的约束是通用包的主体。 它们必须在机械上兼容。

类型别名是错误的方式,因为它应该是一个常量引用。
最好直接写“T Type”,然后你会看到你确实使用了泛型。

为什么要为整个包/模块使用全局类型变量“T”,<> 或 [] 中的局部类型变量更加模块化。

@creker

尤其是我最熟悉的 C#。 但它使用 Go 团队完全排除的运行时代码生成。

对于引用类型,但不适用于值类型。

@DemiMarie

我认为用户需要在类型擦除和单态化之间做出选择。
后者是 Rust 提供零成本抽象的原因。 去也应该。

“类型擦除”是模棱两可的,我假设您的意思是类型参数擦除,Java 提供的东西也不完全正确。
Java具有单态化,但它不断单态化(半)到泛型约束的上限,该约束主要是Object。
为了提供其他类型的方法和字段,上限在内部强制转换为您的适当类型,这非常难看。
如果Valhalla项目被接受,值类型会发生变化,但遗憾的是引用类型不会发生变化。

Go 不必采用 Java 方式,因为:

“版本之间不保证编译包的二进制兼容性”

而这在 Java 中是不可能的。

在我看来,在关于模块化和性能之间权衡的讨论中存在一个可以理解的混淆。 C++ 技术对每种类型的通用代码进行重新类型检查和实例化,这不利于模块化,不利于二进制分发,并且由于代码膨胀,不利于性能。

你在这里说的是哪种表演?

如果“代码膨胀”和“性能”是指“二进制大小”和“指令缓存压力”,那么问题很容易解决:只要不过度保留每个专业化的调试信息,就可以在链接时将具有相同主体的函数折叠成相同的函数(所谓的“Borland 模型” )。 这可以简单地处理原始类型和类型的特化,而无需调用非平凡方法。

如果“代码膨胀”和“性能”是指“链接器输入大小”和“链接时间”,那么如果您可以对构建系统做出某些(合理的)假设,那么问题也相当简单。 您可以代替在每个编译单元中发出每个特化,而是发出所需的特化列表,并让构建系统在链接之前将每个唯一特化恰好实例化一次(“Cfront 模型”)。 IIRC,这是C++ 模块试图解决的问题之一。

因此,除非您指的是我错过的第三种“代码膨胀”和“性能”,否则您似乎在谈论实现的问题,而不是规范:_只要实现不会过度保留调试信息,_性能问题很容易解决。


Go 的更大问题是,如果我们不小心,就有可能在运行时使用类型断言或反射来生成参数化类型的新实例,这没有多少实现的聪明之处——缺少一个昂贵的整体‐程序分析——可以修复。

这确实是模块化的失败,但它与代码膨胀无关:相反,它来自这样一个事实,即 Go 函数(和方法)的类型没有在其参数上捕获足够完整的约束集。

@sighoya

对于引用类型,但不适用于值类型。

根据我的阅读,C# JIT 在运行时对每种值类型进行专门化,对所有引用类型进行一次专门化。 没有编译时(IL-time)专业化。 这就是 C# 方法被完全忽略的原因——Go 团队不想依赖运行时代码生成,因为它限制了 Go 可以运行的平台。 特别是,在 iOS 上,您不允许在运行时进行代码生成。 它有效,我实际上做了一些,但 Apple 不允许在 AppStore 中使用它。

你是怎么做到的?

2018 年 4 月 9 日星期一下午 3:41 Antonenko Artem [email protected]
写道:

@sighoya https://github.com/sighoya

对于引用类型,但不适用于值类型。

根据我的阅读,C# JIT 在运行时对每个值进行专门化
类型和一次用于所有引用类型。 没有编译时间
专业化。 这就是为什么 C# 方法被完全忽略的原因 - Go 团队
不想依赖运行时代码生成,因为它限制了平台 Go
可以继续运行。 特别是,在 iOS 上,您不允许进行代码生成
在运行时。 它有效,我实际上做了一些,但苹果不允许
它在 AppStore 中。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/15292#issuecomment-379870005或静音
线程
https://github.com/notifications/unsubscribe-auth/AGGWB-tslGeUSGXl2ZlEDLf0dCATUaYvks5tm7lvgaJpZM4IG-xv
.

@DemiMarie启动我的旧研究代码只是为了确定(该研究因其他原因而被放弃)。 再一次,调试器误导了我。 我分配一个页面,给它写一些指令,用 PROT_EXEC 保护它并跳转到它。 在调试器下它可以工作。 如预期的那样,没有调试器的应用程序在崩溃日志中使用 CODESIGN 消息被 SIGKILLed。 因此,即使没有 AppStore,它也无法正常工作。 如果 iOS 对 Go 很重要,那么反对运行时代码生成的更有力的论据。

首先,再思考一遍Rob Pike 的 5 条编程规则会很有帮助。

第二(恕我直言):

关于慢速编译和二进制大小,在使用 Go 开发的常见应用程序类型中使用了多少泛型类型(规则 3 中的_n 通常很小_)? 除非问题需要在具体概念(大量类型)中具有高水平的基数,否则可以忽略开销。 即便如此,我仍会争辩说这种方法有问题。 在实施电子商务系统时,没有人为每种产品定义单独的类型及其变体,也许还有可能​​的定制。

冗长是简单和熟悉的一种很好的形式(例如在语法中),它使事情变得更加明显和清晰。 虽然我怀疑使用类型别名重新绑定会导致代码膨胀程度更高,但我确实喜欢熟悉的 Go-ish 语法和伴随它的明显冗长。 Go 的目标之一是易于阅读(而我个人也发现它相对容易和愉快地编写)。

我不明白它如何损害性能,因为在运行时,只使用在编译时生成的具体有界类型。 没有运行时开销。

我看到的类型别名重新绑定的唯一问题可能是二进制分布。

@dc0d性能损害通常意味着由于类模板的不同实现而填充指令缓存。 它与实际性能的关系如何是一个悬而未决的问题,我不知道任何基准,但理论上这是一个问题。

至于二进制大小。 这是人们通常会提出的另一个理论问题(正如我之前所做的那样),但真正的代码将如何受到影响,这又是一个悬而未决的问题。 例如,我认为所有指针和接口类型的特化可能是相同的。 但是所有值类型的专业化都是独一无二的。 这也包括结构。 使用通用容器来存储它们很常见,并且会导致严重的代码膨胀,因为通用容器的实现并不小。

我看到的类型别名重新绑定的唯一问题可能是二进制分布。

在这里我仍然不确定。 泛型提案是否必须支持仅二进制包,或者我们可以只提一下仅二进制包不支持泛型。 这会容易得多,这是肯定的。

前面说过,如果不需要支持调试,则
可以组合相同的模板实例。

2018 年 4 月 10 日星期二上午 5:46 Kaveh Shahbazian [email protected]
写道:

首先,思考一下 Rob Pike 的 5 条编程规则会很有帮助
https://users.ece.utexas.edu/%7Eadnan/pike.html 再来一次。

第二(恕我直言):

关于慢速编译和二进制大小,使用了多少泛型类型
使用 Go 开发的常见应用程序类型( n 是通常从规则 3 开始很小)? 除非问题需要高水平
开销可以在具体概念(大量类型)中的基数
被忽视。 即使那样,我也会争辩说这有问题
方法。 在实施电子商务系统时,没有人定义单独的
每种产品的类型及其变化,也许还有可能
定制。

冗长是简单和熟悉的一种很好的形式(例如
语法)这使事情变得更加明显和清晰。 虽然我对此表示怀疑
使用类型别名重新绑定代码膨胀会更高,我喜欢
熟悉的 Go-ish 语法和伴随它的明显冗长。 之一
Go 的目标是易于阅读(虽然我个人认为
写起来也相对容易和愉快)。

我不明白它如何损害性能,因为在运行时,只有
正在使用的具体有界类型已在
编译时。 没有运行时开销。

我看到的类型别名重新绑定的唯一问题可能是二进制文件
分配。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/15292#issuecomment-380040032或静音
线程
https://github.com/notifications/unsubscribe-auth/AGGWB6aDfoHz2wbsmu8mCGEt652G_VE9ks5tnH9xgaJpZM4IG-xv
.

在“使用相同的参数”,甚至“使用具有相同底层类型的参数”的意义上,实例化甚至不需要是“相同的”。 它们只需要足够接近以产生相同的生成代码。 (对于 Go,这也意味着“相同的指针掩码”。)

@creker

根据我的阅读,C# JIT 在运行时对每种值类型进行专门化,对所有引用类型进行一次专门化。 没有编译时(IL-time)专业化。

嗯,这有时有点复杂,因为它们的字节码在代码执行之前被及时解释,所以代码生成是在执行程序之前但在编译之后完成的,所以你在 vm 的意义上是对的,它在代码中运行生成。

我认为如果我们在编译时生成代码,那么 c# 的通用系统就可以了。
使用 go 无法生成 c# 意义上的运行时代码,因为 go 不是 vm。

@dc0d

我看到的类型别名重新绑定的唯一问题可能是二进制分布。

你能详细说明一下吗。

@sighoya我的错; 我的意思不是二进制分发,而是二进制包——我个人不知道它有多重要。

@creker总结得很好! (MO) 除非找到强有力的理由,否则必须避免任何形式的 Go 语言结构重载。 使用类型别名重新绑定的原因之一是避免重载内置复合类型,如切片或映射。

冗长是简单和熟悉的一种很好的形式(例如在语法中),它使事情变得更加明显和清晰。 虽然我怀疑使用类型别名重新绑定会导致代码膨胀程度更高,但我确实喜欢熟悉的 Go-ish 语法和伴随它的明显冗长。 Go 的目标之一是易于阅读(而我个人也发现它相对容易和愉快地编写)。

我不同意这个观点。 你的提议将迫使用户做任何程序员都知道的最难的事情——命名。 所以我们最终会得到充满匈牙利符号的代码,这不仅看起来很糟糕,而且不必要地冗长并导致口吃。 此外,其他提案也引入了 go-ish 语法,同时不存在这些问题。

我们每天必须设计三类名称:

  • 对于域实体/逻辑
  • 程序工作流数据类型/逻辑
  • 服务/接口数据类型/逻辑

有多少次程序员成功地避免在她/他的代码中命名任何东西?

难与否,它需要每天完成。 它的大部分障碍来自于构建代码库的无能——而不是命名过程本身的困难。 到目前为止,这句话 - 至少以目前的形式 - 对编程世界造成了极大的伤害。 它只是试图强调命名的重要性。 因为我们通过代码中的名称进行通信。

当名称伴随代码结构化实践时,它们会变得更加强大; 在代码布局(文件、目录结构、包/模块)和实践(设计模式、服务抽象 - 例如 REST、资源管理 - 并发编程、访问硬盘驱动器、吞吐量/延迟)方面。

至于语法和冗长,我更喜欢冗长而不是巧妙的简洁(至少在 Go 的上下文中)——再一次,Go 意味着易于阅读,不一定易于编写(奇怪的是,我发现它也擅长于此) .

我阅读了很多关于为什么以及如何在 Go 中实现泛型的经验报告和建议。

如果我尝试在我的 Go 解释器gomacro中实际实现它们,你介意吗?

我在这个话题上有一些经验,过去曾在两种语言中添加过泛型

  1. 一种我在幼稚时创建的现已废弃的语言:) 它转换为 C 源代码
  2. Common Lisp 与我的库cl-parametric-types - 它还支持泛型类型和函数的部分和完全特化

@cosmos72如果能看到保留类型安全的技术原型,那将是一份很好的体验报告。

刚刚开始研究它。 您可以在https://github.com/cosmos72/gomacro/tree/generics-v1上关注进度

目前,我从https://github.com/golang/proposal/blob/master/design/15292-generics.md#Proposal中列出的第三个和第四个 Ian 提案的(稍作修改)混合开始

@cosmos72在下面的链接中有提案摘要。 你的混合物是其中之一吗?
https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4

我已阅读该文档,它总结了各种编程语言对泛型的许多不同方法。

目前,我正在研究 C++、Rust 和其他人使用的“类型专业化”技术,可能还有一点“参数化模板范围”,因为 Go 中新类型的最通用语法是type ( Foo ...; Bar ...) ,我正在扩展它到template[T1,T2...] type ( Foo ...; Bar ...)
另外,我为“受限制的专业化”敞开大门。

我还想实现“多态函数专业化”,即如果程序员未指定,则安排在调用站点的语言自动推断专业化,但我想实现起来可能有些复杂。 我们会看到。

我所指的混合在https://github.com/golang/proposal/blob/master/design/15292/2013-10-gen.mdhttps://github.com/golang/proposal/blob/之间

更新:为了避免在最初的公告之外向这个官方 Go 问题发送垃圾邮件,最好继续在gomacro 问题 #24:添加泛型

更新 2:第一个模板函数编译并成功执行。 见https://github.com/cosmos72/gomacro/tree/generics-v1

仅作记录,可以改写我的观点(关于泛型和类型别名重新绑定):

应该将泛型添加为编译器功能(代码生成、模板等),而不是语言功能(在所有级别上干预 Go 的类型系统)。

@dc0d
但是 C++ 模板不是编译器和语言特性吗?

@sighoya上次我专业地编写 C++ 是在 2001 年左右。所以我可能错了。 但是假设命名的含义是准确的——“模板”部分——是(或者更确切地说不是); 它可能是编译器功能(而不是语言功能),伴随着一些语言结构,这些结构很可能不会重载类型系统中涉及的任何语言结构。

我支持@dc0d。 如果你考虑一下,这个功能只不过是一个集成的代码生成器。

是的:二进制大小可能会增加,但现在我们使用代码生成器,它们几乎相同,但作为外部功能。 如果我必须将我的模板创建为:

type BinaryTreeOfStrings struct {
    left, right *BinaryTreeOfStrings;
    string content;
}

// Its methods here

type BinaryTreeOfBigInts struct {
    left, right *BinaryTreeOfBigInts;
    uint64 content;
}

// AGAIN the same methods but different type

...我真的很喜欢这样,而不是复制粘贴或使用外部工具,此功能成为编译器本身的一部分。

请注意:

  • 是的,结束代码会重复。 就像我们使用发电机一样。 二进制文件会更大。
  • 是的,这个想法不是原创的,而是从 C++ 借来的。
  • 是的,MyType 的功能不涉及任何类型T (直接或间接)的东西也会被重复。 这可以被优化(例如,引用类型T的方法 - 除了指向消息接收对象的指针 - 将为每个T生成;持有对方法的调用的方法将为每个T生成,也将为每个T递归生成 - 而在接收器中它们对T的唯一引用是*T的方法,以及仅调用那些安全方法并满足相同条件的其他方法只能执行一次)。 无论如何,IMO 这一点很重要而且不太重要:即使这种优化不存在,我也会很高兴。
  • 在我看来,类型参数应该是明确的。 特别是当一个对象满足潜在的无限接口时。 再次:代码生成器。

到目前为止,在我的评论中,我的建议是按原样实现它:作为编译器支持的代码生成器,而不是外部工具。

Go 走 C++ 路线是不幸的。 许多人认为 C++ 方法是一团糟,它使程序员反对泛型的整个想法:调试困难、缺乏模块化、代码臃肿。 所有的“代码生成器”解决方案实际上都只是宏替换——如果这是你想要编写代码的方式,为什么我们甚至需要编译器支持?

@andrewcmyers我有这个提案类型别名重新绑定,其中我们只编写普通包,而不是明确使用interface{}我们只是将它用作type T = interface{}作为包级通用参数。 就这样。

  • 我们像一个普通的包一样调试它——它是实际的代码,而不是一些中间半衰期的生物。
  • 没有必要在所有级别干预 Go 类型系统 - 单独考虑可分配性。
  • 这是明确的。 没有隐藏的魔力。 当然,人们可能会发现无法无缝链接通用调用是一个缺点。 我认为它是吸引人的! 在两个连续呼叫中更改类型,在一个语句中不是 Goish (IMO)。
  • 最重要的是它向后兼容 Go 1.x (x >= 8) 系列。

虽然这个想法并不新鲜,但 Go 允许实现它的方式是务实和清晰的。

进一步的好处:Go 中没有运算符重载。 但是通过将类型别名的默认值定义为(例如) type T = int ,不是唯一可用于自定义此通用包的有效类型是具有+内部实现的数字类型

也可以通过添加一些验证器类型和语句来强制别名类型参数实现多个接口。

现在,对于具有实现ErrorStringer接口的参数并且也是支持+运算符的数字类型的泛型类型使用任何显式表示法,这将是非常难看的!

现在我们使用代码生成器,它们几乎相同,但作为一个外部功能。

不同之处在于,广泛接受的代码生成方式(通过go generate )发生在提交/开发时,而不是编译时。 在编译时这样做意味着您需要允许在编译器中执行任意代码,库可能会按数量级增加编译时间和/或您将拥有单独的构建依赖项(即代码不能再使用 Go 构建工具)。 我喜欢 Go 将元编程的调用推送给上游开发人员。

也就是说,与解决这些问题的所有方法一样,这种方法也有缺点并涉及权衡。 就个人而言,我认为在类型系统中支持的实际泛型不仅更好(即具有更强大的功能集),而且还可以保留可预测和安全编译的优势。

我保证会阅读上面的所有内容,但我也会添加一些内容 - GoLang SDK for Apache Beam似乎是一个很好的例子/展示了库设计者必须忍受的困难才能完成任何_正确_高级别的事情。

Go 泛型至少有两个实验性实现。 本周早些时候,我花了一些时间在 (1) 上。 我很高兴地发现代码的可读性影响很小。 而且我发现使用匿名函数来提供相等测试效果很好; 所以我确信不需要运算符重载。 我确实发现的一个问题是错误处理。 如果类型是整数或字符串,则“return nil,err”的常见习语将不起作用。 有很多方法可以解决这个问题,但都需要付出复杂性的代价。 我可能有点奇怪,但我喜欢 Go 的错误处理。 所以这让我观察到 Go 泛型解决方案应该有一个用于类型零值的通用关键字。 编译器将简单地将其替换为数字类型的零,字符串类型的空字符串和结构的 nil。

虽然这个实现没有强制执行包级别的方法,但这样做当然很自然。 而且,当然,这个实现并没有解决关于编译器实例化代码应该去哪里(如果在任何地方)、代码调试器如何工作等的所有技术细节。

将相同的算法代码用于整数和点之类的东西非常好:

type Point struct {
    x,y int
}

有关我的测试和观察,请参见 (2)。

(1) https://github.com/albrow/fo; 另一个是前面提到的https://github.com/cosmos72/gomacro#generics
(2) https://github.com/mandolyte/fo-experiments

@mandolyte您可以使用*new(T)来获取任何类型的零值。

像 default(T) 或 zero(T) 这样的语言结构(第一个是第一个
在 C# IIRC 中)会很清楚,但 OTOH 比 *new(T) 长(虽然更多
高性能)。

2018-07-06 9:15 GMT-05:00 Tom Thorogood [email protected]

@mandolyte https://github.com/mandolyte你可以使用 *new(T) 来获取
任何类型的零值。


您收到此消息是因为您发表了评论。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/15292#issuecomment-403046735或静音
线程
https://github.com/notifications/unsubscribe-auth/AlhWhQ5cQwnc3x_XUldyJXCHYzmr6aN3ks5uD3ETgaJpZM4IG-xv
.

--
这是在 TripleMint 中使用的邮件签名测试

19642 用于讨论通用零值

@tmthrgd不知何故,我错过了那个小花絮。 谢谢!

序幕

泛型都是关于专门定制的构造。 专业分为三类:

  • 专业化类型, Type<T> - 一个_array_;
  • 专门计算, F<T>(T)F<T>(Type<T>) - 一个_可排序数组_;
  • 特殊符号,例如_LINQ_ - Go 中的selectfor语句;

当然,有些编程语言提供了更通用的结构。 但是像 _C++_、_C#_ 或 _Java_ 这样的传统编程语言提供或多或少限于此列表的语言结构。

想法

第一类泛型类型/构造应该是类型不可知的。

第二类泛型类型/构造需要根据类型参数的 _property_ _act_。 例如,_可排序数组_必须能够_比较_其项目的_可比较属性_。 假设T.(P)T的属性,并且A(T.(P))是作用于该属性的计算/操作,则(A, .(P))可以应用于每个单独的项目或被声明为专门的计算,传递给原始的可定制计算。 Go 中后一种情况的一个例子是sort.Interface接口,它也有对应的单独函数sort.Reverse

第三类泛型类型/结构是_type-specialized_语言符号——似乎_in general_不是Go的东西。

问题

待续 ...

欢迎任何比表情符号更具描述性的反馈!

@dc0d我建议在尝试定义泛型之前学习 Sepanov 的“编程元素”。 TL;DR 是我们首先编写具体的代码,比如对数组进行排序的算法。 稍后我们添加其他集合类型,例如 Btree 等。我们注意到我们正在编写本质上相同的排序算法的许多副本,因此我们定义了一些概念,例如“可排序”。 现在我们想对排序算法进行分类,可能是通过它们需要的访问模式,比如只转发、单次传递(一个流)、只转发多次传递(一个单链表)、双向(一个双链表)、随机访问(一个大批)。 当我们添加一个新的集合类型时,我们只需要指出它属于哪个“坐标”类别,就可以访问所有相关的排序算法。 这些算法类别很像“Go”接口。 我希望在 Go 中扩展接口以支持多种类型参数和抽象/关联类型。 我不认为函数需要临时类型参数化。

@dc0d作为将泛型分解为组件部分的尝试,我之前没有考虑过 3,“专门表示法”,作为它自己的单独部分。 也许它可以被描述为通过利用类型约束来定义 DSL。

我可能会争辩说,您的 1 和 2 分别是“数据结构”和“算法”。 有了这个术语,为什么很难将它们完全分开就更清楚了,因为它们通常高度依赖于彼此。 但是 sort.Interface 是一个很好的例子,说明你可以存储行为之间划清界限(最近加了一点糖让它变得更好),因为它将 Indexable 和 Comparable 要求编码为实现排序算法所需的最小行为使用“交换”和“更少”(和 len)。 但这似乎无法解决更复杂的数据结构,如树或堆,目前这两种数据结构都需要一些扭曲才能映射为 Go 接口的纯行为。

我可以想象一个相对较小的泛型添加到接口(或其他),它可以允许大多数教科书数据结构和算法在没有扭曲的情况下相对干净地实现(比如今天的 sort.Interface),但不足以设计 DSL。 当我们完全要添加泛型的所有麻烦时,我们是否将自己限制在这样一个受限制的泛型实现是一个不同的问题。

二叉树的@infogulch坐标结构是“分叉坐标”,其他树也存在等价物。 但是,您也可以通过三个顺序之一来预测树的顺序,前序、中序和后序。 在决定了其中一个之后,树可以作为双向坐标来处理,并且在双向坐标上定义的排序算法系列将是最有效的。

关键是您按访问模式对排序算法进行分类。 每个访问模式只有有限数量的最佳排序算法。 此时您并不关心数据结构。 谈论更复杂的结构没有抓住重点,我们想对排序算法家族而不是数据结构进行分类。 无论您拥有什么数据,您都必须使用现有的一种算法对其进行排序,因此问题变成了排序算法的可用数据访问模式分类中的哪一种最适合您拥有的数据结构。

(恕我直言)

@infogulch

也许它可以被描述为通过利用类型约束来定义 DSL

你说的对。 但由于它们是语言结构集的一部分,IMO 称它们为 DSL 有点不准确。

1 和 2 ... 通常高度依赖

又是真的。 但是在很多情况下,需要传递一个容器类型,而实际用途尚未确定——那时在程序中。 这就是为什么需要单独研究 1 的原因。

sort.Interface是一个很好的例子,你可以在 _storage_ 和 _behavior_ 之间画一条线

说得好;

这似乎打破了更复杂的数据结构

这是我的问题之一:概括类型参数并根据限制进行描述(如List<T> where T:new, IDisposable )或提供适用于所有项目(一组;某种类型)的通用协议?

@基恩

问题变成排序算法的哪些可用数据访问模式分类最适合您拥有的数据结构

真的。 按索引访问是切片(或数组)的_property_。 因此,对可排序容器(或 _tree_ -able 容器,无论 _tree_ 算法是什么)的第一个要求是提供 _access & mutate (swap)_ 实用程序。 第二个要求是项目必须具有可比性。 这就是(对我而言)您所谓的算法令人困惑的部分:必须在双方(在容器和类型参数上)都满足要求。 这就是我无法想象 Go 中泛型的实用实现的一点。 问题的每一方面都可以完美地用接口来描述。 但是如何将这两者组合成一个有效的符号呢?

@dc0d算法需要接口,数据结构提供它们。 如果接口足够强大,这对于完全通用来说已经足够了。 接口由类型参数化,但您需要类型变量。

以“排序”为例,“Ord”是存储在容器中的类型的属性,而不是容器本身。 访问模式是容器的属性。 简单的访问模式是“迭代器”,但该名称来自 C++,Stepanov 更喜欢“坐标”,因为它可以应用于更复杂的多维容器。

试图定义排序,我们想要这样的东西:

bubble_sort : forall T U I => T U -> T U requires
   ForwardIterator<T>, Readable<T>, Writable<T>,
   Ord<U>,  ValueType(T) == U, Distance type(T) == I

注意:我不是在建议这种表示法,只是试图引入一些其他相关的工作,requires 子句采用 Stepanov 首选的语法,函数类型来自 Haskell,其类型类可能代表了这些概念的良好实现。

@基恩
也许我误解了你,但我认为你不能简单地将算法限制在接口上,至少现在定义接口的方式是这样。
以 sort.Slice 为例,我们对切片排序感兴趣,但我不知道如何构造一个表示所有切片的接口。

@urandom你抽象算法而不是集合。 所以你问,“排序”算法中存在哪些数据访问模式,然后对它们进行分类。 因此,容器是否是“切片”并不重要,我们并不是试图定义您可能想要在切片上执行的所有操作,而是试图确定算法的要求并使用它来定义接口。 切片并不特殊,它只是一种类型 T,我们可以在其上定义一组操作。

因此接口与算法库相关,您可以为自己的数据结构定义自己的接口,以便能够使用这些算法。 这些库可以带有内置类型的预定义接口。

@基恩
我以为这就是你的意思。 但在 Go 的上下文中,这可能意味着需要对接口可以定义的内容进行重大改革。 我想各种内置操作,例如迭代或运算符,需要通过方法公开,以便使sort.Slicemath.Max之类的东西在接口上通用。

因此,您必须支持以下接口(伪代码):

type [T] OrderedIterator interface {
   Len() int
   ValueAt(i int) *T
}

...
package sort

func [T] Slice(s [T]OrderedIterator, func(i, j int) bool) {
   ...
}

那么所有切片都会有这些方法吗?

@urandom迭代器不是集合的抽象,而是引用/指针到集合的抽象。 例如,前向迭代器可以有一个方法'successor'(有时是'next')。 能够在迭代器的位置访问数据不是迭代器的属性(否则您最终会得到迭代器的读/写/可变风格)。 最好将“引用”分别定义为 Readable、Writable 和 Mutable 接口:

type T ForwardIterator interface {
   type DistanceType D
   successor(x T) T
}

type T Readable interface {
   type ValueType U 
   source(x T) U
}

注意:类型'T'不是切片,而是切片上迭代器的类型。 如果我们采用将开始和结束迭代器传递给 sort 等函数的 C++ 风格,这可能只是一个简单的指针。

对于随机访问迭代器,我们最终会得到类似的结果:

type T RandomIterator interface {
   type DistanceType D
   setPosition(x DistanceType)
}

所以迭代器/坐标是对集合引用的抽象,而不是集合本身。 如果您将迭代器视为坐标,而将集合视为地图,那么“坐标”这个名称就很好地表达了这一点。

我们不是通过不利用函数闭包和匿名函数来卖空 Go 吗? 将函数/方法作为 Go 中的第一类类型会有所帮助。 例如,使用albrow/fo的语法,冒泡排序可能如下所示:

type SortableContainer[C,T] struct {
    Less func(C,T,T) bool
    Swap func(C,int,int)
    Next func(C) (T,bool)
}

func (bs *SortableContainer[C,T]) BubbleSort(container C, e1,e2 T) {    
    swapCount := 1
    var item1, item2 T
    item1, ok1 = bs.Next()
    if !ok1 {return}
    item2, ok2 = bs.Next()
    if !ok2 {return}
    for swapCount > 0 {
        swapCount = 0
        for {
            if Less(item2, item1) { 
                bs.Swap(C,item2,item1)
                swapCount += 1
            }
        }
    }
}

请忽略任何错误...完全未经测试!

@mandolyte我不确定这是否是写给我的? 我真的看不出我的建议和你的例子有什么区别,除了你使用的是多参数接口,而且我给出的例子是使用抽象/关联类型。 为了清楚起见,我认为您需要多参数接口和抽象/关联类型才能完全通用,Go 目前都不支持这两者。

我建议您的接口不如我建议的那些通用,因为它们将排序顺序、访问模式和可访问性绑定到同一个接口中,这当然会导致接口的扩散,例如两个顺序(更少, 更大)、三种访问类型(只读、只写、可变)和五种访问模式(前向单通道、前向多通道、双向、索引、随机)将导致 36 个接口,而只有 11 个如果关注点是分开的。

您可以使用多参数接口而不是像这样的抽象类型来定义我建议的接口:

type I ForwardIterator interface {
   successor(x I) I
}
type R V Readable interface {
   source(x R) V
}
type V Ord interface {
   less(x V, y V) : bool
}

请注意,唯一需要两个类型参数的是 Readable 接口。 然而,我们失去了迭代器对象“包含”被迭代对象的类型的能力,这是一个大问题,因为现在我们必须在类型系统中移动“值”类型,我们必须让它正确. 这导致不好的类型参数泛滥,并且增加了编码错误的可能性。 我们也失去了在迭代器上定义“DistanceType”的能力,这是对集合中元素进行计数所需的最小数字类型,这对于映射到 int8、int16、int32 等很有用,以提供您需要的类型计数没有溢出的元素。

这与“功能依赖”的概念密切相关。 如果一个类型在功能上依赖于另一个类型,它应该是一个抽象/关联类型。 只有当这两种类型是独立的时,它们才应该是单独的类型参数。

一些问题:

  1. 不能对多参数接口使用当前的 f(x I) 语法。 我不喜欢这种语法将接口(类型的约束)与类型混淆。
  2. 需要一种方法来声明参数化类型。
  3. 需要有一种方法来为具有给定类型参数集的接口声明关联类型。

@keean不确定我是否了解接口数量如何或为何如此之高。 这是一个完整的工作示例: https ://play.folang.org/p/BZa6BdsfBgZ(基于切片,不是通用容器,因此不需要 Next() 方法)。

它只使用一种类型的结构,根本没有接口。 我必须提供所有匿名函数和闭包(这可能是权衡的地方?)。 该示例使用相同的冒泡排序算法对整数切片和“(x,y)”点切片进行排序,其中距原点的距离是 Less() 函数的基础。

无论如何,我希望展示在类型系统中使用函数是如何提供帮助的。

@mandolyte我想我误解了你的提议。 我明白你在说的是“folang”,它已经在 Go 中添加了一些不错的函数式编程特性。 你所实现的基本上是手工管道一个多参数类型类。 您正在将所谓的函数字典传递给 sort 函数。 这是显式地做一个接口会隐式地做的事情。 在多参数接口和关联类型之前可能需要这些特性,但最终在传递所有这些字典时会遇到问题。 我认为接口提供了更清晰、更易读的代码。

对切​​片进行排序是一个已解决的问题。 这是使用go-li(golang 改进)语言实现的slice quicksort.go 的代码。

func main(){
    var data = []int{5,3,1,8,9}

    Sort(data, func(a *int, b *int) int {
        return *a - *b
    })

    fmt.Println(data)
}

您可以在操场上进行实验

您可以粘贴到 playground 的完整示例,因为在 playground 上导入快速排序包不起作用。

@go-li 我相信你可以对切片进行排序,如果你不能,那就有点糟糕了。 关键是通常您希望能够使用相同的代码对任何线性容器进行排序,这样您只需编写一次排序算法,无论您正在排序的容器(数据结构)是什么,也不管是什么内容是。

当你能做到这一点时,标准库可以提供通用的排序功能,没有人需要再编写一个。 这样做有两个好处,错误更少,因为编写正确的排序算法比您想象的要难,Stepanov 使用了大多数程序员无法正确定义“min”和“max”对的示例,所以我们有什么希望更复杂的算法更正。 另一个好处是,当每个排序算法只有一个定义时,任何可以提高清晰度或性能的改进都会使所有使用它的程序受益。 人们可以花时间尝试改进通用算法,而不必为每种不同的数据类型编写自己的算法。

@基恩
另一个问题与我们之前的讨论有关。 我不知道如何定义一个映射函数来更改可迭代项中的项,返回一个新的具体可迭代类型,其项的类型可能与原始类型不同。

我想这样一个函数的用户会希望返回一个具体的类型,而不是另一个接口。

@urandom假设我们并不是要“就地”执行它,这将是不安全的,那么您想要的是一个具有一种类型的“读取迭代器”和另一种类型的“写入迭代器”的映射函数,可以定义如下:

map<I, O, U>(first I, last I, out O, fn U) requires
   ForwardIterator<I>, Readable<I>,
   ForwardIterator<O>, Writable<O>,
   UnaryFunction<U>, Domain(U) == ValueType(I), Codomain(U) == ValueType(O)

为清楚起见,“ValueType”是接口“Readable”和“Writable”的关联类型,“Domain”和“Codomain”是“UnaryFunction”接口的关联类型。 如果编译器可以自动为“UnaryFunction”之类的数据类型派生接口,显然会有很大帮助。 虽然这种看起来像反射,但它不是,而且这一切都发生在编译时使用静态类型。

@keean如何在当前 Go 接口的上下文中对那些 Readable 和 Writable 约束进行建模?

我的意思是,当我们有一个类型A并且我们想要转换为类型B时,该 UnaryFunction 的签名将是func (input A) B (对吗?),但那怎么能仅使用接口进行建模,以及如何对通用map (或filterreduce等)进行建模以保持类型管道?

@geovanisouza92我认为“类型族”会很好用,因为它们可以作为类型系统中的正交机制实现,然后像在 Haskell 中那样集成到接口的语法中。

类型族就像类型上的受限函数(映射)。 由于接口实现是按类型选择的,我们可以为每个实现提供类型映射。

所以如果我们定义:

ValueType MyIntArrayIterator -> Int

函数有点小技巧,但函数有一个类型,例如:

fn(x : Int) Float

我们会写这种类型:

Int -> Float

重要的是要意识到->只是一个中缀类型构造函数,就像 Array 的 '[]' 是一个类型构造函数,我们可以很容易地写这个;

Fn Int Float
Or
Fn<Int, Float>

取决于我们对类型语法的偏好。 现在我们可以清楚地看到如何定义:

Domain  Fn<Int, Float> -> Int
Codomain Fn<Int, Float> -> Float

现在虽然我们可以手动提供所有这些定义,但它们可以很容易地由编译器派生出来。

鉴于这些类型族,我们可以看到我在上面给出的 map 的定义只需要类型 IO 和 U 来实例化泛型,因为所有其他类型在功能上都依赖于这些。 我们可以看到这些类型是由参数直接提供的。

谢谢,@keean。

这对于内置/预定义函数可以正常工作。 您是说相同的概念将应用于用户定义的函数或用户区库吗?

在某些错误上下文的情况下,那些“类型族”将在运行时进行?

空接口、类型切换和反射怎么样?


编辑:我只是好奇,而不是抱怨。

@giovanisouza92好吧,没有人承诺 Go 拥有泛型,所以我预计会持怀疑态度。 我的方法是,如果你要做泛型,你应该做对。

在我的示例中,“地图”是用户定义的。 它没有什么特别之处,在函数中,您只需使用您在这些类型上所需的接口的方法,就像您现在在 Go 中所做的那样。 唯一的区别是我们可以要求一个类型满足多个接口,接口可以有多个类型参数(虽然 map 示例没有使用这个)并且还有关联的类型(以及对类型的约束,例如类型相等 '=='但这就像 Prolog 相等并统一了类型)。 这就是为什么有不同的语法来指定函数所需的接口。 请注意,还有另一个重要区别:

f(x I, y I) requires ForwardIterator<I>

VS

f(x ForwardIterator, y ForwardIterator)

请注意,后者的 'x' 和 'y' 可以是满足 ForwardIterator 接口的不同类型有所不同,而在前一种语法中,'x' 和 'y' 必须都是相同的类型(满足前向迭代器)。 这一点很重要,这样函数就不会受到不足的约束,并允许在编译期间进一步传播具体类型。

我认为类型切换和反射没有任何变化,因为我们只是扩展了接口的概念。 由于 go 具有运行时类型信息,因此您不会遇到与 Haskell 相同的问题并且需要存在类型。

考虑到 Go、运行时多态性和类型族,我们可能希望将类型族本身约束到一个接口,以避免在运行时将每个关联的类型都视为一个空接口,这会很慢。

因此,鉴于这些想法,我将修改我的上述提议,以便在声明接口时,您将为每个关联类型声明一个接口/类型,该接口的所有实现都必须提供满足该接口的关联类型。 通过这种方式,我们可以知道在运行时从该接口对关联类型调用任何方法是安全的,而无需从空接口进行类型切换。

@基恩
为了推进辩论,让我澄清一下误解,我觉得类似于不是在这里发明的综合症正在发生。

双向迭代器(在 T 语法中func (*T) *[2]*T )在 go-li 语法中具有func (*) *[2]*类型。 换句话说,它需要一个指向某种类型的指针,并返回指向两个指向同一类型的下一个和上一个元素的指针。 它是双向链表使用的基本的具体基础类型。

现在你可以编写你所谓的map,我称之为foreach 泛型函数。 毫无疑问,这不仅适用于链表,而且适用于任何公开双向迭代器的东西!

func Foreach(link func(*) *[2]*, list **, direction byte, f func(*)) {

    if nil == *list {
        return
    }

    var end *
    end = *list

    var e *
    e = (*link(*list))[direction]
    f(end)

    for (e != end) && ((*link(e))[direction] != nil) {
        var newe = (*link(e))[direction]
        f(e)
        e = newe
    }
    return
}

Foreach 可以以两种方式使用,您可以将它与 lambda 一起用于列表或集合元素的类似 for 循环的迭代中。

const forward = 1
const backwards = 0
Foreach(iterator, collection, forward, func(element *element_type){
    // do something with every element
})

或者,您可以使用它将功能映射到每个集合元素。

Foreach(iterator, collection, backwards, function_to_be_mapped_on_elements)

双向迭代器当然也可以使用 go 1 中的接口进行建模。
interface Iterator { Iter() [2]Iterator }您需要使用接口对其进行建模,以便包装(“盒子”)底层类型。 迭代器用户一旦找到并想要访问特定的集合元素,则 type 断言已知类型。 这可能是编译时不安全的。

您接下来要描述的是遗留方法和基于泛型的方法之间的区别。

func modern(x func  (*) *[2]*, y func  (*) *[2]*){}

这种方法编译时类型检查两个集合是否具有相同的底层类型,换句话说,迭代器是否实际上返回相同的具体类型

func modern_T_syntax<T>(x func  (*T) *[2]*T, y func  (*T) *[2]*T){}

与上面相同,但使用熟悉的 T 表示类型占位符语法

func legacy(x Iterator, y Iterator){}

在这种情况下,用户可以将整数链表作为 x 传递,将浮点链表作为 y 传递。 这可能会导致潜在的运行时错误、恐慌或其他内部退相干,但这一切都取决于遗留系统对两个迭代器的作用。

现在是误解。 您声称进行迭代器并进行通用排序以对这些迭代器进行排序将是可行的方法。 那将是一件非常糟糕的事情,这就是为什么

迭代器和链表是同一枚硬币的两个方面。 证明:任何暴露迭代器的集合都只是简单地将自己宣传为一个链表。 假设您需要对其进行排序。 做什么?

显然,您从代码库中删除了链接列表并将其替换为二叉树。 或者,如果您想花哨地使用平衡搜索树,例如 avl、red-black,正如 Ian 等人提出的我不知道多少年前的建议。 这仍然没有在 golang 中普遍完成。 现在这将是要走的路。

另一个解决方案是在 O(N) 时间内快速循环迭代器,将指向元素的指针收集到通用指针的切片中,表示为[]*T ,并使用较差的切片排序对这些通用指针进行排序

请给别人的想法一个机会

@go-li 如果我们想避免此处未发明综合症,我们应该向 Alex Stepanov 寻求定义,因为他几乎发明了泛型编程。 以下是我将如何定义它,取自 Stepanov 的“编程元素”第 111 页:

Bidirectional iterator<T> =
    ForwardIterator<T>
/\ predecessor : T -> T
/\ predecessor takes constant time
/\ (forall i in T) successor(i) is defined =>
        predecessor(successor(i)) is defined and equals i
/\ (forall i in T) predecessor(i) is defined =>
        successor(predecessor(i)) is defined and equals i

这取决于 ForwardIterator 的定义:

ForwardIterator<T> =
    Iterator<T>
/\ regular_unary_function(successor)

所以本质上我们有一个接口,它声明了一个successor函数和一个predecessor函数,以及它们必须遵守的一些公理才能有效。

关于legacy并不是 legacy 会出错,目前 Go 显然不会出错,但是编译器缺少优化机会,类型系统缺少进一步传播具体类型的机会。 它还限制了程序员准确地指定他们的意图。 一个例子是一个标识函数,我的意思是准确地返回它传递的类型:

id(x T) T

也许还值得一提的是参数类型和普遍量化类型之间的区别。 参数类型是id<T>(x T) T而全称量词是id(x T) T (在这种情况下,我们通常省略最外层的全称量词forall T )。 对于参数类型,类型系统必须具有在调用站点为id提供的 T 类型,只要 T 在编译完成之前与具体类型统一,就不需要通用量化。 另一种理解方式是参数函数不是类型而是类型的模板,并且它只是在 T 被替换为具体类型之后的有效类型。 使用通用量化函数id实际上有一个类型forall T . T -> T可以由编译器传递,就像Int一样。

@go-li

显然,您从代码库中删除了链接列表并将其替换为二叉树。 或者,如果您想花哨地使用平衡搜索树,例如 avl、red-black,正如 Ian 等人提出的我不知道多少年前的建议。 这仍然没有在 golang 中普遍完成。 现在这将是要走的路。

拥有有序的数据结构并不意味着您永远不需要对数据进行排序。

如果我们想避免此处未发明综合症,我们应该向 Alex Stepanov 寻求定义,因为他几乎发明了泛型编程。

我会反驳任何关于泛型编程是由 C++ 发明的说法。 阅读 Liskov 等人。 1977 CACM 论文,如果你想看到一个早期的通用编程模型,它确实有效(类型安全、模块化、无代码膨胀): https ://dl.acm.org/citation.cfm?id=359789(参见第 4 节)

我认为我们应该停止这个讨论,等待 golang 团队(russ)发表一些博客文章,然后实施解决方案 👍(见 vgo)他们会这样做的 🎉

https://peter.bourgon.org/blog/2018/07/27/a-response-about-dep-and-vgo.html

我希望这个故事可以作为对其他人的警告:如果您有兴趣为 Go 项目做出实质性贡献,那么再多的独立尽职调查也无法弥补并非源自核心团队的设计。

该线程显示了核心团队如何对积极参与与社区一起寻找解决方案不感兴趣。

但最后,如果他们能再次自己解决问题,我没问题,就去做吧👍

@andrewcmyers好吧,也许“发明”有点牵强,这可能更像是 1971 年的 David Musser,后来他与 Stepanov 一起为 Ada 开发了一些通用库。

Elements of Programming 不是一本关于 C++ 的书,示例可能是 C++ 的,但那是完全不同的东西。 我认为这本书对于任何想用任何语言实现泛型的人来说都是必不可少的读物。 在解雇 Stepanov 之前,您应该真正阅读这本书以了解其实际内容。

在 GitHub 可扩展性的限制下,这个问题已经很紧张了。 请让这里的讨论集中在 Go 提案的具体问题上。

Go 走 C++ 路线是不幸的。

@andrewcmyers是的,我完全同意,请不要将 C++ 用于语法建议或作为正确做事的基准。 相反,请查看 D 以获得灵感

@游牧软件

我非常喜欢 D,但是 Go 需要 D 提供的强大的编译时元编程特性吗?

我也不喜欢 C++ 中源自石器时代的模板语法。

但是普通的 ParametricType 呢?Java 或 C# 中的标准,如果需要,也可以使用 ParametricType 重载它

更进一步,我不喜欢 D 中带有 bang 符号的模板调用语法,现在 bang 符号被用来表示对函数参数的可变或不可变访问。

@nomad-software 我并不是说 C++ 语法或模板机制是进行泛型的正确方法。 Stepanov 定义的更多“概念”将类型视为代数,这在很大程度上是进行泛型的正确方法。 查看 Haskell 类型类以了解其外观。 如果您了解发生了什么,Haskell 类型类在语义上非常接近 c++ 模板和概念。

所以 +1 表示不遵循 c++ 语法,+1 表示不实现类型不安全的模板系统 :-)

@keean D 语法的原因是完全避免<,>并遵守上下文无关语法。 这是我将 D 用作灵感的一部分。 <,>对于泛型参数的语法来说是一个非常糟糕的选择。

@nomad-software 正如我在上面指出的(在现在隐藏的注释中),您需要为参数类型指定类型参数,但不是为普遍量化的类型指定类型参数(因此 Rust 和 Haskell 之间的区别,处理类型的方式实际上是不同的在类型系统中)。 还有 C++ 概念 == Haskell 类型类 == Go 接口,至少在概念层面。

D语法真的更可取吗:

auto add(T)(T lhs, T rhs) {
    return lhs + rhs;

为什么这比 C++/Java/Rust 风格更好:

T add<T>(T lhs, T rhs) {
    return lhs + rhs;
}

或 Scala 风格:

T add[T](T lhs, T rhs) {
    return lhs + rhs;
}

我对类型参数的语法做了一些思考。 我从来都不喜欢 C++ 和 Java 中的“尖括号”,因为它们使解析变得非常棘手,从而阻碍了工具的开发。 方括号实际上是一个经典的选择(来自 CLU、System F 和其他具有参数多态性的早期语言)。

然而,Go 的语法非常敏感,可能是因为它已经很简洁了。 基于方括号或括号的可能语法会产生语法歧义,甚至比尖括号引入的语法更加糟糕。 因此,尽管我有这样的倾向,尖括号似乎实际上是 Go 的最佳选择。 (当然,也有真正的尖括号不会产生任何歧义——⟨⟩——但它们需要使用 Unicode 字符)。

当然,用于类型参数的精确语法不如正确使用语义重要。 在这一点上,C++ 语言是一个糟糕的模型。 我的研究小组在Genus (PLDI 2015) 和Familia (OOPSLA 2017) 中的泛型工作提供了另一种扩展类型类并将它们与接口统一的方法。

@andrewcmyers我认为这两篇论文都很有趣,但我想说这对 Go 来说不是一个好的方向,因为 Genus 是面向对象的,而 Go 不是,而且 Familia 统一了子类型和参数多态性,而 Go 两者都没有。 我认为 Go 应该简单地采用参数多态或通用量化,它不需要子类型化,并且在我看来是没有它的更好的语言。

我认为 Go 应该寻找不需要面向对象且不需要子类型的泛型。 Go 已经有了接口,我认为这是一个很好的泛型机制。 如果您可以看到 Go 接口 == c++ 概念 == Haskell 类型类,那么在我看来,添加泛型同时保持“Go”风格的方法是扩展接口以采用多个类型参数(我会也像接口上的关联类型,但这可能是它的单独扩展,有助于接受多个类型参数)。 这将是关键的变化,但要实现这一点,函数签名中的接口需要有一个“替代”语法,这样您就可以将多个类型参数传递给接口,这就是整个尖括号语法的用武之地.

Go 接口不是类型类——它们只是类型——但将接口与类型类统一是 Familia 展示的一种方法。 Genus 和 Familia 的机制与完全面向对象的语言无关。 Go 接口已经以重要的方式使 Go “面向对象”,所以我认为这些想法可以以稍微简化的形式进行调整。

@andrewcmyers

Go 接口不是类型类——它们只是类型

它们对我来说不像类型,因为它们允许多态性。 像 Addable[] 这样的多态数组中的对象仍然具有其实际类型(通过运行时反射可见),因此它们的行为与单参数类型类完全一样。 它们在类型签名中代替类型的事实只是省略了类型变量的简写符号。 不要将符号与语义混淆。

f(x : Addable) == f<T>(x : T) requires Addable<T>

这个身份当然只对单参数接口有效。

接口和单参数类型类之间唯一显着的区别是接口是在本地定义的,但这很有用,因为它避免了 Haskell 对其类型类的全局一致性问题。 我认为这是设计领域的一个有趣点。 多参数接口将为您提供多参数类型类的所有功能,并具有本地化的优势。 无需向 Go 语言添加任何继承或子类型(我认为这是定义 OO 的两个关键特性)。

恕我直言:

仍然具有默认类型比专用于表达类型限制的 DSL 更可取。 就像拥有一个函数f(s T fmt.Stringer)一样,它是一个通用函数,它接受任何类型,也就是/满足fmt.Stringer接口。

这样就可以有一个通用的功能,如:

func add(a, b T int) T int {
    return a + b
}

现在函数add()适用于任何类型Tint s 支持+运算符。

@dc0d我同意这看起来很有吸引力,看看当前的 Go 语法。 然而,它并不“完整”,因为它不能代表泛型所需的所有约束,并且仍然会推动进一步扩展。 这将导致不同语法的激增,我认为这与简单的目标相冲突。 我的观点是简单并不简单,它必须是最简单的,但仍能提供所需的表达能力。 目前我看到 Go 在通用表达能力方面的主要限制是缺乏多参数接口。 例如,一个 Collection 接口可以定义为:

type T U Collection interface {
   member(c T, v U) Bool
   insert(c T, v U) T
}

所以这是有道理的,对吧? 我们想在集合之类的东西上编写接口。 所以问题是如何在函数中使用这个接口。 我的建议是这样的:

func[T, U] f(c T, e U) (Bool, T) requires Collection[T, U] {
   a := member(c, e)
   d := insert(c, e)
   return a, d
}

语法只是一个建议,但我并不介意语法是什么,只要你能用语言表达这些概念。

@keean如果我说我根本不介意语法,那将是不准确的。 但重点是强调每个泛型参数都有一个默认类型。 从这个意义上说,提供的接口示例将变为:

type Collection interface (T interface{}, U interface{}) {
   member(c T, v U) Bool
   insert(c T, v U) T
}

现在(T interface{}, U interface{})部分有助于定义约束。 例如,如果成员旨在满足fmt.Stringer ,那么定义将是:

type Collection interface (T fmt.Stringer, U fmt.Stringer) {
   member(c T, v U) Bool
   insert(c T, v U) T
}

@dc0d在您希望通过多个类型参数进行约束的意义上,这将再次受到限制,请考虑:

type OrderedCollection[T, U] interface
   requires Collection[T, U], Ord[U] {...}

我想我知道你从哪里来的参数放置,你可以有:

type OrderedCollection interface(T, U)
   requires Collection(T, U), Ord(U) {...}

正如我所说,我对语法并不太在意,因为我可以习惯大多数语法。 从上面我认为你更喜欢多参数接口的括号'()'。

@keean让我们考虑一下heap.Interface接口。 标准库中的当前定义是:

type Interface interface {
    sort.Interface
    Push(x interface{}) // add x as element Len()
    Pop() interface{}   // remove and return element Len() - 1.
}

现在让我们将其重写为通用接口,使用默认类型:

type Interface interface (T interface{}) {
    sort.Interface
    Push(x T) // add x as element Len()
    Pop() T   // remove and return element Len() - 1.
}

这没有破坏任何 Go 1.x 代码系列。 一种实现是我对类型别名重新绑定的提议。 但我相信可以有更好的实现。

拥有默认类型允许我们编写可与 Go 1.x 样式代码一起使用的通用代码。 标准库可以成为通用库,而不会破坏任何东西。 这是国际海事组织的大胜利。

@dc0d ,所以您建议进行增量改进? 你的建议在我看来是一种渐进式的改进,但它仍然具有有限的通用表达能力。 您将如何实现“Collection”和“OrderedCollection”接口?

考虑到与以最简单的方式实现完整解决方案相比,几个部分语言扩展可能会导致更复杂的最终产品(具有多种替代语法)。

@keean我不明白requires Collection[T, U], Ord[U]部分。 他们如何限制类型参数TU

@dc0d它们的工作方式与函数相同,但适用于所有内容。 因此,对于作为 OrderedCollection 的任何类型 TU 对,我们要求 TU 也是 Collection 的实例,并且 U 是 Ord。 因此,在我们使用 OrderedCollection 的任何地方,我们都可以酌情使用 Collection 和 Ord 中的方法。

如果我们是极简主义者,则不需要这些,因为我们可以在需要它们的函数类型中包含额外的接口,例如:

type OrderedCollection interface(T, U)
{
   first(c T) U
}

func[T] first(c T[]) T requires Collection(T[], T), Ord T
{...}

func[T] f(c T[]) requires OrderedCollection(T[], T), Collection(T[], T), Ord(T)
{...}

但这可能更具可读性:

type OrderedCollection interface(T, U) 
   requires Collection(T, U), Ord(U)
{
   first(c T) U
}

func[T] first(c T[]) T
{...}

func[T] f(c T[]) requires OrderedCollection(T[], T)
{...}

@keean (IMO)只要类型参数有强制性的默认值,我就感到高兴。 这样就可以保持与 Go 1.x 代码系列的向后兼容性。 这是我试图提出的主要观点。

@基恩

Go 接口不是类型类——它们只是类型

它们对我来说不像类型,因为它们允许多态性。

是的,它们允许亚型多态性。 Go 通过接口类型进行子类型化。 它没有明确声明的子类型层次结构,但这在很大程度上是正交的。 Go 不完全面向对象的原因是缺乏继承。

或者,您可以将接口视为类型类的存在量化应用程序。 我相信这就是你的想法。 这就是我们在 Genus 和 Familia 中所做的。

@andrewcmyers

是的,它们允许亚型多态性。

据我所知,Go 是不变的,没有协变或逆变,这强烈说明这不是子类型。 多态类型系统是不变的,所以在我看来 Go 似乎更接近这种模型,将接口视为单参数类型类似乎更符合 Go 的简单性。 缺乏协变和逆变对泛型来说是一个很大的好处,只要看看这些东西在像 C# 这样的语言中造成的混乱:

https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance

我认为 Go 应该完全避免这种复杂性。 对我来说,这意味着我们不希望泛型和子类型在同一个类型系统中。

或者,您可以将接口视为类型类的存在量化应用程序。 我相信这就是你的想法。 这就是我们在 Genus 和 Familia 中所做的。

因为 Go 在运行时有类型信息,所以不需要存在量化。 在 Haskell 中,类型是未装箱的(如原生“C”类型),这意味着一旦我们将某些东西放入存在集合中,我们就无法(轻松)恢复内容的类型,我们所能做的就是使用提供的接口(类型类)。 这是通过在原始数据旁边存储指向接口的指针来实现的。 在 Go 中,数据的类型被存储,数据是“装箱”(如 C# 装箱和未装箱数据)。 因此,Go 不仅限于与数据一起存储的接口,因为可以(通过使用类型案例)恢复集合中数据的类型,这只能在 Haskell 中通过实现“反射”来实现typeclass(虽然很难将数据取出来,但可以序列化类型和数据,比如字符串,然后在存在框之外反序列化)。 所以我得出的结论是,如果 Haskell 将“反射”类型类作为内置函数提供,Go 接口的行为与类型类完全一样。 因此没有存在框,我们仍然可以对集合的内容进行大小写,但接口的行为与类型类完全一样。 Haskell 和 Go 之间的区别在于装箱数据与未装箱数据的语义,接口是单参数类型类。 实际上,当“Go”将接口视为一种类型时,它实际上在做的是:

Addable[] == exists T . T[] requires Addable[T], Reflection[T]

可能值得注意的是,这与“特征对象”在 Rust 中的工作方式相同。

Go 可以完全避免存在(对程序员可见)、协变和逆变,这是一件好事,在我看来,这将使泛型更简单、更强大。

据我所知,Go 是不变的,没有协变或逆变,这强烈说明这不是子类型。

多态类型系统是不变的,所以对我来说它似乎更接近这个模型,将接口视为单参数类型类似乎更符合 Go 的简单性。

我可以建议你们两个都是对的吗? 因为接口等价于类型类,但类型类是子类型的一种形式。 到目前为止,我发现的子类型定义都非常模糊和不精确,归结为“A 是 B 的子类型,如果一个可以替代另一个”。 IMO 可以很容易地认为类型类可以满足这一点。

请注意,方差参数本身并不能真正起作用 IMO。 方差是类型构造函数的属性,而不是语言。 而且,并非语言中的所有类型构造函数都是变体是很正常的(例如,许多具有子类型的语言都有可变数组,这些数组必须是不变的才能保证类型安全)。 所以我不明白为什么没有变体类型构造函数就不能进行子类型化。

另外,我认为这个讨论对于 Go 存储库上的问题来说有点过于宽泛了。 这不应该讨论类型理论的复杂性,而是讨论是否以及如何将泛型添加到 Go。

@Merovius Variance 是与子类型相关的属性。 在没有子类型的语言中,没有差异。 为了首先存在差异,您必须进行子类型化,这将协变/逆变问题引入类型构造函数。 但是,您是对的,在具有子类型化的语言中,可以使所有类型构造函数保持不变。

类型类绝对不是子类型,因为类型类不是类型。 然而,我们可以将 Go 中的“接口类型”视为 Rust 所说的“特征对象”,实际上是从类型类派生的类型。

目前,Go 的语义似乎适合任一模型,因为它没有差异,并且具有隐含的“特征对象”。 所以也许 Go 正处于一个临界点,泛型和类型系统可以沿着子类型化的路线开发,引入差异并最终得到类似于 C# 中的泛型的东西。 或者,Go 可以引入多参数接口,允许集合的接口,这将打破接口和“接口类型”之间的直接联系。 例如,如果您有:

type (T, U) Collection interface {
    member : (c T, e U) Bool
    insert: (c T, e U) T
}

member(c int32[], e int32) Bool {...}
insert(c int32[], e int32) int32[] {...}

member(c float32[], e float32) Bool {...}
insert(c float32[], e float32) float32[] {...}

类型 T、U 和接口 Collection 之间不再有明显的子类型关系。 所以你只能将实例类型和接口类型之间的关系看作是单参数接口的特殊情况的子类型,我们不能用单参数接口来表达集合之类的抽象。

我认为对于泛型,你显然需要能够对集合之类的东西进行建模,所以多参数接口对我来说是必须的。 但是,我认为泛型中协变和逆变之间的交互创建了一个过于复杂的类型系统,所以我想避免子类型化。

@keean由于接口可以用作类型,而类型类不是类型,因此对 Go 语义最自然的解释是接口不是类型类。 我了解您正在争论将接口概括为类型类; 我认为采用这种语言是一个合理的方向,事实上我们已经在我们发表的作品中广泛地探索了这种方法。

至于 Go 是否有子类型,请考虑以下代码:

package main

type Cloneable interface {
    Clone() Cloneable
}

type CloneableZ interface {
    Clone() Cloneable
    zero() int
}

type S struct {}

func (t S) Clone() Cloneable {
    c := t
    return c
}

func (t S) zero() int {
    return 0
}

var x CloneableZ = S{}
var y Cloneable = x

func main() {
    print("ok\n")
}

xy的赋值表明y的类型可以用于预期x的类型。 这是一种子类型关系,即: CloneableZ <: CloneableS <: CloneableZ 。 即使您根据类型类来解释接口,这里仍然存在子类型关系,例如S <: ∃T.CloneableZ[T] <: ∃T.Cloneable[T]

请注意,Go 允许函数Clone返回S是完全安全的,但 Go 碰巧强制执行不必要的限制性规则以符合接口:事实上,与 Java 相同的规则原来强制执行。 正如@Merovius 所观察到的,子类型化不需要非不变的类型构造函数。

@andrewcmyers多参数接口会发生什么,比如抽象集合所必需的接口?

此外,从 x 到 y 的分配可以看作是完全没有子类型化的接口继承。 在 Haskell(显然没有子类型)中,你会写:

class Cloneable t => CloneableZ t where...

我们有x是实现CloneableZ的类型,根据定义它也实现Cloneable ,因此显然可以分配给y

总结一下,您可以将接口视为一种类型,而 Go 具有有限的子类型,没有协变或逆变类型构造函数,或者您可以将其视为“特征对象”,或者在 Go 中我们将其称为“接口对象”,它实际上是受接口“类型类”约束的多态容器。 在 typeclass 模型中没有子类型,因此没有理由必须考虑协变和逆变。

如果我们坚持子类型化模型,我们就不能拥有集合类型,这就是 C++ 必须引入模板的原因,因为面向对象的子类型化不足以泛型定义容器等概念。 我们最终得到了两种抽象机制,对象和子类型,以及模板/特征和泛型,两者之间的交互变得复杂,以 C++、C# 和 Scala 为例。 将继续调用引入协变和逆变构造函数以增加泛型的功能,与其他语言保持一致。

如果我们想要泛型集合而不引入单独的泛型系统,那么我们应该考虑像类型类这样的接口。 多参数接口意味着不再考虑子类型化,而是考虑接口继承。 如果我们想改进 Go 中的泛型,并允许对集合之类的东西进行抽象,并且我们不希望 C++、C#、Scala 等语言的类型系统复杂,那么多参数接口和接口继承就是方法去。

@基恩

多参数接口会发生什么,比如抽象集合所必需的接口?

请参阅我们关于 Genus 和 Familia 的论文,它们确实支持多参数类型约束。 Familia 将这些约束与接口统一起来,并允许接口约束多种类型。

如果我们坚持子类型模型,我们就不能有集合类型

我不完全确定您所说的“子类型模型”是什么意思,但很明显 Java 和 C# 具有集合类型,所以这种说法对我来说没有多大意义。

x 是一个实现 CloneableZ 的类型,根据定义它也实现了 Cloneable,因此显然可以分配给 y。

不,在我的示例中,x 是一个变量,y 是另一个变量。 如果我知道 y 是某种CloneableZ类型而 x 是某种Cloneable类型,那并不意味着我可以从 y 分配给 x。 这就是我的例子正在做的事情。

为了阐明对 Go 建模需要子类型,下面是示例的锐化版本,其道德等价物不在 Haskell 中进行类型检查。 该示例显示子类型化允许创建不同元素具有不同实现的异构集合。 此外,这组可能的实现是开放式的。

type Cloneable interface {
    Clone() Cloneable
}

type CloneableZ interface {
    Clone() Cloneable
    zero() int
}

type S struct {}

func (t S) Clone() Cloneable {
    c := t
    return c
}

type T struct { x int }

func (t T) Clone() Cloneable {
    c := t
    return c
}

func (t S) zero() int {
    return 0
}

var x CloneableZ = S{}
var y Cloneable = T{}
var a [2]Cloneable = [2]Cloneable{x, y}

@andrewcmyers

我不完全确定您所说的“子类型模型”是什么意思,但很明显 Java 和 C# 具有集合类型,所以这种说法对我来说没有多大意义。

看看为什么 C++ 开发模板,OO 子类型模型无法表达泛化诸如集合之类的事物所必需的通用概念。 C# 和 Java 还必须引入一个与对象、子类型和继承分离的完整泛型系统,然后必须清理这两个系统与协变和逆变类型构造函数等复杂交互的混乱。 事后看来,我们可以避免 OO 子类型化,而是看看如果我们将接口(类型类)添加到简单类型的语言会发生什么。 这就是 Rust 所做的,因此值得一看,但它当然会因为整个生命周期的事情而变得复杂。 Go 有 GC,所以它不会那么复杂。 我的建议是 Go 可以扩展为允许多参数接口,并避免这种复杂性。

关于您声称不能在 Haskell 中执行此示例的说法,代码如下:

{-# LANGUAGE ExistentialQuantification #-}

class ICloneable t where
    clone :: t -> t

class ICloneable t => ICloneableZ t where
    zero :: t

data S = S deriving Show

instance ICloneable S where
    clone x = x

data T = T Int deriving Show

instance ICloneable T where
    clone x = x

instance ICloneableZ T where
    zero = T 0

data Cloneable = forall a . (ICloneable a, Show a) => ToCloneable a

instance Show Cloneable where
    show (ToCloneable x) = show x

main = do
    x <- return S
    y <- return (T 27)
    a <- return [ToCloneable x, ToCloneable y]
    putStrLn (show a)

一些有趣的区别,Go 自动派生这种类型data Cloneable = forall a . (ICloneable a, Show a) => ToCloneable a因为这是你如何将接口(没有存储)转换为类型(有存储),Rust 也派生这些类型并将它们称为“特征对象” . 在 Java、C# 和 Scala 等其他语言中,我们发现您无法实例化接口,这实际上是“正确的”,接口不是类型,它们没有存储空间,Go 会自动为您派生存在容器的类型,以便您可以处理接口就像一个类型,而 Go 通过为存在容器提供与其派生的接口相同的名称来隐藏它。 另一件要注意的是,这[2]Cloneable{x, y}将所有成员强制为Cloneable ,而 Haskell 没有这种隐式强制,我们必须使用ToCloneable显式强制成员.

还有人向我指出,我们不应该考虑 $#$ Cloneable $#$ 的ST子类型,因为ST不是结构上兼容。 我们可以从字面上将任何类型声明为 Cloneable 的实例(只需在 Go 中声明函数clone的相关定义)并且这些类型之间根本不需要相互关联。

大多数泛型提案似乎都包含了额外的标记,我认为这会损害 Go 的可读性和简单的感觉。 我想提出一种不同的语法,我认为它可以很好地与 Go 现有的语法一起使用(甚至恰好在 Github Markdown 中的语法高亮显示得很好)。

提案要点:

  • Go 的语法似乎总是有一种简单的方法来确定类型声明何时结束,因为我们正在寻找一些特定的标记或关键字。 如果在所有情况下都是如此,则可以简单地在类型名称本身之后添加类型参数。
  • 像大多数提案一样,相同的标识符在任何函数声明中都意味着相同的类型。 这些标识符永远不会逃脱声明。
  • 在大多数提案中,您必须声明泛型类型参数,但在此提案中它是隐含的。 有些人会声称这会损害可读性或清晰度(隐含性不好),或者限制命名类型的能力,反驳如下:

    • 当谈到损害可读性时,我认为你可以用任何一种方式争论,额外的或 [T] 通过产生大量语法噪音同样会损害可读性。

    • 如果使用得当,隐含性可以帮助语言不那么冗长。 我们总是用:=省略类型声明,因为它所隐藏的信息根本不够重要,无法每次都拼出来。

    • 命名具体(非泛型)类型at可能是不好的做法,因此本提案假定保留这些标识符作为泛型类型参数是安全的。 虽然这可能需要进行修复迁移?

package main

import "fmt"

type LinkedList a struct {
  Head *Node a
  Tail *Node a
}

type Node a {
  Next *Node a
  Prev *Node a

  Value a
}

func main() {
  // Not sure about how recursive we could get with the inference
  ll := LinkedList string {
    // The string bit could be inferred
    Head: Node string { Value: "hello world" },
  }
}

func (l *LinkedList a) Append(value a) {
  newNode := &Node{Value: value}

  if l.Tail == nil {
    l.Head = newNode
    l.Tail = l.Head
    return
  }

  l.Tail.Next = newNode
  l.Tail = l.Tail.Next
}

这取自一个有更多细节的要点以及此处提出的总和类型: https ://gist.github.com/aarondl/9b950373642fcf5072942cf0fca2c3a2

这不是一个完全淘汰的泛型提案,它也不是,有很多问题需要解决才能将泛型添加到 Go。 这个只处理语法,我希望我们可以就所提议的内容是否可行/可取进行对话。

@aarondl
对我来说看起来不错,使用这种语法我们会:

type Collection a b interface {
   member(c a, e b) Bool
   insert(c a, e b) a
}

func insert(c *LinkedList a, e a) *LinkedList a {
   c.Append(e)
   return c
}

@keean请您稍微解释一下Collection类型。 我无法理解它:

type Collection a b interface {
   member(c a, e b) Bool
   insert(c a, e b) a
}

@dc0d Collection 是一个抽象 _all_ 集合的接口,因此树、列表、切片等,所以我们可以有像memberinsert这样的通用操作,它们适用于包含任何数据类型的任何集合。 在上面我给出了在上一个示例中为 LinkedList 类型定义“插入”的示例:

func insert(c *LinkedList a, e a) *LinkedList a {
   c.Append(e)
   return c
}

我们也可以为切片定义它

func insert(c []a, e a) []a {
   return append(c, e)
}

但是,我们甚至不需要具有类型变量的参数函数,如@aarondl所示,具有多态类型a即可工作,因为您可以为具体类型定义:

func insert(c *LinkedList int, e int) *LinkedList int {
   c.Append(e)
   return c
}

func insert(c *LinkedList float, e float) *LinkedList float {
   c.Append(e)
   return c
}

func insert(c int[], e int) int[] {
   return append(c, e)
}

func insert(c float[], e float) float[] {
   return append(c, e)
}

所以Collection是一个用于泛化容器类型及其内容类型的接口,允许编写对容器和内容的所有组合进行操作的通用函数。

没有理由你不能也有一个集合切片[]Collection ,其中内容都是具有不同值类型的不同集合类型,前提是为每个组合定义了memberinsert .

@aarondl鉴于type LinkedList a已经是一个有效的类型声明,我只能看到两种方法可以明确地使其可解析:使语法上下文敏感(进入解析 C 的问题,呃)或使用无界前瞻( go 语法倾向于避免这种情况,因为在失败情况下会出现错误的错误消息)。 我可能会误解一些东西,但 IMO 反对无令牌方法。

Go 中的@keean接口使用方法,而不是函数。 在您建议的特定语法中,没有任何东西将insert附加到*LinkedList用于编译器(在 Haskell 中,这是通过instance声明完成的)。 方法改变它们正在操作的值也是正常的。 这些都不是一个显示停止器,只是指出您建议的语法不适用于 Go。 可能更像是

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

这还展示了更多关于类型参数的作用域以及应该如何解析的问题。

@aarondl我还有更多关于您的提案的问题。 例如,它不允许约束,因此您只能获得不受约束的多态性。 一般来说,这并不是那么有用,因为你不能对你得到的值做任何事情(例如,你不能用地图实现 Collection,因为并非所有类型都是有效的地图键)。 当有人试图做这样的事情时会发生什么? 如果它是一个编译时错误,它是抱怨实例化(前面的 C++ 错误消息)还是在定义(你基本上什么都做不了,因为没有任何东西适用于所有类型)?

@keean我仍然不明白如何将a限制为列表(或切片或任何其他集合)。 这是集合的上下文相关的特殊语法吗? 如果有,它的价值是什么? 不能以这种方式声明用户定义的类型。

@Merovius 这是否意味着 Go 不能进行多次调度,并使“函数”的第一个参数变得特殊? 这表明关联类型比多参数接口更适合。 像这样的东西:

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt(c Collection, e Collection.Element) {...}

然而,这仍然有问题,因为没有什么限制这两个集合是相同的类型......你最终需要类似的东西:

func[A] useIt(c A, e A.Element) requires A:Collection

为了解释差异,多参数接口有额外的_input_类型参与实例选择(因此与multiple-dispatch联系),而关联类型是_output_类型,只有接收器类型参与实例选择,然后关联的类型取决于接收器的类型。

@dc0d ab是接口的类型参数,就像在 Haskell 类型类中一样。 对于要被视为Collection的东西,它必须定义与接口中的类型匹配的方法,其中ab可以是任何类型。 然而,正如@Merovius所指出的,Go 接口是基于方法的,并且不支持多分派,因此多参数接口可能不适合。 使用 Go 的单调度方法模型,在接口中使用关联类型而不是多参数似乎更合适。 然而,缺乏多重分派使得unify(x, y)之类的功能难以实现,并且您必须使用不太好的双重分派模式。

进一步解释多参数的事情:

type Cloneable[A] interface {
   clone(x A) A
}

这里a代表任何类型,我们不在乎它是什么,只要定义了正确的函数,我们就认为它是Cloneable 。 我们会将接口视为对类型的约束,而不是类型本身。

func clone(x int) int {...}

所以在'clone'的情况下,我们用a替换接口定义中的int ,如果替换成功,我们可以调用clone。 这很好地符合这个符号:

func[A] test(x A) A requires Cloneable[A] {...}

这相当于:

type Cloneable interface {
   clone() Cloneable
}

但声明的是函数而不是方法,并且可以使用多个参数进行扩展。 如果您有一种具有多次调度的语言,那么函数/方法的第一个参数没有什么特别之处,那么为什么要把它写在不同的地方。

由于 Go 没有多次调度,这一切开始让人觉得一次改变太多了。 似乎关联类型会更合适,尽管更有限。 这将允许抽象集合,但不能优雅地解决统一等问题。

@Merovius感谢您查看该提案。 让我试着解决你的担忧。 很遗憾,在我们进一步讨论之前,你拒绝了这个提议,我希望我能改变你的想法——或者你可以改变我的想法:)

无限前瞻:
所以正如我在提案中提到的,目前看来 Go 语法有一种很好的方法来检测几乎所有语法的“结束”。 由于隐含的通用参数,我们仍然会这样做。 单字母小写是创建该通用参数的语法结构 - 或者我们决定制作该内联标记的任何内容,如果我们足够喜欢该@a但不是可能考虑到没有令牌的编译器困难,尽管一旦你这样做,该提案就会失去很多魅力。

不管这个提议下type LinkedList a的问题并不难,因为我们知道a是一个泛型类型参数,所以这会失败并出现与type LinkedList相同的编译器错误prog.go:3:16: expected type, found newline (and 1 more errors) 。 原来的帖子并没有真正出来说出来,但你不能再命名一个具体的类型[a-z]{1}我 - 认为 - 解决了这个问题并且是一种牺牲,我认为我们都可以接受制作(我现在只能看到在 Go 代码中创建具有单字母名称的真实类型的不利之处)。

它只是不受约束的多态性
我省略任何类型的特征或通用参数约束的原因是因为我觉得这是接口在 Go 中的作用,如果你想用一个值做某事,那么这个值应该是一个接口类型而不是一个完全通用的类型。 我认为这个提议也适用于接口。

在这个提议下,我们仍然会遇到与现在使用+之类的运算符相同的问题,因此您不能为所有数字类型创建一个通用的 add 函数,但您可以接受一个通用的 add 函数作为参数。 考虑以下:

func Sort(slice []a, compare func (a, a) bool) { ... }

关于范围界定的问题

你在这里举了一个例子:

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

这些标识符的范围通常与它们所在的特定声明/定义绑定。它们在任何地方都没有共享,我没有看到它们存在的理由。

@keean这很有趣,尽管正如其他人指出的那样,您必须更改您在那里展示的内容才能真正实现接口(目前在您的示例中没有带有接收器的方法,只有函数)。 试图更多地思考这对我最初的提案有何影响。

单字母小写是创建该通用参数的语法结构

我对此感觉不好; 它需要根据上下文对标识符的含义进行单独的生成,并且还意味着任意禁止某些类型的标识符。 但现在不是谈论这些细节的时候。

在这个提议下,我们仍然会遇到与现在使用 + 等运算符相同的问题

我不明白这句话。 目前,+ 运算符没有任何这些问题,因为它的操作数类型是本地已知的,并且错误消息清晰明确并指向问题的根源。 假设您说您要禁止使用所有可能类型都不允许使用的泛型值,我是否正确(我想不出很多这样的操作)? 并为泛型函数中的违规表达式创建编译器错误? IMO 会过多地限制仿制药的价值。

如果你想用一个值做某事,那么那个值应该是一个接口类型而不是一个完全通用的类型。

人们想要泛型的两个主要原因是性能(避免包装接口)和类型安全(确保在不同的地方使用相同的类型,而不关心它是哪一个)。 这似乎忽略了这些原因。

你可以接受一个通用的 add 函数作为参数。

真的。 但是很不符合人体工学。 考虑一下有多少关于sort API 的投诉。 对于许多通用容器,调用者必须实现和传递的函数数量似乎令人望而却步。 考虑一下, container/heap实现在该提案下的外观如何,在人体工程学方面它会比当前的实现更好吗? 看起来,这里的胜利充其量是微不足道的。 您必须实现更多琐碎的功能(并在每个使用站点复制/引用),而不是更少。

@Merovius

@aarondl思考这一点

你可以接受一个通用的 add 函数作为参数。

考虑到定义中缀运算符的一些语法,最好有一个 Addable 接口以允许重载加法:

type Addable interface {
   + (x Addable, y Addable) Addable
}

不幸的是,这不起作用,因为它没有表示我们期望所有类型都相同。 要定义 addable,我们需要类似多参数接口的东西:

type Addable[A] interface {
   + (x A, y A) A
}

然后你还需要 Go 进行多次调度,这意味着函数中的所有参数都被视为接口匹配的接收器。 所以在上面的例子中,任何类型都是Addable ,如果它上面定义了一个满足接口定义中的函数定义的函数+

但是鉴于这些更改,您现在可以编写:

type S struct {
   value: int
}

func (+) (x S, y S) S {
   return S {
      value: x.value + y.value
   }
}

func main() {
    println(S {value: 27} + S {value: 5})
}

当然,函数重载和多分派可能是人们在 Go 中永远不需要的东西,但是像在用户定义的类型(如向量、矩阵、复数等)上定义基本算术之类的事情总是不可能的。 就像我上面所说的,接口上的“关联类型”将允许增加泛型编程能力,但不能完全通用。 在 Go 中是否会发生多次调度(可能是函数重载)?

像在向量、矩阵、复数等用户定义类型上定义基本算术这样的事情总是不可能的。

有些人可能会认为这是一个功能:) AFAIR 有一些提案或线程在某处讨论是否应该这样做。 FWIW,我认为这是 - 再次 - 偏离主题。 运算符重载(或一般的“如何让 Go 变得更 Haskell”的想法)并不是这个问题的真正重点:)

在 Go 中是否会发生多次调度(可能是函数重载)?

永不说永不。 不过,我个人并不期待。

@Merovius

有些人可能认为这是一个功能:)

当然,如果 Go 不这样做,那么还有其他语言会 :-) Go 不必是所有人的一切。 我只是想在 Go 中为泛型建立一些范围。 我的重点是创建完全通用的语言,因为我讨厌重复自己和样板(而且我不喜欢宏)。 如果每次我不得不为某些特定的数据类型在“C”中编写一个链表或一棵树时都有一分钱。 它实际上使一些项目对于一个小团队来说是不可能的,因为你需要记住大量的代码来理解它,然后通过更改来维护它。 有时我认为不需要泛型的人只是还没有编写足够大的程序。 当然,你也可以让一大群开发人员在做某事,而每个开发人员只负责总代码的一小部分,但我有兴趣让单个开发人员(或小团队)尽可能高效。

鉴于函数重载和多次调度超出了范围,并且还考虑到@aarondl建议的解析问题,似乎将关联类型添加到接口,并将类型参数添加到函数将是你想要的在 Go 中使用泛型。

这样的事情似乎是正确的事情:

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt<T>(c T, e T.Element) requires T:Collection {...}

然后在实现中会决定是使用参数类型还是通用量化类型。 对于参数类型(如 Java),“通用”函数实际上不是一个函数,而是某种类型安全的函数模板,因此不能作为参数传递,除非它提供了它的类型参数:

f(useIt) // not okay with parametric types
f(useIt<List>) // okay with parametric types

对于通用量化类型,您可以将 useIt 作为参数传递,然后可以在f中为其提供类型参数。 支持参数类型的原因是因为您可以在编译时单态化多态性,这意味着在运行时无需详细说明多态函数。 我不确定这是 Go 的问题,因为 Go 已经在接口上进行运行时调度,所以只要useIt的类型参数实现 Collection,就可以在运行时调度到正确的接收者,所以通用量化可能是 Go 的正确方法。

我想知道,只有@bcmills 提到了 SFINAE。 提案中甚至没有提到(尽管以排序为例)。
切片和链表的Sort看起来如何呢?

@基恩
我不知道如何根据您的建议定义一个通用的“切片”集合。 您似乎正在定义一个可能正在实现“Collection”的“IntSlice”(尽管 Insert 返回的类型与接口所需的类型不同),但这不是通用的“切片”,因为它似乎仅适用于整数,并且方法实现仅适用于整数。 我们是否需要为每种类型定义特定的实现?

有时我认为不需要泛型的人只是还没有编写足够大的程序。

我可以向你保证,印象是错误的。 FWIW,ISTM 认为“另一方”将“没有看到需要”与“没有看到使用”放在同一个桶中。 看到用处,不反驳。 不过,我真的不认为有必要。 即使在大型代码库中,我也做得很好。

并且不要将“希望它们正确完成并指出现有提案不存在的地方”与“从根本上反对这个想法”混淆。

还给出了@aarondl建议的解析问题。

正如我所说,我不认为现在谈论解析问题真的很有成效。 解析问题可以解决。 在语义上,受限多态性的缺失要严重得多。 IMO,添加泛型而不这样做是不值得的。

@urandom

我不知道如何根据您的建议定义一个通用的“切片”集合。

如上所述,您仍然需要为每种类型的切片定义一个单独的实现,但是您仍然可以从能够根据通用接口编写算法中获益。 如果您想允许所有切片的通用实现,则需要允许参数关联类型和方法。 注意我将类型参数移到关键字之后,因此它出现在接收器类型之前。

type<T> []T.Element = Int

func<T> ([]T) Member(e T) Bool {...}
func<T> ([]T) Insert(e T) Collection {...}

但是,现在您还必须处理专业化问题,因为有人可以为更专业化的[]int定义关联的类型和方法,而您必须处理要使用的类型和方法。 通常你会选择更具体的实例,但它确实增加了另一层复杂性。

我不确定这实际上使您受益多少。 使用我上面的原始示例,您可以编写通用算法来使用接口作用于通用集合,并且您只需要为您实际使用的类型提供方法和关联类型。 对我来说主要的胜利是能够定义算法,比如对任意集合进行排序并将这些算法放入库中。 如果我有一个“形状”列表,我只需要为我的形状列表定义集合接口方法,然后我就可以对它们使用库中的任何算法。 能够为所有切片类型定义接口方法对我来说不太感兴趣,并且对于 Go 来说可能过于复杂?

@Merovius

不过,我真的不认为有必要。 即使在大型代码库中,我也做得很好。

如果您可以处理 100,000 行程序,那么您将能够使用 100,000 行通用行比使用 100,000 非通用行(由于重复)做更多的事情。 因此,您可能是一个超级明星开发人员,能够处理非常大的代码库,但您仍然可以使用非常大的通用代码库实现更多目标,因为您将消除冗余。 该通用程序将扩展到更大的非通用程序。 在我看来,您还没有达到复杂性限制。

但是我认为你是对的'需要'太强了,我很高兴编写 go 代码,只是偶尔会因为缺少泛型而感到沮丧,我可以通过简单地编写更多代码来解决这个问题,并且在 Go 中该代码是直接的和字面意思。

在语义上,缺乏受约束的多态性要严重得多。 IMO,添加泛型而不这样做是不值得的。

我同意这一点。

与使用 100,000 条非通用行相比,您将能够使用 100,000 条通用行做更多的事情(由于重复)

我很好奇,从您的假设示例中,这些行中有多少是通用函数?
根据我的经验,这不到 2%(来自具有 115k LOC 的代码库),所以我认为这不是一个好的论点,除非您为“集合”编写库

我真希望我们最终能得到仿制药

@基恩

关于您声称不能在 Haskell 中执行此示例的说法,代码如下:

这段代码在道德上并不等同于我编写的代码。 除了 ICloneable 接口之外,它还引入了新的 Cloneable 包装器类型。 Go 代码不需要包装器; 其他支持子类型的语言也不会。

@andrewcmyers

这段代码在道德上并不等同于我编写的代码。 除了 ICloneable 接口之外,它还引入了新的 Cloneable 包装器类型。

这不是这段代码的作用:

type Cloneable interface {...}

它引入了从接口派生的数据类型“Cloneable”。 您看不到“ICloneable”,因为您没有接口的实例声明,您只需声明方法。

当实现接口的类型不必在结构上兼容时,您是否可以将其视为子类型?

@keean我认为Cloneable仅仅是一种类型,而不是真正的“数据类型”。 在像 Java 这样的语言中, Cloneable抽象基本上不会增加​​成本,因为与您的代码不同,不会有包装器。

在我看来,要求实现接口的类型之间的结构相似性似乎是有限且不可取的,因此我对您在这里的想法感到困惑。

@andrewcmyers
我交替使用类型和数据类型。 任何可以包含数据的类型都是数据类型。

因为与您的代码不同,不会有包装器。

总是有一个包装器,因为 Go 类型总是被装箱的,所以包装器存在于所有东西周围。 Haskell 需要明确的包装器,因为它具有未装箱的类型。

实现接口的类型之间的结构相似性,所以我对你在这里的想法感到困惑。

结构子类型要求类型是“结构兼容的”。 由于没有像具有继承的 OO 语言那样的显式类型层次结构,因此子类型不能是名义上的,所以它必须是结构性的,如果它存在的话。

不过,我确实明白您的意思,我将其描述为将接口视为抽象基类,而不是接口,与实现所需方法的任何类型具有某种隐式标称子类型关系。

实际上,我认为 Go 现在适合这两种模型,并且它可以从这里开始,但我建议将其称为接口而不是类表明一种非子类型化的思维方式。

@keean我不明白你的评论。 首先你告诉我你不同意并且我“还没有达到我的复杂性限制”然后你告诉我你同意(因为“需要”这个词太强了)。 我还认为您的论点是错误的(您假设 LOC 是复杂性的主要衡量标准,并且每一行代码都是平等的)。 但最重要的是,我认为“谁在编写更复杂的程序”并不是真正富有成效的讨论。 我只是想澄清一下,“如果你不同意我的观点,那一定意味着你没有解决困难或有趣的问题”这一论点没有说服力,也没有诚意。 我希望您可以相信人们可以不同意您对该功能的重要性,同时同样有能力并做同样有趣的事情。

@merovius
我是说你可能是一个比我更有能力的程序员,因此能够以更复杂的方式工作。 我当然不认为你正在研究不那么有趣或不那么复杂的问题,我很抱歉它以这种方式出现。 我昨天花了很多时间试图让扫描仪工作,这是一个非常无趣的问题。

我可以认为泛型可以帮助我用我有限的脑力编写更复杂的程序,并且我也承认我并不“需要”泛型。 是学历的问题。 我仍然可以在没有泛型的情况下进行编程,但我不一定能编写同样复杂的软件。

我希望让你放心,我是真诚地行事,我在这里没有隐藏的议程,如果 Go 不采用泛型,我仍然会使用它。 我对做泛型的最佳方法有意见,但这不是唯一的意见,我只能从我自己的经验谈起。 如果我不帮忙,我还有很多其他的事情可以花时间,所以只要说出这个词,我就会重新专注于其他地方。

@Merovius感谢您继续对话。

| 人们想要泛型的两个主要原因是性能(避免包装接口)和类型安全(确保在不同的地方使用相同的类型,而不关心它是哪一个)。 这似乎忽略了这些原因。

也许我们正在以非常不同的方式看待我提出的建议,因为从我的角度来看,据我所知,它可以做到这两件事? 在链表示例中,没有使用接口进行包装,因此它的性能应该与为给定类型手写一样。 在类型安全方面它是相同的。 您可以在这里举一个反例来帮助我了解您的来源吗?

| 真的。 但是很不符合人体工学。 考虑一下有多少关于排序 API 的投诉。 对于许多通用容器,调用者必须实现和传递的函数数量似乎令人望而却步。 考虑一下,在这个提议下,容器/堆实现会如何看待,在人体工程学方面它会比当前的实现更好吗? 看起来,这里的胜利充其量是微不足道的。 您必须实现更多琐碎的功能(并在每个使用站点复制/引用),而不是更少。

我其实根本不关心这个。 我不相信函数的数量会让人望而却步,但我绝对愿意看到一些反例。 回想一下,人们抱怨的 API 不是您必须为其提供函数的 API,而是此处的原始 API: https ://golang.org/pkg/sort/#Interface 您需要在其中创建一种简单的新类型你的切片+类型,然后在上面实现3个方法。 鉴于与此接口相关的投诉和痛苦,创建了以下内容: https ://golang.org/pkg/sort/#Slice,我对此 API 没有任何问题,我们将恢复此 API 的性能损失根据我们正在讨论的提案,只需将定义更改为func Slice(slice []a, less func(a, a) bool)

container/heap数据结构而言,无论您接受什么通用提案,都需要完全重写。 container/heap就像sort包只是在您自己的数据结构之上提供算法,但两个包都不会拥有数据结构,否则我们将拥有[]interface{}和与此相关的费用。 大概我们会更改它们,因为由于泛型,您将能够拥有一个具有具体类型的切片的Heap ,这在我在这里看到的任何提案(包括我自己的)下都是正确的.

我试图梳理出我们对我所提议的观点的差异。 而且我认为分歧的根源(过去任何个人偏好的语法)是对通用类型没有限制。 但我仍在试图弄清楚这对我们有什么好处。 如果答案是任何涉及性能的地方都不允许使用接口,那么我在这里就没什么好说的了。

考虑以下哈希表定义:

// Hasher turns a key into a hash
type Hasher interface {
  func Hash() []byte
}

type HashTable v struct {
   Keys   []Hasher
   Values []v
}

// Note that the generic arguments must be repeated here and immediately
// understood without reading another line of code, which to me
// is a readability win over the sudden appearance of the K and V which are
// defined elsewhere in the code in the example below. This is of course because
// the tokenized type declarations with constraints are fairly painful in general
// and repeating them everywhere is simply too much.
func (h (*HashTable v)) Insert(key Hasher, value v) { ... }

我们是说[]Hasher由于性能/存储问题而无法启动,并且为了在 Go 中成功实现泛型,我们绝对必须具有以下内容?

// Without selecting another proposal I have no idea how the constraint might be defined or implemented so let's just pretend
type [K: Hasher, V] HashTable a struct {
   Keys   []K
   Values []V
}

func (h *HashTable) Insert(key K, value V) { ... }

希望你看到我来自哪里。 但是我绝对有可能不理解您希望对某些代码施加的约束。 也许有些用例我没有考虑过,无论我希望更全面地了解需求是什么以及提案如何使它们失败。

也许我们正在以非常不同的方式看待我提出的建议,因为从我的角度来看,据我所知,它可以做到这两件事?

您引用的部分中的“this”是指使用接口。 问题不在于您的提案也没有,而是您的提案不允许受约束的多态性,这排除了它们的大多数用法。 以及您为 where 接口建议的替代方案,它也没有真正解决泛型的核心用例(因为我提到的两件事)。

例如,您的提案(如最初编写的那样)实际上不允许编写任何类型的通用映射,因为这需要至少能够使用==比较键(这是一个约束,所以实现一个map 需要受约束的多态性)。

鉴于与此界面相关的投诉和痛苦,创建了以下内容: https ://golang.org/pkg/sort/#Slice

请注意,在您的泛型提案中,接口仍然是不可能的,因为它依赖于长度和交换的反射(因此,再次,您对切片操作有限制)。 即使我们接受该 API 作为泛型应该能够完成的下限(很多人不会。仍然有很多关于该 API 缺乏类型安全性的抱怨),您的提案也不会通过那个酒吧。

但同样,您引用了对您提出的特定点的响应,即您可以通过在 API 中传递函数文字来获得受约束的多态性。 您建议解决缺乏约束多态性的特定方法将需要或多或少地实现旧 API。 即你引用了我对这个论点的回应,然后你只是在重复:

根据我们正在讨论的提案,我们将通过简单地将定义更改为 func Slice(slice []a, less func(a, a) bool) 来恢复性能损失。

不过,那是旧的 API。 您是在说“我的提议不允许受约束的多态性,但这没问题,因为我们不能使用泛型,而是使用现有的解决方案(反射/接口)”。 好吧,用“我们可以在没有泛型的情况下为那些最基本的用例做人们已经在做的事情”来回应“你的提案不允许人们想要泛型的最基本用例”似乎并没有让我们明白任何地方,TBH。 一个泛型提案甚至不能帮助您编写基本的容器类型、排序、最大值……似乎并不值得。

在我在这里看到的任何提案(包括我自己的)下都是如此。

大多数泛型提案都包含一些约束类型参数的方法。 即表达“类型参数必须有一个Less方法”,或者“类型参数必须是可比较的”。 你的 - AFAICT - 没有。

考虑以下哈希表定义:

你的定义不完整。 a)密钥类型也需要相等,并且 b)您不会阻止使用不同的密钥类型。 即这将是合法的:

type hasherA uint64

func (a hasherA) Hash() []byte {
    b := make([]byte, 8)
    binary.BigEndian.PutUint64(b, uint64(a))
    return b
}

type hasherB string

func (b hasherB) Hash() []byte {
    return []byte(b)
}

h := new(HashTable int)
h.Insert(hasherA(42), 1)
h.Insert(hasherB("Hello world"), 2)

但它不应该是合法的,因为您使用不同的密钥类型。 即容器没有按照人们想要的程度进行类型检查。 您需要键和值类型上参数化哈希表

type HashTable k v struct {
    Keys []k
    Values []v
}

func (h *(HashTable k v)) Insert(key k, value v) {
    // You can't actually do anything with k, as it's unconstrained. i.e. you can't hash it, compare it…
    // Implementing this is impossible in your proposal.
}

// If it weren't impossible, you'd get this:
h := new(HashTable hasherA int)
h[hasherA(42)] = 1
h[hasherB("Hello world")] = 2 // compile error - can't use hasherB as hasherA

或者,如果有帮助,想象一下您正在尝试实现一个哈希集。 您会遇到同样的问题,但现在生成的容器没有对interface{}进行任何额外的类型检查。

这就是为什么您的提案没有解决最基本的用例:它依赖于接口来约束多态性,但实际上并没有提供任何方法来检查这些接口的一致性。 您可以有一致的类型检查有约束的多态性,但不能两者兼而有之。 但你需要两者。

为了在 Go 中成功实现泛型,我们绝对必须具备以下内容?

至少是这么觉得的,是的,差不多。 如果一个提案不允许编写类型安全的容器或排序,或者……它并没有真正向现有语言添加任何足以证明成本合理的东西。

@Merovius好的。 我想我已经明白想要什么了。 请记住,您的用例与我想要的相差甚远。 我并不真正渴望类型安全的容器,尽管我怀疑——正如你所说——这可能是少数人的意见。 我希望看到的一些最重要的事情是结果类型而不是错误和简单的切片操作,而无需在任何地方进行重复或反射,我的建议可以合理地解决这些问题。 但是,如果您的基本用例是在不使用接口的情况下编写通用容器,我可以从您的角度看到它“没有解决最基本的用例”,

请注意,在您的泛型提案中,此接口仍然是不可能的,因为它依赖于长度和交换的反射(因此,再次,您对切片操作有限制)。 即使我们接受该 API 作为泛型应该能够完成的下限(很多人不会。仍然有很多关于该 API 缺乏类型安全性的抱怨),您的提案也不会通过那个酒吧。

阅读本文很明显,您完全误解了通用切片在此提案下将/应该工作的方式。 正是通过这种误解,您得出了错误的结论,即“您的提案中仍然无法使用此接口”。 根据任何提议,通用切片必须是可能的,这就是的想法。 正如我所见,世界上的len()将被定义为: func len(slice []a) ,这是一个通用切片参数,这意味着它可以以非反射方式计算任何切片的长度。 正如我上面所说的(简单的切片操作),这是这个提议的很多要点,我很抱歉我无法通过我给出的例子和我提出的要点来很好地传达这一点。 通用切片应该能够像今天的[]int一样轻松使用,我再说一遍,任何没有解决这个问题的提议(切片/数组交换、赋值、len、cap 等) ) 在我看来是不足的。

说了这么多,现在我们真的很清楚彼此的目标是什么。 当我提出我所做的事情时,我非常说这只是一个语法建议,而且细节非常模糊。 但无论如何我们都进入了细节,其中一个细节最终是缺乏约束,当我写下来时,我只是没有想到它们,因为它们对于我想做的事情并不重要,这并不是说我们不能添加它们或者它们是不可取的。 继续使用建议的语法并尝试硬塞约束的主要问题是通用参数的定义当前(有意地)重复自身,因此没有引用其他地方的代码来确定约束等。如果我们要引入约束我不明白我们怎么能保持这个。

最好的反例是我们之前讨论的排序函数。

type Sort(slice []a:Lesser, less func(a:Lesser, a:Lesser)) { ... }

正如您所看到的,没有很好的方法来实现这一点,而泛型的令牌垃圾邮件方法又开始听起来更好了。 为了定义这些约束,我们需要从原始提案中改变两件事:

  • 需要有一种方法可以指向类型参数并对其进行约束。
  • 约束需要比单个定义持续更长的时间,也许该范围是一种类型,也许该范围是一个文件(文件实际上听起来很合理)。

免责声明:以下不是对提案的实际修改,因为我只是在其中抛​​出随机符号,我只是使用这些语法作为示例来说明我们可以做些什么来修改提案的原始状态

// Decorator style, follows the definition of the type thorugh all
// of it's methods.
<strong i="14">@a</strong>: Lesser, Hasher, Equaler
func Sort(slice []a) { ... }
<strong i="15">@k</strong>: Equaler, Hasher
type HashTable k v struct

// Inline, follows the definition of the type through
// all of it's methods.
func [a: Hasher, Equaler] Sort(slice []a) { ... }
type [k: Hasher, Equaler] HashTable k v struct

// File-scope global style, if k appears as a generic argument
// it's constrained by this that appears at the top of the file underneath
// the imports but before any other code.
<strong i="16">@k</strong>: Equaler, Hasher

再次注意,我实际上并不想真正添加到提案中。 我只是在展示我们可以使用什么样的结构来解决问题,而它们现在看起来有点无关紧要。

那么我们需要回答的问题是:我们是否还能从隐含的泛型参数中获得价值? 该提案的主要目的是保持语言简洁的 Go-like 感觉,保持简单,通过消除过多的标记来保持足够低的噪音。 在很多不需要约束的情况下,例如一个map函数或者一个Result类型的定义,是不是看起来不错,感觉像Go,有用吗? 假设约束也以某种形式可用。

func map(slice []a, mapper func(a) b) {
  for i := range slice {
    slice[i] = mapper(slice[i])
  }
}

type Result a b struct {
  Ok  a
  Err b
}

@aarondl我将尝试解释一下。 您需要类型约束的原因是因为这是您可以在类型上调用函数或方法的唯一方式。 考虑不受约束的类型a这可以是什么类型,它可以是字符串或 Int 或任何东西。 所以我们不能在上面调用任何函数或方法,因为我们不知道类型。 我们可以使用类型切换和运行时反射来获取类型,然后在其上调用一些函数或方法,但这是我们希望通过泛型避免的事情。 当您限制类型时,例如a是 Animal 我们可以调用为a上的动物定义的任何方法。

在您的示例中,是的,您可以传入一个映射器函数,但这将导致函数接受很多参数,并且基本上就像一种没有接口的语言,只是一等函数。 要传递您将要在类型a上使用的每个函数,将在任何实际程序中获得非常长的函数列表,特别是如果您主要编写用于依赖注入的通用代码,您想要这样做最小化耦合。

例如,如果调用 map 的函数也是泛型的怎么办? 如果调用它的函数是通用的等等。如果我们还不知道a的类型,我们如何定义映射器?

func m(slice []a) []b {
   mapper := func(x a) b {...}
   return map(slice, mapper)
}

尝试定义mapper x上调用哪些函数?

@keean我了解约束的目的和功能。 我根本不认为它们像通用容器结构(可以说不是通用容器)和通用切片这样简单的东西那么高,因此甚至没有将它们包含在原始提案中。

我仍然主要相信接口是解决问题的正确答案,例如您正在谈论的问题,您在哪里进行依赖注入,这似乎根本不是泛型的正确位置,但我该说谁。 在我看来,他们的职责之间的重叠相当大,因此为什么@Merovius和我必须讨论我们是否可以没有他们生活,他几乎让我相信他们在某些用例中会有用,因此我探索了我们可以做些什么来将该功能添加到我最初提出的提案中。

至于您的示例,您不能在 x 上调用任何函数。 但是您仍然可以像对它自己非常有用的任何其他切片一样对切片进行操作。 也不确定 func 中的 func 是什么......也许你打算分配给一个 var?

@aarondl
谢谢,我修正了语法,但我认为意思仍然很清楚。

我上面给出的示例使用参数多态性和接口来实现某种程度的泛型编程,但是缺乏多分派总是会给可实现的通用性水平设置一个上限。 因此,Go 似乎不会提供我正在寻找的语言中的功能,这并不意味着我不能将 Go 用于某些任务,事实上我已经是并且它运行良好,即使我有剪切和粘贴实际上只需要一个定义的代码。 我只是希望将来如果该代码需要更改,开发人员可以找到它的所有粘贴实例。

然后,考虑到它会增加复杂性,我对在没有对语言进行如此大的改变的情况下可能的有限通用性是否是一个好主意有两种看法。 也许 Go 最好保持简单,人们可以添加诸如预处理之类的宏或其他编译到 Go 的语言来提供这些功能? 另一方面,添加参数多态性将是一个很好的第一步。 允许限制这些类型参数将是一个很好的下一步。 然后您可以将关联的类型参数添加到接口,并且您将拥有一些相当通用的东西,但这可能是您在没有多次调度的情况下所能获得的。 通过拆分成单独的较小功能,我想您会增加让它们被接受的机会?

@基恩
多分派有必要吗? 很少有语言本身支持它。 甚至 C++ 也不支持它。 C# 有点通过dynamic支持它,但我从未在实践中使用过它,而且通常关键字在实际代码中非常罕见。 我记得的示例处理诸如 JSON 解析之类的事情,而不是编写泛型。

多分派有必要吗?

恕我直言,我认为@keean谈到了类型类/接口提供的静态多重调度。
这甚至在 C++ 中通过方法重载提供(我不知道 C#)

您的意思是动态多重分派,这在没有联合类型的静态语言中非常麻烦。 动态语言通过省略静态类型检查(动态语言的部分类型推断,与 C# 的“动态”类型相同)来规避这个问题。

可以将类型提供为“只是”一个参数吗?

func Append(t, t2 type, arr []t, value t2) []t {
    v := t(value) // conversion
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)

@Inuart写道:

可以将类型提供为“只是”一个参数吗?

值得怀疑的是,这在多大程度上是可能的或希望的

如果支持通用约束,则可以实现您想要的:

func Append(arr []t, value s) []t  requires Convertible<s,t>{
    v := t(value) // conversion
    return append(arr, v)
}

var arr []int64
v := 0.5

arr = Append(arr, v)

这也应该有约束条件:

func convert(value s) t requires Convertible<s,t>{
    return t(value);
}

f:float64:=2.0

i:int64=convert(f)

值得一提的是,我们的 Genus 语言确实支持多分派。 约束模型可以提供多个被分派到的实现。

我知道编译时安全需要Convertible<s,t>表示法,但可能会降级为运行时检查

func Append(t, t2 type, arr []t, value t2) []t {
    v, ok := t(value) // conversion
    if !ok {
        panic(...) // or return an err
    }
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)

但这看起来更像是reflect的语法糖。

@Inuart的重点是编译器可以在编译时检查类型实现了类型类,因此运行时检查是不必要的。 好处是更好的性能(所谓的零成本抽象)。 如果它是运行时检查,您也可以使用reflect

@creker

多分派有必要吗?

我太在意这件事了。 一方面,multi-dispatch(带有多参数类型类)不适用于存在,即“Go”所谓的“接口值”。

type Equals<T> interface {eq(right T) bool}
(left I) eq(right I) bool {return left == right}
(left I) eq(right F) bool {return false}
(left F) eq(right I) bool {return false}
(left F) eq(right F) bool {return left == right}

func main() {
    x := []Equals<?>{I{2}, F{4.0}, I{2}, F{4.0}}
}

我们无法定义Equals的切片,因为我们无法指示右手参数来自同一个集合。 我们甚至不能在 Haskell 中这样做:

data Equals = forall a . IEquals a a => Equals a

这不好,因为它只允许一个类型与自身进行比较

data Equals = forall a b . IEquals a b => Equals a

这不好,因为我们无法将b限制为与a相同的集合中的另一个存在(如果a甚至在集合中)。

但是,它确实可以很容易地使用新类型进行扩展:

(left K) eq(right I) bool {return false}
(left K) eq(right F) bool {return false}
(left I) eq(right K) bool {return false}
(left F) eq(right K) bool {return false}
(left K) eq(right K) bool {return left == right}

如果使用默认实例或专业化,这将更加简洁。

另一方面,我们可以在现在可以使用的“Go”中重写它:

package main

type I struct {v int}
type F struct {v float32}

type EqualsInt interface {eqInt(left I) bool}
func (right I) eqInt (left I) bool {return left == right}
func (right F) eqInt (left I) bool {return false}

type EqualsFloat interface {eqFloat(left F) bool}
func (right I) eqFloat (left F) bool {return false}
func (right F) eqFloat (left F) bool {return left == right}

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

type EqualsLeft interface {eq(right EqualsRight) bool}
func (left I) eq (right EqualsRight) bool {return right.eqInt(left)}
func (left F) eq (right EqualsRight) bool {return right.eqFloat(left)}

type Equals interface {
    EqualsLeft
    EqualsRight
}

func main() {
    x := []Equals{I{2}, F{4.0}, I{2}, F{4.0}}
    println(x[0].eq(x[1]))
    println(x[1].eq(x[0]))
    println(x[0].eq(x[2]))
    println(x[1].eq(x[3]))
}

这与存在(接口值)很好地工作,但是它更复杂,更难看到发生了什么以及它是如何工作的,并且它有很大的限制,我们需要每种类型一个接口,我们需要硬编码可接受的像这样的右侧类型:

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

这意味着我们必须修改库源以添加新类型,因为接口EqualsRight不可扩展。

因此,如果没有多参数接口,我们就无法定义可扩展的泛型运算符,例如相等。 使用多参数接口存在(接口值)变得有问题。

我对很多建议的语法(语法?) Blah[E]的主要问题是底层类型没有显示任何有关包含泛型的信息。

例如:

type Comparer[C] interface {
    Compare(other C) bool
}
// or
type Comparer c interface {
    Compare(other c) bool
}
...

这意味着我们要声明一个新类型,它会在底层类型上添加更多信息。 type声明的重点不是基于另一种类型定义名称吗?

我会提出一种更符合以下要求的语法

type Comparer interface[C] {
    Compare(other C) bool
}

这意味着真正的Comparer只是基于interface[C] { ... }的类型,而interface[C] { ... }当然是与interface { ... }不同的类型。 如果需要,这允许您使用通用接口而不命名它(普通接口允许这样做)。 我认为这个解决方案更直观,并且适用于 Go 的类型系统,但如果我错了,请纠正我。

注意:声明泛型类型只允许在具有以下语法的接口、结构和函数上:
interface[G] { ... }
struct[G] { ... }
func[G] (vars...) { ... }

然后“实现”泛型将具有以下语法:
interface[G] { ... }[string]
struct[G] { ... }[string]
func[G] (vars...) { ... }[int](args...)

并通过一些示例使其更加清晰:

接口

package add

type Adder interface[E] {
    // Adds the element and returns the size
    Add(elem E) int
}

// Adds the integer 5 to any implementation of Adder[int].
func AddFiveTo(a Adder[int]) int {
    return a.Add(5)
}

结构

package heap

type List struct[T] {
    slice []T
}

func (l *List) Add(elem T) { // T is a type defined by the receiver
    l.slice = append(l.slice, elem)
}

职能

func[A] AddManyTo(a Adder[A], many ...A) {
    for _, each := range a {
        a.Add(each)
    }
}

这是对 Go2 合同草案的回应,我将使用它的语法,但我将其发布在这里,因为它适用于任何参数多态性提案。

不应允许嵌入类型参数。

考虑

type X(type T C) struct {
  R // A regular type with method Foo()
  T // Some type parameter
}
// X defines some methods other than Foo(),
// some of which invoke Foo.

对于一些任意类型R和一些不包含Foo()的任意合约C #$ 。

T将具有C所需的所有选择器,但T的特定实例化也可能具有任意其他选择器,包括Foo

假设Bar是一个结构,可以在C下接受,它有一个名为Foo的字段。

X(Bar)可能是非法实例化。 如果没有一种方法来指定类型没有选择器的合同,这将必须是一个推断的属性。

X(Bar)的方法可以继续将Foo的引用解析为X(Bar).R.Foo 。 这使得编写泛型类型成为可能,但可能会使不熟悉解析规则的挑剔的读者感到困惑。 在X的方法之外,选择器将保持模棱两可,因此,虽然interface { Foo() }不依赖于X X一些实例化会不满足。

不允许嵌入类型参数更简单。

(但是,如果允许这样做,则字段名称将为T ,原因与定义为type S = io.Reader的嵌入式S的字段名称为S的原因相同Reader也是因为实例化T的类型根本不需要有名称。)

@jimmyfrasche我认为具有泛型类型的嵌入式字段非常有用,即使在某些地方可能有些尴尬,也可以允许它们使用。 我的建议是在所有泛型代码中假设嵌入式类型每个可能的级别定义了所有可能的字段和方法,以便在泛型代码中删除所有非泛型类型的嵌入式方法和字段。

所以给出:

type R struct(type T) {
    io.Reader
    T
}

如果不通过 Reader 间接调用,R 上的方法将无法在 R 上调用 Read。 例如:

func (r R) Do() {
     r.Read(buf)     // Illegal
     r.Reader.Read(buf)  // ok
}

我能看到的唯一缺点是动态类型可能包含比静态类型更多的成员。 例如:

func (r R) Do() {
    var x interface{} = r
    x.(io.Reader)    // Succeeds
}

@rogpeppe

我能看到的唯一缺点是动态类型可能包含比静态类型更多的成员。

直接使用类型参数就是这种情况,所以我认为参数类型也应该没问题。 我认为@jimmyfrasche提出的问题的解决方案可能是将参数化类型的所需方法集放入合同中。

contract C(t T) {
  interface { Foo() } (X(T){})
  // ...
}

type X(type T C) struct {
  R // A regular type with method Foo()
  T // Some type parameter
}
// X defines some methods other than Foo(),
// some of which invoke Foo.

这将允许直接在X Foo调用 Foo。 当然,这会违反“合同中没有本地名称”的规则......

@stevenblenkinsop嗯,如果尴尬的话,可以不参考X来做到这一点

contract C(t T) {
  struct{ R; T }{}.Foo
}

C仍然绑定到X的实现,尽管有点松散。

如果你不这样做,你写

func (x X(T)) Fooer() interface { Foo() } {
  return x
}

它编译吗? 它不符合@rogpeppe的规则,当您不在合同中作出保证时,似乎也需要采用它。 但是,它是否仅在您嵌入没有足够合同的类型参数或所有嵌入时才适用?

禁止它会更容易。

在 Go2 草案公布之前,我就开始着手这个提案。

当我看到公告时,我准备愉快地废弃我的,但我仍然对草案的复杂性感到不安,所以我完成了我的。 它不那么强大但更简单。 如果不出意外,它可能有一些值得窃取的部分。

它扩展了@ianlancetaylor早期提案的语法,因为那是我开始时可用的。 这不是根本的。 它可以用(type T等语法或等效的东西代替。 我只需要一些语法作为语义的符号。

它位于此处: https ://gist.github.com/jimmyfrasche/656f3f47f2496e6b49e041cd8ac716e4

规则必须是任何从比嵌入类型参数更深的方法提升的方法都不能被调用,除非(1)类型参数的身份已知或(2)方法被断言为可在外部调用类型由约束类型参数的协定。 编译器还可以确定提升方法在外部类型O中必须具有的深度的上限和下限,并使用它们来确定该方法是否可在嵌入O的类型上调用,即是否有可能与其他推广的方法发生冲突。 类似的东西也适用于任何断言具有可调用方法的类型参数,其中类型参数中方法的深度范围为 [0, inf)。

嵌入类型参数似乎太有用了,无法完全禁止它。 一方面,它允许透明组合,而嵌入接口的模式不允许这样做。

我还发现了定义合同的潜在用途。 如果您希望能够接受T类型的值(可能是指针类型),该值可能在*T上定义了方法,并且您希望能够将该值放入一个接口,你不一定要把T放在接口中,因为方法可能在*T上,你不一定要把*T放在接口中,因为T本身可能是指针类型(因此*T可能有一个空方法集)。 但是,如果你有一个像

type Wrapper(type T) { T }

如果你的合同说它满足接口,你可以在任何情况下在接口中放置一个*Wrapper(T)

你不能就这样吗

type Interface interface {
  SomeMethod(int) error
}

contract MightBeAPointer(t T) {
  Interface(t)
}

func Example(type T MightBeAPointer)(v T) {
  var i Interface = v
  // ...
}

我正在尝试处理有人打电话的情况

type S struct{}
func (s *S) SomeMethod(int) error { ... }
...
var s S
Example(S)(s)

这不起作用,因为S不能转换为Interface ,只有*S可以。

显然,答案可能是“不要那样做”。 但是,合同提案描述的合同如下:

contract Contract(t T) {
    var _ error = t.SomeMethod(int(0))
}

由于自动寻址, S将满足此合同, *S也是如此。 我要解决的是合约中方法调用和接口转换之间的能力差距。

无论如何,这有点切题,显示了嵌入类型参数的一种潜在用途。

重新嵌入,我认为“可以嵌入到结构中”是合同在允许的情况下必须捕获的另一个限制。

考虑:

contract Embeddable(type X, Y) {
    type S struct {
        X
        Y
    }
}

type Embedded(type First, Second Embeddable) struct {
        First
        Second
}

// Error: First and Second both provide method Read.
// That must be diagnosed to the Embeddable contract, not the definition of Embedded itself.
type Boom = Embedded(*bytes.Buffer, *strings.Reader)

@bcmills允许使用不明确的选择器嵌入类型,所以我不确定应该如何解释该合同。

无论如何,如果您只嵌入已知类型,那很好。 如果您只嵌入类型参数,那很好。 唯一奇怪的情况是当您嵌入一个或多个已知类型和一个或多个类型参数时,然后仅当已知类型的选择器和类型参数不不相交时

@bcmills允许使用不明确的选择器嵌入类型,所以我不确定应该如何解释该合同。

嗯,好点子。 我又错过了一个触发错误的约束条件。¹

contract Embeddable(type X, Y) {
    type S struct {
        X
        Y
    }
    var _ io.Reader = S{}
}

¹ https://play.golang.org/p/3wSg5aRjcQc

这需要XY之一,但不是两者都是io.Reader 。 有趣的是,合约系统的表现力足以允许这样做。 我很高兴我不必为这样的野兽弄清楚类型推断规则。

但这并不是真正的问题。

这是你做的时候

type S (type T C) struct {
  io.Reader
  T
}
func (s *S(T)) X() io.Reader {
  return s
}

那应该无法编译,因为T可能有一个Read选择器,除非C

struct{ io.Reader; T }.Read

但是,当C不确保选择器集不相交并且S不引用选择器时,规则是什么? 每个实例化S是否有可能满足除了创建模棱两可选择器的类型之外的接口?

除了创建模糊选择器的类型之外,每个实例化S是否有可能满足接口?

是的,似乎是这样。 我想知道这是否意味着更深层次的……🤔

我还没有能够构建出任何令人讨厌的东西,但是不对称是非常令人不快的,让我感到不安:

type I interface { /* ... */ }
a := G(A) // ok, A satisfies contract
var _ I = a // ok, no selector overlap
b := G(B) // ok, B satisfies contract
var _ = b // error, selector overlap

我担心G0(B)使用G1(B)使用 . . . 使用Gn(B)Gn是导致错误的原因。 . . .

FTR,您无需通过模棱两可的选择器来触发嵌入的类型错误。

// Error: Duplicate field name Reader
type Boom = Embedded(*bytes.Reader, *strings.Reader)

您假设嵌入字段名称基于参数类型,而它更有可能是嵌入类型参数的名称。 这就像当您嵌入一个类型别名并且字段名称是别名而不是它所别名的类型的名称时。

这实际上在参数化类型部分的设计草案中指定:

当参数化类型是结构体,并且类型参数作为字段嵌入到结构体中时,字段的名称是类型参数的名称,而不是类型参数的名称。

type Lockable(type T) struct {
    T
    mu sync.Mutex
}

func (l *Lockable(T)) Get() T {
    l.mu.Lock()
    defer l.mu.Unlock()
    return l.T
}

(注意:如果你在方法声明中写了 Lockable(X),这效果很差:方法应该返回 lT 还是 lX?也许我们应该简单地禁止在结构中嵌入类型参数。)

我只是坐在场边观望。 但也有点担心。

有一件事我并不尴尬,那就是 90% 的讨论都超出了我的想象。

在不知道泛型或参数多态性是什么的情况下,通过编写软件谋生似乎 20 年并没有阻止我完成这项工作。

遗憾的是,我大约一年前才花时间学习围棋。 我错误地假设这是一个陡峭的学习曲线,并且需要很长时间才能变得富有成效。

我大错特错了。

我能够学习足够多的 Go 来构建一个微服务,该微服务在不到一个周末的时间内完全破坏了我遇到性能问题的 node.js 服务。

具有讽刺意味的是,我只是在玩。 我对用围棋征服世界并不是特别认真。

然而,在几个小时内,我发现自己从懒散的失败姿势中坐了起来,就像我在座位边上看一部动作惊悚片一样。 我正在构建的 API 很快就完成了。 我意识到这确实是一门值得我投入宝贵时间的语言,因为它的设计显然非常务实。

这就是我喜欢 Go 的地方。 速度非常快.....学习了。 我们都知道它的性能。 但是它的学习速度是我这些年来学习的其他 8 种语言所无法比拟的。

从那时起,我一直在歌颂 Go,并让另外 4 个开发者爱上了它。 我只是和他们坐了几个小时,然后建造一些东西。 结果不言自明。

简单,快速学习。 这些是该语言真正的杀手级功能。

需要数月艰苦学习的编程语言通常无法留住他们想要吸引的开发人员。 我们有工作要做,而雇主希望每天都能看到进步(感谢敏捷,感谢它)

所以,我希望 Go 团队可以考虑两件事:

1) 我们要解决什么日常问题?

我似乎找不到一个真实世界的例子,一个可以通过泛型解决的显示停止器,或者它们将被调用的任何东西。

有问题的日常任务的食谱样式示例,以及如何通过这些语言更改建议改进它们的示例。

2) 保持简单,就像 Go 的所有其他强大功能一样

这里有一些非常聪明的评论。 但我敢肯定,大多数像我这样每天都使用 Go 进行一般编程的开发人员,对他们现在的方式非常满意和高效。

也许是启用此类高级功能的编译器参数? '--铁杆'

如果我们对编译器性能产生负面影响,我会非常难过。 只是说'n

这就是我喜欢 Go 的地方。 速度非常快.....学习了。 我们都知道它的性能。 但是它的学习速度是我这些年来学习的其他 8 种语言所无法比拟的。

我完全同意。 在完全编译的语言中,功能与简单性的结合是完全独一无二的。 我绝对不希望 Go 失去这一点,尽管我想要泛型,但我认为它们不值得为此付出代价。 不过,我认为没有必要失去它。

我似乎找不到一个真实世界的例子,一个可以通过泛型解决的显示停止器,或者它们将被调用的任何东西。

对于泛型,我有两个主要的主要用例:类型安全的样板消除了复杂的数据结构,例如二叉树、集合和sync.Map ,以及编写基于操作的 _compile-time_ 类型安全函数的能力纯粹基于他们的论点的功能,而不是他们在内存中的布局。 有一些更好的事情我不介意能够做,但如果不可能在不完全破坏语言简单性的情况下添加对它们的支持,我不介意_不_能够做它们。

老实说,该语言中已经有一些非常容易滥用的功能。 我认为,它们_不_经常被滥用的主要原因是编写“惯用”代码的 Go 文化,以及在大多数情况下提供干净、易于找到此类代码示例的标准库。 在标准库中很好地使用泛型绝对应该是实现它们时的优先事项。

@camstuart

我似乎找不到一个真实世界的例子,一个可以通过泛型解决的显示停止器,或者它们将被调用的任何东西。

泛型是这样您不必自己编写代码。 所以你再也不需要自己实现另一个链表、二叉树、双端队列或优先级队列。 您永远不需要实现排序算法、分区算法或旋转算法等。数据结构成为标准集合的组成部分(例如列表映射),处理成为标准算法的组成部分(我需要对数据进行排序、分区、并旋转)。 如果您可以重用这些组件,错误率就会下降,因为每次您重新实现优先级队列或分区算法时,都有可能出错并引入错误。

泛型意味着您编写的代码更少,并且重复使用更多。 它们意味着标准的、维护良好的库函数和抽象数据类型可以在更多情况下使用,因此您不必自己编写。

更好的是,从技术上讲,所有这些现在都可以在 Go 中完成,但只会在编译时类型安全几乎完全丧失的情况下_和_以及一些可能主要的运行时开销。 泛型可以让你做到这一点而没有任何这些缺点。

通用功能实现:

/*

* "generic" is a KIND of types, just like "struct", "map", "interface", etc...
* "T" is a generic type (a type of kind generic).
* var t = T{int} is a value of type T, values of generic types looks like a "normal" type

*/

type T generic {
    int
    float64
    string
}

func Sum(a, b T{}) T{} {
    return a + b
}

函数调用者:

Sum(1, 1) // 2
// same as:
Sum(T{int}(1), T{int}(1)) // 2

通用结构实现:

type ItemT generic {
    interface{}
}

type List struct {
    l []ItemT{}
}

func NewList(t ItemT) *List {
    l := make([]t)
    return &List{l}
}

func (p *List) Push(item ItemT{}) {
    p.l = append(p.l, item)
}

呼叫者:

list := NewList(ItemT{int})
list.Push(42)

作为一个刚刚学习 Swift 并且不喜欢它,但在 Go、C、Java 等其他语言方面有丰富经验的人; 我真的相信泛型(或模板,或任何你想称之为的)不是添加到 Go 语言中的好东西。

也许我只是对当前版本的 Go 更有经验,但对我来说,这感觉像是对 C++ 的回归,因为更难理解其他人编写的代码。 用于类型的经典 T 占位符使得理解函数试图做什么变得非常困难。

我知道这是一个受欢迎的功能请求,所以如果它落地我可以处理它,但我想加我的 2 美分(意见)。

@jlubawy
你知道我永远不必实现链表或快速排序算法的另一种方式吗? 正如 Alexander Stepanov 指出的那样,大多数程序员无法正确定义“min”和“max”函数,所以我们希望在没有大量调试时间的情况下正确实现更复杂的算法。 我更愿意将这些算法的标准版本从库中提取出来,然后应用到我拥有的类型上。 有什么选择?

@jlubawy

或模板,或任何你想叫它的东西

一切都取决于实施。 如果我们谈论的是 C++ 模板,那么是的,它们通常很难理解。 即使写出来也很困难。 另一方面,如果我们采用 C# 泛型,那就完全是另一回事了。 这个概念本身在这里不是问题。

如果你不知道,Go 团队已经公布了 Go 2.0 的草案:
https://golang.org/s/go2designs

Go 2.0(合同)中有一个泛型设计草案。 您可能想查看并在他们的Wiki上提供反馈

这是相关部分:

泛型

看完草稿后,我问:

为什么

T:可添加

意思是“实现合同可添加的类型 T”? 为什么要添加一个新的
当我们已经有接口时的概念? 接口分配是
签入构建时间,所以我们已经有办法不需要任何
这里有额外的概念。 我们可以用这个词来表示:任何
类型 T 实现接口 Addable。 此外,T:_ 或 T:Any
(作为任何特殊关键字或 interface{} 的内置别名)都可以
诀窍。

只是我不知道为什么要重新实现大多数类似的东西。 没有
感觉和将是多余的(因为多余的是新的错误处理
恐慌的处理)。

2018-09-14 6:15 GMT-05:00 Koala Yeung [email protected] :

如果你不知道,Go 团队已经公布了 Go 2.0 的草案:
https://golang.org/s/go2designs

Go 2.0(合同)中有一个泛型设计草案。 你可能想要
查看并提供反馈
https://github.com/golang/go/wiki/Go2GenericsFeedback在他们的 Wiki 上
https://github.com/golang/go/wiki/Go2GenericsFeedback

这是相关部分:

泛型


您收到此消息是因为您发表了评论。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/15292#issuecomment-421326634或静音
线程
https://github.com/notifications/unsubscribe-auth/AlhWhS8xmN5Y85_aUKT5VnutoOKUAaLLks5ua4_agaJpZM4IG-xv
.

--
这是在 TripleMint 中使用的邮件签名测试

编辑:“[...] 如果您不需要特殊要求,就可以解决问题
类型参数”。

2018-09-17 11:10 GMT-05:00 Luis Masuelli [email protected] :

看完草稿后,我问:

为什么

T:可添加

意思是“实现合同可添加的类型 T”? 为什么要添加一个新的
当我们已经有接口时的概念? 接口分配是
签入构建时间,所以我们已经有办法不需要任何
这里有额外的概念。 我们可以用这个词来表示:任何
类型 T 实现接口 Addable。 此外,T:_ 或 T:Any
(作为任何特殊关键字或 interface{} 的内置别名)都可以
诀窍。

只是我不知道为什么要重新实现大多数类似的东西。 没有
感觉和将是多余的(因为多余的是新的错误处理
恐慌的处理)。

2018-09-14 6:15 GMT-05:00 Koala Yeung [email protected] :

如果你不知道,Go 团队已经公布了 Go 2.0 的草案:
https://golang.org/s/go2designs

Go 2.0(合同)中有一个泛型设计草案。 您可以
想看看并提供反馈
https://github.com/golang/go/wiki/Go2GenericsFeedback在他们的 Wiki 上
https://github.com/golang/go/wiki/Go2GenericsFeedback

这是相关部分:

泛型


您收到此消息是因为您发表了评论。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/15292#issuecomment-421326634或静音
线程
https://github.com/notifications/unsubscribe-auth/AlhWhS8xmN5Y85_aUKT5VnutoOKUAaLLks5ua4_agaJpZM4IG-xv
.

--
这是在 TripleMint 中使用的邮件签名测试

--
这是在 TripleMint 中使用的邮件签名测试

@luismasuelli-jobsity 如果我正确阅读了 Go 中通用实现的历史,那么引入合同的原因似乎是因为他们不希望在接口中重载运算符。

一个最终被拒绝的早期提议使用接口来约束参数多态性,但似乎已被拒绝,因为您不能在此类函数中使用像“+”这样的常用运算符,因为它在接口中不可定义。 合同允许您编写t == tt + t以便您可以指示类型必须支持相等或加法等。

编辑:Go 也不支持多个类型参数接口,因此在某种程度上 Go 将 typeclass 分成了两个独立的东西,将函数类型参数相互关联的合同,以及提供方法的接口。 它失去的是基于多种类型选择类型类实现的能力。 如果您只需要使用接口或合同,它可以说更简单,但如果您需要同时使用两者,则更复杂。

为什么T:Addable的意思是“实现合约 Addable 的类型 T”?

这实际上不是它的意思。 对于一种类型的参数,它看起来就是这样。 在草案的其他地方,它评论说每个函数只能有一个合约,这就是主要区别所在。合约实际上是关于函数类型的声明,而不仅仅是独立的类型。 例如,如果您有

func Example(type K, V someContract)(k K, v V) V

你可以做类似的事情

contract someContract(k K, v V) {
  k.someMethod(v)
}

这极大地简化了多种类型的协调,而无需在函数签名中重复指定类型。 请记住,他们试图避免“奇怪地重复的通用模式”。 例如,具有用于约束类型的参数化接口的相同函数将类似于

type someMethoder(V) interface {
  someMethod(V)
}

func Example(type K: someMethoder(V), V)(k K, v V) V

这有点尴尬。 但是,如果需要,合同语法允许您仍然执行此操作,因为如果合同的“参数”与函数的类型参数数量相同,则编译器会自动填充合同的“参数”。 但是,如果您愿意,可以手动指定它们,这意味着如果您真的愿意,您可以_ 执行func Example(type K, V someContract(K, V))(k K, v V) V ,尽管在这种情况下它并不是特别有用。

更清楚地表明合约是关于整个函数而不是单个参数的一种方法是简单地根据名称将它们关联起来。 例如,

contract Example(k K, v V) {
  k.someMethod(v)
}

func Example(type K, V)(k K, v V) V

将与上述相同。 然而,缺点是合约不能重用,并且您失去了手动指定合约参数的能力。

编辑:为了进一步说明他们为什么要解决奇怪的重复模式,请考虑他们一直提到的最短路径问题。 使用参数化接口,定义最终看起来像

type E(Node) interface {
  Nodes() []Node
}

type N(Edge) interface {
  Edges() (from, to Edge)
}

type Graph(type Node: N(Edge), Edge: E(Node)) struct { ... }
func New(type Node: N(Edge), Edge: E(Node))(nodes []Node) *Graph(Node, Edge) { ... }
func (*Graph(Node, Edge)) ShortestPath(from, to Node) []Edge { ... }

就个人而言,我更喜欢为函数指定合约的方式。 我并不_太_热衷于将“正常”函数体作为实际的合同规范,但我认为可以通过引入某种类似 gofmt 的简化器来解决很多潜在问题,该简化器可以为您自动简化合同,消除无关的部分。 然后你_可以_只是将一个函数体复制到其中,简化它,然后从那里修改它。 不过,不幸的是,我不确定这将如何实现。

不过,有些事情仍然很难指定,而且合同和接口之间明显的重叠似乎仍然有点奇怪。

我发现“CRTP”版本更清晰、更明确且更易于使用(无需创建仅存在于定义一组变量上的现有合约之间的关系的合约)。 诚然,这可能只是多年来对这个想法的熟悉。

澄清。 通过草案设计,契约可以应用于功能和类型

"""
如果您只需要使用接口或合同,它可以说更简单,但如果您需要同时使用两者,则更复杂。
"""

只要它们允许您在合同中引用一个或多个接口(而不仅仅是运算符和函数,因此允许 DRY),这个问题(以及我的主张)就会得到解决。 我有可能误读或没有完全阅读合同内容,也有可能支持上述功能而我没有注意到。 如果不是,它应该是。

你不能做以下吗?

contract Example(t T, v V) {
  t.(interface{
    SomeMethod() V
  })
}

您不能使用在其他地方声明的接口,因为您不能引用与声明合同相同的包中的标识符,但您可以这样做。 或者他们可以取消该限制; 这似乎有点武断。

@DeedleFake不,因为任何接口类型都可以进行类型断言(然后在运行时可能会出现恐慌,但不会执行合约)。 但是您可以改用分配。

t.(someInterface)也意味着它必须是一个接口

好点子。 哎呀。

我看到的这样的例子越多,“从函数体中弄清楚”似乎就越容易出错。

在很多情况下,人们会感到困惑,不同操作的语法相同,不同构造的含义阴影等等,但是一个工具可以将其简化为正常形式。 但是这样一个工具的输出变成了事实上的子语言,用于表达我们必须死记硬背的类型约束,当有人偏离并手工编写合同时,这更加令人惊讶。

我还要注意

contract I(t T) {
  var i interface { Foo() }
  i = t
  t.(interface{})
}

表示T必须是至少具有Foo()的接口,但它也可以具有任何其他数量的附加方法。

T必须是至少具有Foo()的接口,但它也可以具有任何其他数量的附加方法

不过,这是个问题吗? 您是否通常不希望限制事物以使它们允许特定功能但您不关心其他功能? 否则像这样的合同

contract Example(t T) {
  t + t
}

例如,不允许减法。 但是从我正在实现的任何角度来看,我不在乎一个类型是否允许减法。 如果我限制它能够执行减法,那么人们将无法任意将任何可以执行的操作传递给Sum()函数或其他东西。 这似乎是武断的限制。

不,这根本不是问题。 这只是一个不直观的(对我来说)属性,但也许这是由于咖啡不足。

公平地说,当前的合约声明需要有更好的编译器消息才能使用。 有效合同的规则应该是严格的。

你好
大约 1/2 年前,我提出了一个关于泛型约束的建议。
现在我制作了一个版本 2 。 主要变化是:

  • 该语法已适应 go-team提出的语法。
  • 字段约束已被省略,这允许相当多的简化。
  • 被认为并非绝对必要的段落已被删除。

我最近想到了一个关于类型标识的有趣问题(但在设计的这个阶段可能比适当的更详细?):

func Foo() interface{} {
    type S struct {}
    return S{}
}

func Bar(type T)() interface{} {
    type S struct {}
    return S{}
}

func Baz(type T)() interface{} {
    type S struct{t T}
    return S{}
}

func main() {
    fmt.Println(Foo() == Foo()) // 1
    fmt.Println(Bar(int)() == Bar(string)()) // 2
    fmt.Println(Baz(int)() == Baz(string)()) // 3
}
  1. 打印true ,因为返回值的类型源自相同的类型声明。
  2. 印刷…?
  3. 打印false ,我假设。

即问题是,当泛型函数中声明的两种类型相同时,何时不同。 我不认为这在 ~spec~ 设计中有所描述? 至少我现在找不到它:)

@merovius我认为中间情况应该是:

fmt.Println(Bar(int)() == Bar(int)()) // 2

这是一个有趣的案例,它取决于类型是“生成的”还是“应用的”。 实际上存在采用不同方法的 ML 变体。 应用类型将泛型视为类型函数,因此 f(int) == f(int)。 生成类型将泛型视为类型模板,每次使用它时都会创建一个新的唯一“实例”类型,因此 t<int> != t<int>。 这必须在整个类型系统级别进行处理,因为它对统一、推理和健全性有微妙的影响。 有关此类问题的更多详细信息和示例,我建议阅读 Andreas Rossberg 的“F-ing modules”论文: https ://people.mpi-sws.org/~rossberg/f-ing/ 尽管该论文正在谈论 ML“ functors”这是因为 ML 将其类型系统分为两个级别,并且 functor 是 ML 等价的泛型,并且仅在模块级别可用。

@keean你假设错了。

@merovius是的,我的错误,我看到问题是因为未使用类型参数(幻像类型)。

对于生成类型,每个实例化都会导致“S”的唯一类型不同,因此即使不使用参数,它们也不会相等。

对于应用类型,来自每个实例化的“S”将是相同的类型,因此它们将是相等的。

如果案例 2 的结果基于编译器优化而改变,那将是很奇怪的。 听起来像UB。

这是 2018 年的人,我不敢相信我实际上必须像 1982 年那样输入:

func min(x, y int) int {
如果 x < y {
返回 x
}
返回 y
}

func max(x, y int) int {
如果 x > y {
返回 x
}
返回 y
}

我的意思是,说真的,伙计们 MIN(INT,INT) INT,这不是语言吗?
我生气。

@dataf3l如果您希望它们在预购时按预期工作,那么:

func min(x, y int) int {
   if x <= y {
      return x
   }
   return y
}

因此,对 (min(x, y), max(x, y)) 始终是不同的,并且是 (x, y) 或 (y, x),因此这是两个元素的稳定排序。

因此,这些应该在语言或库中的另一个原因是人们大多误会它们:-)

我想到了 < vs <=,对于整数,我不确定我是否完全看到了区别。
可能我只是笨...

我不确定我是否完全看到了差异。

在这种情况下没有。

@cznic在这种情况下是真的,因为它们是整数,但是由于线程是关于泛型的,我认为库注释是关于具有 min 和 max 的通用定义,因此用户不必自己声明它们。 重新阅读 OP 我可以看到他们只想要整数的简单最小值和最大值,所以我的错,但他们在关于泛型的线程中要求简单的集成函数的话题不在 :-)

泛型是该语言的重要补充,尤其是在缺乏内置数据结构的情况下。 到目前为止,我对 Go 的体验是,它是一门很棒且易于学习的语言。 但它有一个巨大的权衡,那就是你必须一遍又一遍地编写相同的东西。

也许我遗漏了一些东西,但这似乎是语言中相当大的缺陷。 归根结底,内置数据结构很少,每次创建数据结构时,我们都必须复制和粘贴代码以支持每个T

除了将我的观察作为“用户”在这里发布之外,我不确定如何做出贡献。 我不是一个经验丰富的程序员,无法为设计或实现做出贡献,所以我只能说泛型将大大提高语言的生产力(只要构建时间和工具仍然像现在一样棒)。

@webern谢谢。 请参阅https://go.googlesource.com/proposal/+/master/design/go2draft.md

@ianlancetaylor ,在发布后,一个相当激进/独特的想法突然出现在我的脑海中,我认为就语言和工具而言,它是“轻量级的”。 我还没有完全阅读你的链接,我会的。 但是,如果我想以 MD 格式提交通用编程的想法/建议,我该怎么做?

谢谢。

@webern写下来(大多数人一直在使用降价格式的要点)并在此处更新 wiki https://github.com/golang/go/wiki/Go2GenericsFeedback

许多其他人已经这样做了。

我已经合并(根据最新提示)并上传了我们的前 Gophercon 原型实现的 CL,该解析器(和打印机)实现了合同草案设计。 如果您对尝试语法感兴趣,请查看: https ://golang.org/cl/149638。

玩它:

1) 在最近的 repo 中挑选 CL:
git fetch https://go.googlesource.com/go refs/changes/38/149638/2 && git cherry-pick FETCH_HEAD

2)重建并安装编译器:
去安装 cmd/编译

3)使用编译器:
go tool 编译 foo.go

有关详细信息,请参阅 CL 说明。 享受!

contract Addable(t T) {
    t + t
}

func Sum(type T Addable)(x []T) T {
    var total T
    for _, v := range x {
        total += v
    }
    return total
}

这个仿制药设计, func Sum(type T Addable)(x []T) T ,非常非常非常丑!!!

func Sum(type T Addable)(x []T) T相比,我认为func Sum<T: Addable> (x []T) T更清晰,对来自其他编程语言的程序员没有负担。

你的意思是语法更冗长?
它不是func Sum(T Addable)(x []T) T一定有一些原因。

如果没有type关键字,就无法区分通用函数和返回另一个函数的函数,后者本身正在被调用。

@urandom这只是实例化时的一个问题,我们不需要type关键字,而只需使用模棱两可的 AIUI。

问题是,如果没有type关键字, func Foo(x T) (y T)可能被解析为声明一个采用T并且不返回任何内容的泛型函数,或者一个采用T的非泛型函数T

函数总和(x []T) T

我同意,我更喜欢这些方面的东西。 鉴于泛型所代表的语言范围的扩展,我认为引入这种语法来“引起注意”对泛型函数是合理的。

我还认为这将使代码更容易(阅读:更少的 Lisp-y)为人类读者解析,并减少进一步遇到一些晦涩的解析歧义的机会(参见 C++ 的“最令人烦恼的解析”,以帮助激发大量的谨慎)。

这是 2018 年的人,我不敢相信我实际上必须像 1982 年那样输入:

func min(x, y int) int {
如果 x < y {
返回 x
}
返回 y
}

func max(x, y int) int {
如果 x > y {
返回 x
}
返回 y
}

我的意思是,说真的,伙计们 MIN(INT,INT) INT,这不是语言吗?
我生气。

这是有原因的。
如果你不明白,你可以学习或离开。
你的选择。

我真诚地希望他们做得更好。
但是你的“你可以学习或离开”的态度,并没有为其他人提供一个很好的榜样。 它读起来不必要的磨蚀。 我不认为这就是这个社区对@petar-dambovaliev 的看法。 但是,我不能告诉你做什么,或者如何在网上表现,那不是我的职责。

我知道对泛型有很多强烈的感受,但请记住我们的Gopher 价值观。 请保持对话的尊重和欢迎各方。

@bcmills谢谢你,你让社区变得更美好。

@katzdm同意,该语言已经有很多括号了,这些新东西对我来说真的很模棱两可

定义generics似乎不可避免地引入诸如type's type之类的东西,这使得Go相当复杂。

希望这不是太离题,但function overload的功能对我来说似乎已经足够了。

顺便说一句,我知道有一些关于超载的讨论。

@xgfone同意,该语言已经有这么多括号,使代码不清楚。
func Sum<T: Addable> (x []T) Tfunc Sum<type T Addable> (x []T) T更好更清晰。

为了一致性(使用内置泛型), func Sum[T: Addable] (x []T) T优于func Sum<T: Addable> (x []T) T

我可能会受到以前在其他语言中的工作的影响,但Sum<T: Addable> (x []T) T乍一看似乎更加独特和可读。

我也同意@katzdm的观点,因为它更能引起人们对语言中新事物的关注。 非 Go 开发人员跳入 Go 也很熟悉。

FWIW,Go 有大约 0% 的机会将尖括号用于泛型。 C++ 的语法是不可解析的,因为如果不了解 a、b 和 c 的类型,就无法从泛型调用中分辨出 a<b>c(合法但无意义的一系列比较)。 出于这个原因,其他语言避免对泛型使用尖括号。

func a < b Addable> (...
如果您意识到在func之后您只能拥有函数名(< ,我想您可以。

@carlmjohnson我希望你是对的

f := sum<int>(10)

但是在这里你知道sum是一个合同..

C++ 的语法是不可解析的,因为如果不了解 a、b 和 c 的类型,就无法从泛型调用中分辨出 a<b>c(合法但无意义的一系列比较)。

我认为值得指出的是,虽然 Go 与 C++ 不同,在类型系统中不允许这样做,因为<>运算符在 Go 和<中返回bool <>不能与bool一起使用,它在语法上是合法的,所以这仍然是一个问题。

尖括号的另一个问题是List<List<int>> ,其中>>被标记为右移运算符。

使用[]有什么问题? 在我看来,上述大部分问题都可以通过使用它们来解决:

  • 在语法上,使用上面的示例f := sum[int](10)是明确的,因为它具有与数组或映射访问相同的语法,然后类型系统可以稍后找出它,就像它已经为例如,数组和映射访问之间的区别。 这与<>的情况不同,因为单个<是合法的,导致歧义,但单个[不是。
  • func Example[T](v T) T也是明确的。
  • ]]不是它自己的令牌,因此也避免了这个问题。

设计草案提到了类型声明中的歧义,例如在type A [T] int中,但我认为这可以通过几种不同的方式相对容易地解决。 例如,通用定义可以移动到关键字本身,而不是类型名称,即:

  • func[T] Example(v T) T
  • type[T] A int

这里的复杂性可能来自类型声明块的使用,例如

type (
  A int
)

但我认为这非常罕见,基本上可以说如果你需要泛型,那么你就不能使用这些块之一。

我觉得写起来会很不幸

type[T] A []T
var s A[int]

因为方括号从A的一侧移动到另一侧。 当然可以,但我们应该追求更好。

也就是说,在当前语法中使用type关键字确实意味着我们可以用方括号替换括号。

这似乎与数组类型与表达式语法是[N]Tarr[i]没有什么不同,就如何声明某些内容与它的使用方式不匹配而言。 是的,在var arr [N]T中,方括号在arr的同一侧结束,就像使用arr时一样,但我们通常根据类型和表达式语法来考虑语法正相反。

我扩展和改进了一些旧的不成熟的想法,试图统一自定义和内置泛型。

我不确定讨论( vs < vs [type的使用是否是自行车脱落或语法确实存在问题

@ianlancetaylor ...想知道反馈是否需要对提议的设计进行任何调整? 我自己对反馈的感觉是,许多人认为接口和合同可以结合起来,至少在最初是这样。 一段时间后似乎发生了转变,这两个概念应该分开。 但我可能读错了趋势。 希望在今年的版本中看到一个实验性选项!

是的,我们正在考虑修改草案设计,包括查看人们提出的许多反建议。 什么都没有最终确定。

顺便补充一些实战经验报告:
我在我的 Go 解释器https://github.com/cosmos72/gomacro 中实现了泛型作为语言扩展。 有趣的是,这两种语法

type[T] Pair struct { First T; Second T }
type Pair[T] struct { First T; Second T }

结果在解析器中引入了很多歧义:第二个可以被解析为声明 Pair 是T结构的数组,其中 T 是某个常量整数。 当使用Pair时也存在歧义: Pair[int]也可以被解析为表达式而不是类型:它可以索引名为Pair的数组/切片/映射索引表达式int (注意: int和其他基本类型不是 Go 中的保留关键字),所以我不得不求助于一种新的语法 - 诚然丑陋,但确实有效:

template[T] type Pair struct { First T; Second T }
type pairOfInt = Pair#[int]
var p Pair#[int]

和类似的功能:

template[T] func Sum(args ...T) T { /*...*/ }
Sum#[int] (1,2,3)

因此,虽然理论上我同意语法是表面问题,但我必须指出:
1) 从一方面来看,语法是 Go 程序员将要接触的 - 所以它必须是富有表现力的、简单的并且可能是可口的
2) 另一方面,为了解决引入的歧义,语法选择不当会使解析器、类型检查器和编译器复杂化

Pair[int]也可以被解析为表达式而不是类型:它可以使用索引表达式int索引名为Pair的数组/切片/映射

这不是解析歧义,只是语义歧义(直到名称解析之后); 无论哪种方式,句法结构都是相同的。 请注意, Sum#[int]也可以是类型或表达式,具体取决于Sum是什么。 现有代码中的(*T)也是如此。 只要名称解析不影响正在解析的结构,就可以了。

将此与<>的问题进行比较:

f ( a < b , c < d >> (e) )

您甚至无法将其标记化,因为>>可能是一两个标记。 然后,您无法判断f是否有一个或两个参数......表达式的结构会根据a表示的内容发生显着变化。

无论如何,我很想看看团队中当前关于泛型的想法是什么,特别是“约束即代码”是否已被迭代或放弃。 我可以理解想要避免定义一种独特的约束语言,但事实证明,编写充分约束所涉及类型的代码会导致一种不自然的风格,而且您还必须限制编译器可以根据代码实际推断出的类型因为否则这些推论可能会变得任意复杂,或者可能依赖于未来可能改变的语言的事实。

@cosmos72

也许我错了,但除了@stevenblenkinsop所说的之外,是否有可能是一个术语:

a b

如果已知 b 是字母数字(无运算符/无分隔符)并附加可选的[identifier]并且 a 不是特殊关键字/特殊字母数字(例如,无导入/包/类型/功能)?。

不太懂go的语法。

在某些方面,像 int 和 Sum[int] 这样的类型无论如何都会被视为表达式:

type (
    nodeList = []*Node  // nodeList and []*Node are identical types
    Polar    = polar    // Polar and polar denote identical types
)

如果 go 允许中缀函数,那么a type tag确实会模棱两可,因为type可能是中缀函数或类型。

我今天注意到这个提案的问题概述声称 Swift:

声明T满足Equatable协议使得在函数体中使用==有效。 Equatable似乎是 Swift 内置的,无法以其他方式定义。

这似乎更像是一个旁白,而不是对这个主题的决定产生深刻影响的事情,但如果它给了比我聪明得多的人一些灵感,我想指出实际上并没有什么特别之处大约Equatable除了它是在语言中预定义的(主要是为了让许多其他内置类型可以“符合”它)。 完全可以创建类似的协议:

protocol Equatable2 {
    static func == (lhs: Self, rhs: Self) -> Bool
}

class uniq: Equatable2 {
    static func == (lhs: uniq, rhs: uniq) -> Bool {
        return false
    }
}

let narf = uniq(), poit = uniq()

func !=<T: Equatable2> (lhs: T, rhs: T) -> Bool {
    return !(lhs == rhs)
}

print(narf != poit)

@sighoya
我在谈论为泛型提出的语法a[b]的歧义,因为它已经用于索引切片和映射 - 而不是a b

与此同时,我一直在研究 Haskell,虽然我事先知道它广泛使用类型推断,但它的泛型的表现力和复杂性让我感到惊讶。

不幸的是,它有一个非常奇特的命名方案,所以乍一看并不总是很容易理解。 例如class实际上是类型的约束(通用或非通用)。 Eq类是其值可以与 '==' 和 '/=' 比较的类型的约束:

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool

表示如果存在中缀函数==/=的“特化”(实际上是 Haskell 用语中的“实例”),则类型a满足约束Eq $ /=接受两个参数,每个参数的类型a并返回Bool结果。

我目前正在尝试将 Haskell 泛型中的一些想法应用到 Go 泛型的提案中,看看它们是否适合。 我很高兴看到对 C++ 和 Java 以外的其他语言的调查正在进行中:

上面的 Swift 示例和我的 Haskell 示例表明,对泛型类型的约束已经被几种编程语言在实践中使用,并且在泛型和约束的各种方法方面存在大量经验,并且在这些程序员中可以获得(和其他)语言。

在我看来,在最终确定 Go 泛型提案之前,当然值得研究这些经验。

奇怪的想法:如果您希望泛型类型满足的约束形式恰好与接口定义或多或少一致,您可能会使用我们已经习惯的现有类型断言语法:

type Comparer interface {
  Compare(v interface{}) (*int, error)
}
type PriorityQueue<T.(Comparer)> struct {
  things []T
}

如果这已经在别处详尽讨论过,我们深表歉意; 我还没看过,但我仍然被文学所吸引。 我一直忽略它,因为,好吧,我不想在任何版本的 Go 中使用泛型。 但这个想法似乎正在获得动力,并且在整个社区中都有一种不可避免的感觉。

@jesse-amano 有趣的是,您不希望在任何版本的 Go 中使用泛型。 我觉得这很难理解,因为作为一名程序员,我真的不喜欢重复自己。 每当我用“C”编程时,我发现自己必须在一些新的数据类型上实现相同的基本事物,如列表或树,并且不可避免地我的实现充满了错误。 使用泛型,我们只能拥有任何算法的一个版本,整个社区都可以为使该版本成为最佳版本做出贡献。 不重复自己的解决方案是什么?

关于另一点,Go 似乎正在为泛型约束引入新语法,因为接口不允许重载运算符(如 '==' 和 '+')。 有两种方法可以向前推进,为泛型约束定义一个新机制,这是 Go 似乎正在采用的方式,或者允许接口重载运算符,这是我更喜欢的方式。

我更喜欢第二个选项,因为它使语言语法更小更简单,并允许声明可以使用常用运算符的新数字类型,例如可以使用“+”添加的复数。 反对这一点的论点似乎是人们可能会滥用运算符重载来使“+”做一些奇怪的事情,但这对我来说似乎不是一个论据,因为我已经可以滥用任何函数名称,例如我可以编写一个名为“打印”的函数' 这会擦除我硬盘上的所有数据并终止程序。 我希望能够限制运算符和函数的重载以符合某些公理性质,如交换性或关联性,但如果它不适用于运算符和函数,我认为没有多大意义。 运算符只是一个中缀函数,而函数毕竟只是一个前缀运算符。

还有一点要提的是,引用多个类型参数的泛型约束非常有用,如果单参数泛型约束是类型的谓词,多参数约束是类型的关系。 Go 接口不能有多个类型参数,因此需要再次引入新语法,或者需要重新设计接口。

所以在某种程度上我同意你的观点,Go 并不是作为一种通用语言设计的,任何使用泛型的尝试都是次优的。 也许最好让 Go 不使用泛型,并围绕泛型从头开始设计一种新语言,以保持语言的简洁性和简单的语法。

@keean我对在需要时重复几次自己并没有那么强烈的反感,而且 Go 的错误处理方法、方法接收器等通常似乎可以很好地避免大多数错误。

在过去四年的少数案例中,我发现自己处于需要将复杂但可泛化的算法应用于两个以上复杂但自洽的数据结构的情况,并且在所有情况下 - 我这样说严肃地说——我发现通过 go:generate 生成代码已经绰绰有余了。

当我阅读经验报告时,在许多情况下,我认为 go:generate 或类似的工具可以解决问题,而在其他一些情况下,我觉得也许 Go1 不是正确的语言,而其他的可能是代替使用(如果某些 Go 代码需要使用它,可能使用插件包装器)。 但我知道我很容易推测我_可能已经_做了什么,_可能_起作用了; 到目前为止,我的实践经验为零,这让我希望 Go1 有更多表达泛型类型的方式,但可能是我思考事物的方式很奇怪,或者我只是非常幸运地只工作在不需要泛型的项目上。

我希望如果 Go2 最终支持通用语法,它将有一个相当简单的映射到将要生成的逻辑,没有可能由装箱/拆箱、“具体化”、继承链等引起的奇怪的边缘情况。其他语言不得不担心。

@jesse-amano 不过,根据我的经验,不是几次,每个程序都是众所周知的算法的组合。 我不记得上一次写原始算法是什么时候了,可能是一个需要领域知识的复杂优化问题。

在编写程序时,我做的第一件事是尝试将问题分解为我可以编写的众所周知的块、参数解析器、一些文件流、基于约束的 UI 布局。 人们犯错的不仅仅是复杂的算法,几乎没有人能在第一次写出正确的“min”和“max”实现(参见:http://componentsprogramming.com/writing-min-function-part5/)。

go:generate 的问题在于它基本上只是一个宏处理器,它没有类型安全性,你必须以某种方式对生成的代码进行类型检查和错误检查,在运行生成之前你无法做到这一点。 这种元编程很难调试。 我不想写程序来写程序,我只想写程序:-)

因此,与泛型的不同之处在于,我可以编写一个简单的 _direct_ 程序,可以根据我对含义的理解进行错误检查和类型检查,而无需生成代码、调试并将错误返回到生成器。

一个非常简单的例子是“交换”,我只想交换两个值,我不在乎它们是什么:

swap<A>(x: *A, y: *A) {
   let tmp = *x
   *x = *y
   *y = tmp
}

现在我觉得看这个函数是否正确是微不足道的,看它是泛型的,可以应用到任何类型上也是微不足道的。 为什么我要一次又一次地为每种类型的指针输入这个函数,这些指针指向我可能想要使用交换的值。 当然,我可以从中构建更大的通用算法,例如就地排序。 我认为即使是简单算法的 go:generate 代码也不容易看出它是否正确。

我很容易犯这样的错误:

let tmp = *x
*y = *x
*x = tmp

每次我想交换两个指针的内容时手动输入。

我知道在 Go 中做这种事情的惯用方法是使用一个空接口,但这不是类型安全的并且很慢。 然而在我看来,Go 没有合适的特性来优雅地支持这种泛型编程,而空接口提供了一个逃生口来解决这些问题。 与其完全改变 go 的风格,不如从头开发一种适合这种泛型的语言。 有趣的是,'Rust' 得到了很多通用的东西,但是因为它使用静态内存管理而不是垃圾收集,它增加了很多复杂性,这对于大多数编程来说并不是真正必要的。 我认为在 Haskell、Go 和 Rust 之间,可能有所有必要的东西来制作一个像样的主流通用语言,只是混合在一起。

有关信息:我目前正在编写有关 Go 泛型的愿望清单

目的是在我的 Go 解释器gomacro中实际实现它,它已经具有 Go 泛型的不同实现(以 C++ 模板为模型)。

还没写完,欢迎反馈:)

@基恩

我阅读了您链接的有关 min 函数的博客文章,以及导致它的四篇文章。 我什至没有观察到有人试图提出“几乎没有人可以编写'min'的正确实现......”的论点。 作者实际上似乎承认他们的第一个实现是正确的......只要域仅限于数字。 它是对象和类的引入,以及它们仅在一个维度上进行比较的要求,除非该维度中的值是相同的,除非是什么时候——等等,这会产生额外的复杂性。 需要仔细定义复杂对象上的比较器和排序函数所涉及的微妙隐藏要求正是我_不_喜欢泛型作为一个概念的原因(至少在 Go 中;Java 与 Spring 似乎已经是一个足够好的组合环境将一堆成熟的库组合成一个应用程序)。

我个人认为宏生成器不需要类型安全。 如果他们正在生成清晰的代码( gofmt有助于将标准设置得相当低),那么编译时错误检查就足够了。 无论如何,对于生成器(或调用它的代码)的用户来说,这对生产来说并不重要; 在公认的少数时候,我被要求将通用算法编写为宏,一些单元测试(通常是浮点数、字符串和指向结构的指针——如果有任何硬编码的类型应该'不是硬编码的,这三个中的一个将与它不兼容;如果这三个中的任何一个不能在通用算法中使用,那么它就不是通用算法)足以确保宏正常工作。

swap是一个不好的例子。 对不起,但确实如此。 它已经是 Go 中的单行代码,不需要通用函数来包装它,程序员也没有空间犯不明显的错误。

*y, *x = *x, *y

标准库中也已经有一个就地的sort 。 它使用接口。 要制作特定于您的类型的版本,请定义:

type myslice []mytype
func (s myslice) Len() int { return len(s) }
func (s myslice) Less(i, j int) bool { return s[i].whatWouldAlsoBeNeededInAGenericImpl(s[j]) }
func (s myslice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

诚然,它的输入字节数比SortableList<mytype>(myThings).Sort()多几个字节,但它的读起来不那么密集,不太可能在整个应用程序的其余部分“口吃”,如果确实出现错误,我不太可能需要像堆栈跟踪一样重的东西来找到原因。 当前的方法有几个优点,我担心如果我们过多地使用泛型,我们会失去它们。

@jesse-amano
即使您不了解稳定排序的必要性,“最小/最大”的问题也适用。 例如,一个开发人员在一个模块中为某些数据类型实现了 min/max,然后在没有适当检查假设的情况下被另一个团队成员用于排序或其他算法,并导致奇怪的错误,因为它不稳定。

我认为编程主要是组合标准算法,程序员很少创造新的创新算法,所以 min/max 和 sort 只是示例。 我选择的具体例子中的漏洞只是表明我没有选择很好的例子,它没有解决实际问题。 我选择“swap”是因为它非常简单,而且我可以快速输入。 我可以选择许多其他算法,排序、旋转、分区,这些都是非常通用的算法。 当您编写一个使用像红/黑树这样的集合的程序时,很快就会厌倦必须为您想要集合的每种不同数据类型重做树,因为您想要类型安全,并且空接口比“C”中的“void*”好一点。 然后你会再次对使用这些树中的每一个的每个算法做同样的事情,比如前序、中序、后序迭代、搜索,这是在我们进入任何复杂的东西之前,比如 Tarjan 的网络算法(不相交集合、堆、最小生成树、最短路径、流等)

我认为代码生成器有它们的位置,例如从 json-schema 生成验证器或从语法定义生成解析器,但我认为它们不适合替代泛型。 对于泛型编程,我希望能够编写任何算法一次,并使其清晰、简单和直接。

无论如何,我同意你关于“Go”的看法,我不认为“Go”从一开始就被设计为一种好的通用语言,现在添加泛型可能不会产生一种好的通用语言,并且将失去它已经拥有的一些直接性和简单性。 就个人而言,如果您必须使用代码生成器(除了从 json-schema 生成验证器或从语法文件生成解析器之类的东西),那么您可能无论如何都使用了错误的语言。

编辑:关于使用“float”“string”“pointer-to-struct”测试泛型,我认为除了“swap”之外,没有很多泛型算法可以处理不同的类型。 真正的“通用”功能实际上仅限于洗牌,并且不会经常发生。 受约束的泛型更有趣,泛型类型受某些接口的约束。 如您所见,使用标准库中的就地排序示例,您可以在有限的情况下使一些受约束的泛型在“Go”中工作。 我喜欢 Go 接口的工作方式,你可以用它们做很多事情。 我更喜欢真正的受限泛型。 我不太喜欢像当前的泛型提案那样添加第二个约束机制。 接口直接约束类型的语言会更加优雅。

有趣的是,据我所知,引入新约束的唯一原因是因为 Go 不允许在接口中定义运算符。 早期的泛型提案确实允许类型受接口约束,但由于无法处理像“+”这样的运算符而被放弃。

@基恩
也许有一个更好的地方进行长时间的讨论。 (也许不是;我环顾四周,这似乎是在 Go2 中讨论泛型的地方。)

我当然理解稳定排序的必要性! 我怀疑原始 Go1 标准库的作者也理解它,因为sort.Stable自公开发布以来一直存在。

我认为标准库的sort包的优点在于它_doesn't_ 仅适用于切片。 当接收者是一个切片时,这当然是最简单的,但你真正需要的是一种知道容器中有多少值的方法( Len() int方法),如何比较它们( Less(int, int) bool方法),以及如何交换它们(当然是Swap(int, int)方法)。 您可以使用渠道实现sort.Interface ! 当然,这很慢,因为通道不是为高效索引而设计的,但在充足的执行时间预算下,它可以被证明是正确的。

我并不是要吹毛求疵,但一个不好的例子的问题是......它很糟糕。 像sortmin这样的东西只是_not_ 点支持像泛型这样的高影响语言特性。 我非常强烈地觉得这些例子中的漏洞_does_解决了实际问题; _my_ 的一点是,当语言中已经存在更好的解决方案时,就不需要泛型了。

@jesse-amano

语言中已经存在更好的解决方案

哪一个? 我没有看到比类型安全的约束泛型更好的东西。 生成器不是 Go,简单明了。 接口和反射会产生不安全、缓慢和容易出现恐慌的代码。 这些解决方案已经足够好了,因为没有别的了。 泛型将解决样板文件、不安全的空接口结构的问题,最糟糕的是,消除了更容易出现运行时恐慌的反射的许多使用。 即使是新的错误包提案也缺乏泛型,它的 API 将从中受益匪浅。 您可以以As为例 - 不习惯,容易出现恐慌,难以使用,需要经过兽医检查才能正确使用。 这一切都是因为 Go 缺少任何类型的泛型。

sortmin和其他泛型算法都是很好的例子,因为它们展示了泛型的主要好处——可组合性。 它们允许构建可以链接在一起的通用转换例程的广泛库。 最重要的是,它将易于使用、安全、快速(至少使用泛型是可能的),不需要样板、生成器、接口{}、反射和其他晦涩的语言特性,因为没有其他方法。

@creker

哪一个?

对于排序的东西,包sort 。 任何实现sort.Interface的东西都可以排序(使用您选择的稳定或不稳定算法;一些就地版本是通过sort包提供的,但您可以自由编写自己的相似或不同的 API)。 由于标准库sort.Sortsort.Stable都对通过参数列表传递的值进行操作,因此您返回的值与您开始使用的值相同 - 因此,必然地,类型你回来的类型和你开始的类型一样。 它是完全类型安全的,编译器会完成所有工作来推断您的类型是否实现了所需的接口,并且能够_至少_尽可能多地进行编译时优化,就像使用泛型风格的sort<T>函数一样.

对于交换东西,单线x, y = y, x 。 同样,不需要类型断言、接口转换或反射。 它只是交换两个值。 编译器可以轻松确保您的操作是类型安全的。

在所有情况下,我认为没有一个特定工具是比泛型更好的解决方案,但是对于泛型应该解决的任何给定问题,我相信有更好的解决方案。 我可能在这里错了; 我仍然愿意看到一个泛型可以做的事情的例子,而所有现有的解决方案都会很糟糕。 但如果我能在其中戳洞,那么它就不是这些例子之一。

我也不太喜欢xerrors包,但xerrors.As并没有让我觉得不习惯; 毕竟,它是一个与json.Unmarshal非常相似的 API。 它可能需要更好的文档和/或示例代码,但其他方面都很好。

但是不, sortmin本身就是非常糟糕的例子。 前者已经存在于 Go 中并且是完全可组合的,所有这些都不需要泛型。 后者在最广泛的意义上是sort的输出之一(我们已经解决了),并且在可能需要更专业或优化的解决方案的情况下,您无论如何都会编写专门的解决方案而不是依靠仿制药。 同样,标准库的sort包中没有使用生成器、接口{}、反射或“晦涩”的语言功能。 有非空接口(在 API 中定义良好,因此如果使用不正确,会出现编译时错误,推断为不需要强制转换,并在编译时检查,因此不需要断言)。 可能有一些样板_如果_您正在排序的集合是一个切片,但如果它恰好是一个结构(例如表示二叉搜索树的根节点的结构?),您可以使其满足sort.Interface也是,所以它实际上比通用集合更灵活。

@jesse-amano

我的观点是,当语言中已经存在更好的解决方案时,就不需要泛型了

我认为更好的解决方案实际上是相对地基于您的看法。 如果我们有更好的语言,我们可以有更好的解决方案,这就是为什么我们要让这种语言变得更好。 例如,如果存在更好的泛型,我们可以在我们的 stdlib 中有更好的sort ,至少目前实现排序接口的方式对我来说不是一个好的用户体验,我仍然需要输入很多类似的代码我强烈地觉得我们可以把它抽象掉。

@jesse-amano

我认为标准库的 sort 包的优点在于它不仅适用于切片。

我同意,我喜欢标准排序。

前者已经存在于 Go 中并且是完全可组合的,所有这些都不需要泛型。

这是一种错误的二分法。 Go 中的接口已经是泛型的一种形式。 机制不是事物本身。 超越语法,看到目标,即能够以通用方式不受限制地表达任何算法。 'sort' 的接口抽象是一个泛型,它允许任何可以实现所需方法的数据类型进行排序。 符号完全不同。 我们可以写:

f<T>(x: T) requires Sortable(T)

这意味着类型“T”必须实现“Sortable”接口。 在“Go”中,这可能写成func f(x Sortable) 。 所以至少 Go 中的函数应用程序可以通用处理,但是有些操作不能像算术或解引用。 Go 做得很好,因为接口可以被认为是类型谓词,但是 Go 没有关于类型关系的答案。

很容易看出 Go 的局限性,请考虑:

func merge(x, y Sortable)

我们将合并两个可排序的东西,但是 Go 不允许我们强制这两个东西必须相同。 将此与以下内容进行对比:

merge<T>(x: T, y: T) requires Sortable(T)

在这里,我们很清楚我们正在合并两个相同的可排序类型。 'Go' 丢弃了底层的类型信息,只是将任何“可排序”的东西都视为相同。

让我们尝试一个更好的例子:假设我想编写一个可以包含任何数据类型的红/黑树,作为一个库,以便其他人可以使用它。

Go 中的接口已经是泛型的一种形式。

如果是这样,那么这个问题可能已经解决了,因为原来的陈述是:

这个问题建议 Go 应该支持某种形式的泛型编程。

模棱两可对各方都不利。 接口确实是泛型编程的_a_ 形式,并且它们确实_不一定_ 必须自己解决其他形式的泛型编程可以解决的最后一个问题。 因此,为了简单起见,让我们允许任何可以使用本提案/问题范围之外的工具解决的问题被视为“在没有泛型的情况下解决”。 (我相信现实世界中遇到的绝大多数可解决的问题,如果不是全部的话,都在那个集合中,但这只是为了确保我们都说同一种语言。)

考虑: func merge(x, y Sortable)

我不清楚为什么合并两个可排序的东西(或实现sort.Interface的东西)与合并两个集合有任何不同_in general_。 对于切片,这是append ; 对于地图,这是for k, v := range m { n[k] = v } ; 而对于更复杂的数据结构,必然有更复杂的合并策略,具体取决于结构(可能需要其内容来实现结构所需的某些方法)。 假设您正在谈论一种更复杂的排序算法,该算法在将它们重新合并在一起之前对分区进行分区并选择子算法,那么您需要的不是分区是“可排序的”,而是某种保证您的分区是在合并之前已经_sorted_。 这是一种非常不同的问题,模板语法不能以任何明显的方式帮助解决; 自然,您会需要一些非常严格的单元测试来保证合并排序算法的可靠性,但您肯定不想公开一个 _exported_ API 来给开发人员带来这种负担。

您确实提出了一个有趣的观点,即 Go 没有一种很好的方法来检查两个值是否属于相同类型而无需反射、类型切换等。我确实觉得使用interface{}是一个完全可以接受的解决方案通用容器(例如循环链表)的情况作为包装 API 以实现类型安全的样板绝对是微不足道的:

type MyStack struct { stack Stack }
func (s *MyStack) Push(v MyType) error { return s.stack.Push(v) }
func (s *MyStack) Pop() (MyType, error) {
  v, err := s.stack.Pop()
  var m MyType
  if v != nil {
    if m, ok := v.(MyType); ok { return m, err; }
    panic("this code should be unreachable from the exported API")
  }
  return nil, err
}

我很难想象为什么这个样板会是一个问题,但如果是这样,一个合理的替代方案可能是(文本/)模板。 您可以使用//go:generate stackify MyType github.com/me/myproject/mytype注释来注释要为其定义堆栈的类型,并让go generate为您生成样板。 只要cmd/stackify/stackify_test.go尝试使用至少一个结构和至少一个内置类型,并且它编译并通过,我不明白为什么这会是一个问题——而且可能非常接近如果您定义了一个模板,那么任何编译器最终都会“在幕后”做些什么。 唯一的区别是错误更有帮助,因为它们不那么密集。

(在某些情况下,我们可能想要一个通用的_something_,它更关心两个相同类型的东西而不是它们的行为,这不属于“东西的容器”类别。这会很有趣,但是向语言添加通用模板构造语法仍然可能不是唯一可用的解决方案。)

假设样板文件_不是_一个问题,我有兴趣解决创建一个红/黑树的问题,该树对于调用者来说就像sortencoding/json这样的包一样容易使用。 我肯定会失败,因为……好吧,我只是没那么聪明。 但我很高兴知道我能走多远。

编辑:可以在这里看到一个示例的开头,尽管它远未完成(最好我可以在几个小时内拼凑起来)。 当然,类似的数据结构也存在其他尝试

@jesse-amano

如果是这样,那么这个问题可能已经解决了,因为原来的陈述是:

不仅仅是接口是泛型的一种形式,而且改进接口方法可以让我们在泛型中一路走好。 例如,多参数接口(您可以拥有多个“接收者”)将允许类型上的关系。 允许接口覆盖诸如加法和取消引用之类的运算符将消除对任何其他形式的类型约束的需要。 接口_可以_成为您需要的所有类型约束,如果它们的设计理解完全通用泛型的端点。

接口在语义上类似于 Haskell 的类型类,而 Rust 的 trait _do_ 解决了这些通用问题。 类型类和特征解决了 C++ 模板所做的所有相同的通用问题,但以类型安全的方式(但可能不是所有元编程使用,我认为这是一件好事)。

我很难想象为什么这个样板会是一个问题,但如果是这样,一个合理的替代方案可能是(文本/)模板。

我个人对这么多样板文件没有意见,但我理解完全没有样板文件的愿望,作为一名程序员,这很无聊且重复,而这正是我们编写程序要避免的那种任务。 因此,就个人而言,我认为为“堆栈”接口/类型类编写实现正是使您的数据类型“可堆叠”的正确方式。

Go 有两个限制阻碍进一步的泛型编程。 “类型”等价问题,例如定义数学函数,以便结果和所有参数必须相同。 我们可以想象:

mul<T>(x, y T) T requires Addable(T) {
    r := 0
    for i := 0; i < y; ++i  {
        r = r + x
    }
    return r
}

为了满足 '+' 的约束,我们需要确保xy是数字的,而且它们都是相同的底层类型。

另一个是接口仅限于单一的“接收器”类型。 这个限制意味着您不必只在上面键入一次样板文件(我认为这是合理的),而是针对您想要放入 MyStack 的每种不同类型。 我们想要的是将包含的类型声明为接口的一部分:

type Stack<T> interface {...}

除其他外,这将允许在T中声明一个参数化的实现,以便我们可以使用 Stack 接口将任何T放入 MyStack 中,只要所有使用 Push 和在同一个 MyStack 实例上的 Pop 对同一个“值”类型进行操作。

通过这两个更改,我们应该能够创建通用的红/黑树。 没有它们应该是可能的,但是就像堆栈一样,您必须为您希望放入红/黑树的每种类型声明一个新的接口实例。

从我的角度来看,上面对接口的两个扩展是 Go 完全支持“泛型”所需要的。

@jesse-amano
看看红/黑树的例子,我们真正想要的一般是“映射”的定义,红/黑树只是一种可能的实现。 因此,我们可能期望这样的界面:

type Map<Key, Value> interface {
   put(x Key, y Value) 
   get(x Key) Value
}

然后可以提供红/黑树作为实现。 理想情况下,我们希望编写不依赖于实现的代码,因此您可以提供哈希表、红黑树或 BTree。 然后我们将编写我们的代码:

f<K, V, T>(index T) T requires Map<K, V> {
   ...
}

现在无论f是什么,它都可以独立于 Map 的实现工作, f可能是别人写的库函数,不需要知道我的应用程序是否使用了红色/黑树或哈希图。

按照现在的情况,我们需要像这样定义一个特定的地图:

type MapIntString interface {
   put(x Int, y String)
   get(x Int) String
}

这还不错,但这意味着如果我们要能够在我们不使用的应用程序中使用它,则必须为键和值类型的每种可能组合编写“库”函数f编写库时不知道键和值的类型。

虽然我同意@keean最后的评论,但困难在于在 Go 中编写一个实现已知接口的红/黑树,例如刚才建议的那个。

如果没有泛型,众所周知,为了实现与类型无关的容器,必须使用interface{}和/或反射——不幸的是,这两种方法都很慢且容易出错。

@基恩

不仅接口是泛型的一种形式,而且改进接口方法可以让我们在泛型中一路走好。

迄今为止,我不认为与此问题相关的任何提案是一种改进。 说它们在某些方面都存在缺陷似乎是毫无争议的。 我相信这些缺陷严重超过任何好处,而且许多_声称_的好处实际上已经得到现有功能的支持。 我的信念是基于实践经验,而不是推测,但它仍然是轶事。

我个人对这么多样板文件没有意见,但我理解完全没有样板文件的愿望,作为一名程序员,这很无聊且重复,而这正是我们编写程序要避免的那种任务。

我也不同意这一点。 作为一名有偿专业人士,我的目标是减少时间/精力成本_为我自己和他人_,同时增加我雇主的收益,但这些可能是衡量的。 一项“无聊”的任务只有在耗时的情况下才是糟糕的; 它不会很困难,或者不会很无聊。 如果前期只是有点耗时,但消除了未来耗时的活动和/或让产品更快发布,那么它仍然是完全值得的。

然后可以提供红/黑树作为实现。

我认为最近几天我在实施红/黑树方面取得了不错的进展, (它尚未完成;甚至没有自述文件)但我担心如果它不够丰富,我已经无法说明我的观点很清楚,我的目标不是为接口而努力,而是为实现而努力。 我正在写一棵红/黑树,当然我希望它_有用_,但我不在乎其他开发人员可能想要将它用于什么_特定_的东西。

我知道红/黑树库所需的最小接口是其元素上存在“弱”排序的接口,所以我需要一些_like_一个名为Less(v interface{}) bool的函数,但如果调用者有一个方法做了类似的事情,但没有命名为Less(v interface{}) bool ,由他们编写样板包装器/垫片以使其工作。

当您访问红/黑树包含的元素时,您会得到interface{} ,但如果您愿意相信我的保证,即库提供了_is_ 红/黑树,我不明白您为什么不这样做不要相信您放入的元素类型与您取出的元素类型完全相同。 如果您_do_ 信任这两个保证,那么该库根本不会出错。 只需编写(或粘贴)十几行代码即可涵盖类型断言。

现在您拥有了一个完全安全的库(同样,假设不超过您首先愿意为下载该库而提供的信任级别),它甚至具有您想要的确切函数名称。 这个很重要。 在 Java 风格的生态系统中,库作者正在向后弯腰编写代码以反对_exact_ 接口定义(他们几乎_have_,因为语言通过class MyClassImpl extends AbstractMyClass implements IMyClass语法强制执行它)并且有一堆额外的官僚主义,您必须竭尽全力为第三方库制作一个外观以适应您组织的编码标准(这是相同数量的样板,如果不是更多的话),或者允许这成为一个“例外”您的组织的编码标准(最终您的组织在其标准中的例外情况与在其代码库中的例外情况一样多),或者放弃使用一个非常好的库(为了争论,假设该库实际上是好的)。

理想情况下,我们希望编写不依赖于实现的代码,因此您可以提供哈希表、红黑树或 BTree。

我同意这个理想,但我认为 Go 已经满足了它。 使用如下界面:

type MyStorage interface {
  Get(KeyType) (ValueType, error)
  Put(KeyType, ValueType) error
}

唯一缺少的是参数化KeyTypeValueType是什么的能力,我不相信这特别重要。

作为红/黑树库的(假设的)维护者,我不在乎你的类型是什么。 我只会将interface{}用于处理“一些数据”的所有核心函数,并且_maybe_ 提供了一些导出的示例函数,让您可以更轻松地将它们与常见类型(如stringint )一起使用

作为红/黑树库的(假设的)调用者,我可能只是希望它用于快速存储和查找时间。 我不在乎它是一棵红/黑树。 我关心我能从中得到Get东西和Put东西,而且——重要的是——我关心那些东西是什么。 如果库不提供名为GetPut的函数,或者不能与我定义的类型完美交互,那对我来说并不重要,只要对我来说很容易自己编写GetPut方法,并使我自己的类型满足库在我使用它时需要的接口。 如果这不容易,我通常会发现这是库作者的错,而不是语言的错,但再次有可能存在我不知道的反例。

顺便说一句,如果不是这样,代码可能会变得更加复杂。 正如您所说,键/值存储有许多可能的实现。 传递一个抽象的键/值存储“概念”隐藏了如何完成键/值存储的复杂性,并且我团队中的开发人员可能会为他们的任务选择错误的一个(包括我自己的未来版本,其对键的了解/value 存储实现已内存不足!)。 尽管我们在代码审查中尽了最大努力,但应用程序或其单元测试可能包含微妙的依赖于实现的代码,当某些键/值存储依赖与数据库的连接而其他不依赖时,这些代码会停止可靠地工作。 当错误报告带有很大的堆栈跟踪时,这是很痛苦的,并且堆栈跟踪中唯一引用 _real_ 代码库中某些内容的行指向使用接口值的行,所有这些都是因为该接口的实现是生成的代码(你只能在运行时看到)而不是普通的结构,方法返回可读的错误值。

@jesse-amano
我同意你的观点,我喜欢“用户”代码声明一个抽象其工作方式的接口的“Go”方式,然后为库/依赖项编写该接口的实现。 这与大多数其他语言对接口的看法相反。 但是一旦你得到它是非常强大的。

我仍然希望在通用语言中看到以下内容:

  • 参数类型,例如: RBTree<Int, String> ,因为这将强制用户集合的类型安全。
  • 类型变量,例如: f<T>(x, y T) T ,因为这是定义相关函数族(如加法、减法等)所必需的,其中函数是多态的,但我们要求所有参数具有相同的底层类型。
  • 类型约束,例如: f<T: Addable>(x, y T) T ,它将接口应用于类型变量,因为一旦我们引入类型变量,我们需要一种方法来约束这些类​​型变量,而不是将Addable视为一种类型。 如果我们将Addable视为一个类型并写成f(x, y Addable) Addable ,我们无法判断xy的原始底层类型是否相同彼此或返回的类型。
  • 多参数接口,例如: type<K, V> Map<K, V> interface {...} ,可以像merge<K, V, T: Map<K, V>>(x, y T) T一样使用,它允许我们声明不仅由容器类型参数化的接口,而且在这种情况下还包括键和值地图的类型。

我认为这些都会增加语言的抽象能力。

这方面有什么进展或时间表吗?

@leaxoy @ianlancetaylorGopherCon上安排了关于“Go 中的泛型”的演讲。 我希望在那次谈话中听到更多关于当前事态的信息。

@griesemer感谢您提供该链接。

@keean我也很想在这里看到 Rust 的 Where 子句,这可能是对您的type constraints提案的改进。 它允许使用类型系统来限制诸如“在查询之前启动事务”之类的行为,以便在没有运行时反射的情况下对其进行类型检查。 看看这个视频: https ://www.youtube.com/watch?v=jSpio0x7024

@jadbox抱歉,如果我的解释不清楚,但“where”子句几乎正是我所提议的。 rust 中“where”之后的内容是类型约束,但我想我在之前的帖子中使用了关键字“requires”。 至少十年前,这些东西都是在 Haskell 中完成的,除了 Haskell 在类型签名中使用 '=>' 运算符来指示类型约束,但它是相同的底层机制。

我在上面的摘要帖子中省略了这个,因为我想让事情变得简单,但我想要这样的东西:

merge<K, V, T>(x, y T) T requires T: Map<K, V>

但除了对长约束集更具可读性的语法之外,它并没有真正增加您可以做的任何事情。 您可以通过将约束放在初始声明中的类型变量之后,使用“where”子句表示任何内容,如下所示:

merge<K, V, T: Map<K, V>>(x, y T) T

如果您可以在声明类型变量之前引用它们,则可以在其中放置任何约束,并且可以使用逗号分隔的列表将多个约束应用于同一类型变量。

据我所知,'where'/'requires' 子句的唯一优点是所有类型变量都已预先声明,这可能使解析器和种类推断更容易。

这仍然是对最近宣布的当前/最新工作的Go 2 泛型提案进行反馈/讨论的正确线程吗?

简而言之,我真的很喜欢提案的总体方向,特别是合同机制。 但我关心的是编译时泛型参数必须(总是)是类型参数,这似乎是一个单一的假设。 我在这里写了一些关于这个问题的反馈:

对于 Go 2 泛型来说,只有类型参数泛型就足够了吗?

当然这里的评论是可以的,但总的来说,我不认为 GitHub 问题是一个很好的讨论格式,因为它们不提供任何类型的线程。 我认为邮件列表更好。

我认为目前还不清楚人们希望在常量值上参数化函数的频率。 最明显的情况是数组维度——但您已经可以通过将所需的数组类型作为类型参数传递来做到这一点。 除了这种情况,通过将 const 作为编译时参数而不是运行时参数传递,我们真正获得了什么?

Go 已经提供了许多不同的很好的方法来解决问题,我们永远不应该添加任何新的东西,除非它解决了一个非常大的问题和缺点,这显然不是这样做的,即使在这种情况下,随之而来的增加的复杂性也是一个非常付出高昂的代价。

Go 之所以独一无二,正是因为它的方式。 如果它没有坏,那么不要试图修复它!

对 Go 的设计方式不满意的人应该去使用已经具有这种增加且令人讨厌的复杂性的众多其他语言中的一种。

Go 之所以独一无二,正是因为它的方式。 如果它没有坏,那么请不要试图修复它!

坏了,应该修。

坏了,应该修。

它可能不会像你认为的那样工作——但语言永远不能。 它肯定没有坏掉。 考虑到可用的信息和辩论,然后花时间做出明智和明智的决定始终是最佳选择。 在我看来,由于添加了越来越多的功能来解决越来越多的潜在问题,许多其他语言都受到了影响。 请记住,“不”是暂时的,“是”是永远的。

参与过过去的大问题,我建议在 Gopher Slack 上为那些想要讨论这个问题的人打开一个频道,这个问题被暂时锁定,然后为任何想要巩固问题的人发布解冻问题的时间来自 Slack 的讨论? 一旦可怕的“478 个隐藏项目加载更多…”链接出现,Github 问题不再作为论坛工作。

我可以建议在 Gopher Slack 上为想要讨论这个问题的人打开一个频道吗
邮件列表更好,因为它们提供了可搜索的存档。 仍然可以在此问题上发布摘要。

参与过过去的大型问题,我建议在 Gopher Slack 上为那些想要讨论这个问题的人开设一个频道

请不要将讨论完全转移到封闭的平台上。 如果在任何地方,所有人都可以使用 golang-nuts(ish?我不知道实际上没有 Google 帐户是否也可以使用,但至少它是每个人都拥有或可以获得的标准通信方式)并且应该将它移到那里. GitHub 已经够糟糕了,但我不情愿地接受我们坚持使用它进行交流,并不是每个人都能获得 Slack 帐户或使用他们糟糕的客户。

不是每个人都能获得 Slack 帐户或使用他们糟糕的客户

这里的“可以”是什么意思? Slack 是否存在我不知道的真正限制,或者人们只是不喜欢使用它? 后者很好,我想,但有些人也抵制 Github,因为他们不喜欢微软,所以你失去了一些人,但得到了一些人。

不是每个人都能获得 Slack 帐户或使用他们糟糕的客户

这里的“可以”是什么意思? Slack 是否存在我不知道的真正限制,或者人们只是不喜欢使用它? 后者很好,我想,但有些人也抵制 Github,因为他们不喜欢微软,所以你失去了一些人,但得到了一些人。

Slack 是一家美国公司,因此将遵循美国实施的任何外交政策。

Github 也有同样的问题,并且只是在没有任何警告的情况下踢出伊朗人的新闻。 不幸的是,除非我们使用 Tor 或 IPFS 或其他东西,否则我们必须在任何实际讨论论坛中尊重美国/欧洲法律。

Github 也有同样的问题,并且只是在没有任何警告的情况下踢出伊朗人的新闻。 不幸的是,除非我们使用 Tor 或 IPFS 或其他东西,否则我们必须在任何实际讨论论坛中尊重美国/欧洲法律。

是的,我们被 GitHub 和 Google Groups 困住了。 我们不要在列表中添加更多有问题的服务。 聊天也不是一个好的存档; 当这些讨论的线程很好并且在 golang-nuts 上(它们直接进入您的收件箱)时,很难深入挖掘这些讨论。 Slack 意味着如果你和其他人不在同一个时区,你必须翻阅大量的聊天档案,一个非 sequers 等。邮件列表意味着你至少在某种程度上把它组织成线程,人们倾向于接受他们的回复时间更长,这样您就不会随意留下大量随机的 1-off 评论。 此外,我只是没有 Slack 帐户,他们愚蠢的客户无法在我使用的任何机器上工作。 另一方面,Mutt(或您选择的电子邮件客户端,是的标准)在任何地方都可以使用。

请保留这个关于泛型的问题。 GitHub 问题跟踪器不适合泛型等大规模讨论这一事实值得讨论,但不是在这个问题上。 我已将上面的几条评论标记为“离题”。

关于 Go 的独特性:Go 有一些不错的功能,但并不像某些人想象的那么独特。 作为两个例子,CLU 和 Modula-3 具有相似的目标和相似的回报,并且都以某种形式支持仿制药(CLU 的情况是从 1975 年开始!)它们目前没有工业支持,但 FWIW,有可能获得编译器为他们两个工作。

对语法的几个查询,是否需要类型参数中的type关键字? 像其他语言一样,对类型参数采用<>会更有意义吗? 这可能会使事情更具可读性和熟悉性...

虽然我不反对提案中的方式,但只是提出来考虑

代替:

type Vector(type Element) []Element
var v Vector(int)
func (v *Vector(Element)) Push(x Element) { *v = append(*v, x) }
type VectorInt = Vector(int)

我们可以有

type Vector<Element> []Element
var v Vector<int>
func (v *Vector<Element>) Push(x Element) { *v = append(*v, x) }
type VectorInt = Vector<int>

草案中提到了<>语法, @jnericks (你的用户名非常适合这个讨论......)。 反对它的主要论点是它大大增加了解析器的复杂性。 更一般地说,它使 Go 成为一种难以解析的语言,但几乎没有什么好处。 大多数人都同意它确实提高了可读性,但对于是否值得权衡存在分歧。 就个人而言,我认为不是。

type关键字的使用是消除歧义所必需的。 否则很难区分func Example(T)(arg int) {}func Example(arg int) (int) {}

我通读了关于 go generics 的最新提案。 除了合同声明语法外,其他都符合我的口味。

正如我们所知,在 go 中我们总是像这样声明结构或接口:

type MyStruct struct {
        a int
        s string
}

type MyInterface inteface {
    Method1() err
    Method2() string
}

但最新提案中的合同声明是这样的:

contract Ordered(T) {
    T int, int8
}

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

在我看来,契约语法在形式上与传统方法不一致。 下面的语法怎么样:

type Ordered(T) contract {
    T int, int8
}

if there is only one type parameter, the declaration above can be also wrote like this:

type Ordered contract {
    int , int8
}


if there are more than one type parameter, we have to use named parameter:

type G(Node, Edge) contract {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

现在合同的形式与传统是一致的。 我们可以使用 struct, interface 在类型块中声明合约:

type (
        Sequence contract {
                string, []byte
        }

    Stringer(T) contract {
        T String() string
    }

    Stringer contract { // equivalent with the above Stringer(T), single type parameter could be omitted
        String() string
    }

        MyStruct struct {
                a int
                b string
        }

    G(Node, Edge) contract {
        Node Edges() []Edge
        Edge Nodes() (from Node, to Node)
    }
)

于是“契约”就变成了和struct、interface同级的关键字。 不同之处在于合约用于声明类型的元类型。

@bigwhite我们仍在讨论这个符号。 支持设计草案中建议的符号的论点是合约不是类型(例如,不能声明合约类型的变量),因此合约是一种新的实体,与常量一样徒劳无功、函数、变量或类型。 支持您的建议的论点是合同只是一种“类型类型”(或元类型),因此应遵循一致的表示法。 支持您的建议的另一个论点是,它将允许使用“匿名”合同文字而无需明确声明它们。 总之,恕我直言,这还没有解决。 但也很容易改变。

FWIW, CL 187317 目前支持这两种表示法(尽管合约参数必须与合约一起写入),例如:

type C contract(X) { ... }

contract C (X) { ... }

在内部以相同的方式被接受和表示。 更一致的方法是:

type C(type X) contract { ... }

合同不是一种类型。 它甚至不是元类型,因为它的唯一类型
关心的是它的参数。 没有单独的接收器类型
合同可以被认为是元类型。

Go 也有函数声明:

func Name(args) { body }

提议的合同语法更直接地反映了这一点。

无论如何,这些类型的语法讨论在优先级列表中似乎很低
这点。 更重要的是看草案的语义和
它们如何影响代码,基于这些可以编写什么样的代码
语义,什么代码不能。

编辑:关于内联合约,Go 有函数文字。 我看不出有任何理由不能有合同文字。 因为它们不是类型或值,所以它们可以出现的位置数量会更有限。

@stevenblenkinsop我不会说事实上的合同不是一种类型(或元类型)。 我认为这两种观点都有非常合理的论据。 例如,仅指定方法的单个参数协定本质上充当类型参数的“上限”:任何有效的类型参数都必须实现这些方法。 这就是我们通常使用接口的目的。 在这些情况下允许接口而不是合同可能很有意义,a)因为这些情况可能很常见; b) 因为在这种情况下满足合同仅仅意味着满足作为合同阐明的接口。 也就是说,这样的合约的行为非常类似于与另一种类型“比较”的类型。

@griesemer将合同视为类型可能会导致 Russel 悖论出现问题(就像所有类型的类型都不是它们自己的“成员”一样)。 我认为最好将它们视为“类型的约束”。 如果我们认为类型系统是一种“逻辑”形式,我们可以在 Prolog 中对其进行原型设计。 类型变量变成逻辑变量,类型变成原子,契约/约束可以通过约束逻辑编程来解决。 这一切都非常整洁且不自相矛盾。 在语法方面,我们可以将合约视为返回布尔值的类型的函数。

@keean任何接口都已经用作“类型的约束”,但它们是类型。 类型论的人们非常以一种非常正式的方式将类型的约束视为类型。 正如我上面提到的,对于任何一种观点都可以提出合理的论据。 这里没有“逻辑悖论”——事实上,当前正在进行的原型在内部将合约建模为一种类型,因为它目前简化了问题。

Go 中的@griesemer接口是“子类型”,而不是对类型的限制。 然而,我确实发现对合约和接口的需求对 Go 的设计不利,但是将接口更改为类型约束而不是子类型可能为时已晚。 我在上面论证过 Go 接口不一定是子类型,但我没有看到很多人支持这个想法。 这将允许接口和合同是同一件事——如果接口也可以为操作员声明的话。

这里有悖论,所以要小心,吉拉德悖论是罗素悖论最常见的“编码”类型理论。 类型理论引入了全域的概念来防止这些悖论,并且您只能从全域“U+1”引用全域“U”中的类型。 在内部,这些类型理论被实现为高阶逻辑(例如 Elf 使用 lambda-prolog)。 这反过来又减少了对高阶逻辑的可判定子集的约束求解。

因此,虽然您可以将它们视为类型,但您需要添加一组使用限制(句法或其他),以有效地让您回到对类型的约束。 我个人发现直接使用约束更容易,并且避免了两个进一步的抽象层,高阶逻辑和依赖类型。 这些抽象没有增加类型系统的表达能力,并且需要进一步的规则或限制来防止悖论。

对于当前将约束视为类型的原型,如果您可以将此“约束类型”用作普通类型,然后在该类型上构造另一个“约束类型”,则会出现危险。 您将需要检查以防止自引用(这通常是微不足道的)和相互引用循环。 这种原型真的应该用 Prolog 编写,因为它可以让您专注于实现规则。 我相信 Rust 开发人员不久前终于意识到了这一点(参见 Chalk)。

@griesemer有趣,将合同重新建模为类型。 根据我自己的心智模型,我会将约束视为元类型,将契约视为一种类型级别的结构。

type A int
func (a A) Foo() int {
    return int(a)
}

type C contract(T, U) {
    T int
    U int, uint
    U Foo() int
}

var B (int, uint; Foo() int).type = A
var C1 C = C(A, B)

这向我表明,合同的当前类型声明样式语法是两者中更正确的一种。 不过,我认为草案中列出的语法仍然更好,因为它不需要解决“如果它是一种类型,它的值是什么样子”的问题。

@stevenblenkinsop你把我弄丢了,为什么在T不使用的时候将它传递给C contract ,而var行试图做什么?

@griesemer感谢您的回复。 Go 的设计原则之一是“只提供一种方法来做某事”。 最好只保留一份合同申报表。 type C(type X) contract { ... } 更好。

@Goodwine我已重命名类型以将它们与合同参数区分开来。 也许这有帮助? (int, uint; Foo() int).type旨在成为具有intuint基础类型并实现Foo() int的任何类型的元类型。 var B旨在显示使用类型作为值,并将其分配给类型为元类型的变量(因为元类型就像其值是类型的类型)。 var C1旨在显示类型为合约的变量,并显示可能分配给此类变量的示例。 基本上,试图回答“如果合同是一种类型,它的值是什么样的?”这个问题。 关键是要表明该值本身似乎不是一种类型。

我遇到了多种类型的合同问题。

您可以为类型参数合同添加或保留它,两者
type Graph (type Node, Edge) struct { ... }

type Graph (type Node, Edge G) struct { ... }没问题。

但是,如果我只想在两个类型参数之一上添加合同怎么办?

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

VS

contract G(Edge) {
    Edge Nodes() (from Node, to Node)
}

@themez这在草稿中。 例如,您可以使用语法(type T, U comparable(T))来仅约束一个类型参数。

@stevenblenkinsop我明白了,谢谢。

@themez这已经出现了好几次了。 我认为使用看起来像变量定义的类型这一事实存在一些混淆。 事实并非如此; 合约更多的是整个函数的细节,而不是参数定义。 我认为假设您基本上会为您创建的每个通用函数/类型编写一个新合约,可能由其他合约组成以帮助重复。 像@stevenblenkinsop提到的东西确实可以捕捉到这种假设没有意义的边缘情况。

至少,这是我得到的印象,尤其是因为它们被称为“合同”。

@keean我认为我们对“约束”一词的解释不同; 我使用它相当非正式。 根据接口的定义,给定一个接口I和一个x I ,只有实现I的类型的值才能分配给x 。 因此, I可以被视为对这些类型的“约束”(当然仍有无限多的类型满足该“约束”)。 类似地,可以使用I作为泛型函数的类型参数P的约束; 仅允许具有实现I的方法集的实际类型参数。 因此I也限制了可能的实际参数类型的集合。

在这两种情况下,这样做的原因都是为了描述函数内部的可用操作(方法)。 如果I用作(值)参数的类型,我们知道该参数提供了这些方法。 如果I us 用作“约束”(代替合同),我们知道所有受约束的类型参数的值都提供了这些方法。 这显然很简单。

我想要一个具体的例子来说明为什么这种将接口用于单参数合同的具体想法只声明方法“崩溃”而没有您在评论中提到的一些限制。

如何引入合同提案? 使用 go modules go1.14参数? GO114CONTRACTS环境变量? 两个都? 还有什么..?

抱歉,如果之前已解决此问题,请随时将我重定向到那里。

我特别喜欢当前仿制药草案设计的一件事是它在contractsinterfaces之间放置了清水。 我觉得这很重要,因为这两个概念很容易混淆,尽管它们之间存在三个基本区别:

  1. Contracts描述了_set_ 类型的要求,而interfaces描述了_single_ 类型必须满足它的方法。

  2. Contracts可以通过列出支持它们的类型来处理内置操作、转换等; interfaces只能处理内置类型本身没有的方法。

  3. 无论它们在类型理论方面是什么, contracts都不是我们通常认为的 Go 中的类型,即您不能声明contract类型的变量并赋予它们一些值。 另一方面, interfaces是类型,您可以声明这些类型的变量并为它们分配适当的值。

尽管我可以看到contract的意义,它需要单个类型参数才能具有某些方法,但可以用interface来表示(这是我过去甚至提倡的建议),我现在觉得这将是一个不幸的举动,因为它会再次混淆contractsinterfaces之间的水域。

contracts可以按照@bigwhite建议使用现有“类型”模式的方式合理声明之前,我并没有真正想到。 然而,我再次对这个想法不感兴趣,因为我觉得它会妥协上面的(3)。 此外,如果有必要(出于解析原因)在声明通用结构时重复type关键字,如下所示:

type List(type Element) struct {
    next *List(Element)
    val  Element
}

如果contracts以与草案设计方法相比有点“口吃”的类似方式声明,大概也有必要重复它。

我不喜欢的另一个想法是“合同文字”,它允许将contracts写入“就地”而不是单独的构造。 这将使泛型函数和类型定义更难阅读,并且正如一些人认为的那样,它不会帮助说服那些人泛型是一件好事。

很抱歉,对泛型草案的修改提议如此抗拒(诚然存在一些问题),但作为 Go 简单泛型的热心倡导者,我觉得这些观点值得提出。

我想建议不要在“合同”类型上调用谓词。 有两个原因:

  • 术语“合同”已经在计算机科学中以不同的方式使用。 例如,参见:(https://scholar.google.com/scholar?hl=en&as_sdt=0%2C33&q=contracts+languages&btnG=)
  • 在计算机科学文献中,这个想法已经有多个名称。 我知道至少〜三个〜四个:“排版”,“类型类”,“概念”和“约束”。 添加另一个只会进一步混淆问题。

@griesemer “类型约束”纯粹是编译时的事情,因为类型在运行前被删除。 约束导致通用代码被详细说明为可以执行的非通用代码。 子类型在运行时存在,并且在对类型的约束至少是类型相等或类型不相等的意义上不是约束,根据类型系统,诸如“是子类型”之类的约束可选地可用。

对我来说,子类型的运行时性质是关键的区别,如果 X <: Y 我们可以在预期 Y 的地方传递 X,但我们只知道类型为 Y 而没有不安全的运行时操作。 从这个意义上说,它不限制 Y 类型,Y 始终是 Y。子类型也是“定向的”,因此可以是协变的或逆变的,具体取决于它是应用于输入参数还是输出参数。

使用类型约束“pred(X)”,我们从完全多态的 X 开始,然后约束允许的值。 所以只说实现'打印'的X。 这是无方向的,因此没有协变或逆变。 它实际上是不变的,因为我们在编译时就知道 X 的基本类型。

因此,我认为将接口视为对类型的约束是危险的,因为它忽略了协变和逆变等重要差异。

这是否回答了您的问题,还是我错过了重点?

编辑:我应该指出,我在上面专门指的是“Go”接口。 关于子类型的要点适用于所有具有子类型的语言,但 Go 在将接口设为类型并因此具有子类型关系方面是不寻常的。 在 Java 等其他语言中,接口明确不是类型(类是类型),因此接口_是_对类型的约束。 因此,虽然将接口视为类型的约束通常是正确的,但对于“Go”来说,这是错误的。

@Inuart现在说这将如何添加到实现中还为时过早。 目前还没有提案,只是一个设计草案。 它肯定不会出现在 1.14 中。

@andrewcmyers我喜欢“合同”这个词,因为它描述了通用函数的编写者与其调用者之间的关系。

像“typesets”和“type classes”这样的词表明我们在谈论元类型,当然我们是元类型,但契约也描述了多种类型之间的关系。 我知道,例如 Haskell 中的类型类可以有多个类型参数,但在我看来,这个名称不适合所描述的想法。

我一直不明白为什么 C++ 将其称为“概念”。 那有什么意思?

“约束”或“约束”对我来说没问题。 目前,我认为合同包含多个约束。 但我们可以改变这种想法。

我不太担心存在一种称为“合同”的现有编程语言结构。 我认为这个想法与我们想要表达的想法相对相似,因为它是函数与其调用者之间的关系。 我知道表达这种关系的方式是完全不同的,但我觉得有一个潜在的相似之处。

我一直不明白为什么 C++ 将其称为“概念”。 那有什么意思?

概念是共享一些共性的实例的抽象,例如签名。

到目前为止,概念这个术语​​更适合接口,因为后者也用于表示两个组件之间的共享边界。

@sighoya我还要提到“概念”是概念性的,因为它们包括对防止滥用操作员至关重要的“公理”。 例如,加法“+”应该是关联的和可交换的。 这些公理不能用 C++ 表示,因此它们作为抽象概念存在,因此是“概念”。 所以一个概念是句法“契约”加上语义公理。

@ianlancetaylor “约束”是我们在 Genus 中所说的(http://www.cs.cornell.edu/~yizhou/papers/genus-pldi2015.pdf),所以我偏爱这个术语。 术语“契约”将是一个完全合理的选择,除了它在 PL 社区中非常活跃地用于指代接口和实现之间的关系,这也具有契约的味道。

@keean如果不是专家,我认为您所描绘的二分法并不能很好地反映现实。 例如,编译器是否生成泛型函数的实例化版本完全是一个实现问题,因此具有约束的运行时表示是完全合理的,例如以每个所需操作的函数指针表的形式。 实际上,就像接口方法表一样。 同样,Go 中的接口不适合您的子类型定义,因为您可以安全地将它们向下投影(通过类型断言) ,并且因为您在 Go 中的任何类型构造函数都没有协变或逆变。

最后:无论您绘制的二分法是否真实和准确,都不会改变界面归根结底只是一个方法列表-即使在您的二分法中,也没有理由使该列表可以' 不能被重新用作运行时表示的表仅编译的约束,具体取决于它所使用的上下文。

怎么样:

类型约束 C(T) {
}

或者

类型合同 C(T) {
}

与其他类型声明不同,它强调这不是运行时构造。

关于新合同设计,我有一些问题。

1.

当一个泛型类型 A 嵌入另一个泛型类型 B 时,
或者一个泛型函数 A 调用另一个泛型函数 B,
我们还需要在 A 上指定 B 的合同吗?

如果答案是真的,那么如果一个泛型类型嵌入了许多其他泛型类型,
或者一个泛型函数调用许多其他泛型函数,
那么我们需要将多个合约合二为一,作为嵌入类型或调用函数的合约。
这可能会导致类似 const 中毒的问题。

  1. 除了当前的类型种类和方法集约束之外,我们还需要其他约束吗?
    例如从一种类型转换为另一种类型,从一种类型分配给另一种类型,
    两种类型的可比性,是可发送通道,是可接收通道,
    有一个指定的字段集,...

3.

如果一个泛型函数使用如下一行

v.Foo()

我们如何编写允许Foo是函数类型的方法或字段的合约?

@merovius类型约束必须在编译时解析,否则类型系统可能不健全。 这是因为您可以拥有一个依赖于另一个直到运行时才知道的类型。 然后你有两个选择,你必须实现一个完全依赖的类型系统(它允许类型检查在运行时发生,因为类型变得已知)或者你必须将存在类型添加到类型系统中。 Existentials 对静态已知类型和仅在运行时已知的类型(例如依赖于从 IO 读取的类型)的相位差进行编码。

如上所述的子类型通常直到运行时才知道,尽管许多语言在静态已知类型的情况下进行了优化。

如果我们假设上述更改之一是引入了语言(依赖类型或存在类型),那么我们仍然需要区分子类型和类型约束的概念。 对于 Go 特别是类型限制器是不变的,我们可以忽略这些差异,并且我们可以认为 Go 接口_are_ 对类型的约束(静态)。

因此,我们可以将 Go-interface 视为单个参数合约,其中参数是所有函数/方法的接收者。 那么为什么 Go 既有接口又有合约呢? 在我看来,这似乎是因为 Go 不想允许运算符接口(如“+”),并且因为 Go 没有依赖类型或存在类型。

因此,有两个因素会在类型约束和子类型之间产生真正的区别。 一个是协/逆变,由于类型构造函数的不变性,我们可以在 Go 中忽略它,另一个是需要依赖类型或存在类型来使类型系统具有类型约束,如果类型约束的类型参数存在运行时多态性。

@keean Cool,所以 AIUI 我们至少同意 Go 中的接口可以被视为约束:)

关于其余部分: 以上您声称:

“类型的约束”纯粹是编译时的事情,因为类型在运行前被删除。 约束导致通用代码被详细说明为可以执行的非通用代码。

该声明比您的最新声明更具体,即需要在编译时解决约束。 我想说的是,编译器可以执行该解析(以及所有相同的类型检查),但仍会生成通用代码。 它仍然是合理的,因为类型系统的语义是相同的。 但是约束仍然会有一个运行时表示。 这有点挑剔 - 但这就是为什么我觉得基于运行时与编译时定义这些并不是最好的方法。 它将实现问题混合到关于类型系统的抽象语义的讨论中。

FWIW,我之前曾争论过我更喜欢使用接口来表达约束 - 并且得出的结论是,允许在泛型代码中使用运算符是这样做的主要障碍,因此是引入单独的合同形式的概念。

@keean谢谢,但不,您的回复没有回答我的问题。 请注意,在我的评论中,我描述了一个非常简单的示例,该示例使用接口代替相应的合同/“约束”。 我要求提供一个_简单_ _concrete_ 示例,为什么这种情况不会像您在之前的评论中提到的那样“在没有一些限制的情况下”起作用。 你没有提供这样的例子。

请注意,我没有提到子类型、协方差或逆变(无论如何我们在 Go 中不允许,签名必须始终匹配)等。相反,我一直在使用基本的和已建立的 Go 术语(接口、实现、类型参数等)来解释我所说的“约束”是什么意思,因为这是这里每个人都理解的通用语言,所以每个人都可以跟随。 (此外,与您在此处的声明相反,在 Java 中,根据Java 规范,接口对我来说看起来像是一种类型:“接口声明指定了一个新的命名引用类型”。如果这没有说接口是一种类型,那么Java Spec 的人有一些工作要做。)

但看起来你用你的最新评论间接回答了我的问题,正如@Merovius已经观察到的那样,当你说:“因此,我们可以将 Go 接口视为单个参数合同,其中参数是所有函数/方法的接收者."。 这正是我一开始就提出的观点,所以感谢您确认我一直在说的话。

@dotaheor

当一个泛型 A 嵌入另一个泛型 B,或者一个泛型函数 A 调用另一个泛型函数 B 时,我们是否还需要在 A 上指定 B 的契约?

如果一个泛型类型 A 嵌入了另一个泛型类型 B,那么传递给 B 的类型参数必须满足 B 使用的任何协定。为此,A 使用的协定必须隐含 B 使用的协定。也就是说,所有约束传递给 B 的类型参数必须在 A 使用的合约中表示。这也适用于泛型函数调用另一个泛型函数时。

如果答案是真的,那么如果一个泛型类型嵌入了许多其他泛型类型,或者一个泛型函数调用了许多其他泛型函数,那么我们需要将许多契约合并为一个作为嵌入类型或调用函数的契约。 这可能会导致类似 const 中毒的问题。

我认为你说的是​​真的,但这不是 const 中毒问题。 const-poisoning 问题是您必须在传递参数的任何地方传播const ,然后如果您发现某个必须更改参数的地方,您必须在所有地方删除const 。 泛型的情况更像是“如果您调用多个函数,则必须将正确类型的值传递给每个函数。”

无论如何,在我看来,人们极不可能编写通用函数来调用许多其他通用函数,这些函数都使用不同的合约。 那怎么会自然发生呢?

除了当前的类型种类和方法集约束之外,我们还需要其他约束吗? 例如可从一种类型转换为另一种类型,可从一种类型分配给另一种类型,两种类型之间的可比性,是可发送通道,是可接收通道,具有指定的字段集,...

正如设计草案所解释的那样,可转换性、可分配性和可比性等约束以类型的形式表示。 正如设计稿所解释的那样,可发送或可接收通道等约束只能以chan T的形式表示,其中T是一些类型参数。 无法表达类型具有指定字段集的约束,但我怀疑这种情况会经常出现。 我们将不得不通过编写真正的代码来看看它是如何工作的,看看会发生什么。

如果一个泛型函数使用如下一行

v.Foo()
我们如何编写允许 Foo 是函数类型的方法或字段的合约?

在当前的设计草案中,你不能。 这看起来像是一个重要的用例吗? (我知道之前的设计草案确实支持这一点。)

@griesemer您错过了我所说的仅当您将依赖类型或存在类型引入类型系统时才有效的观点。

否则,如果您使用契约作为接口,您可能会在运行时失败,因为您需要将类型检查推迟到知道类型之后,并且类型检查可能会失败,因此这不是类型安全的。

我还看到将接口解释为子类型,因此您必须小心,以后不要有人尝试将协/逆变换引入类型构造函数。 最好不要将接口作为类型,那么就没有这种可能性,并且设计者的意图是明确的,即这些不是子类型。

对我来说,合并接口和契约是一个更好的设计,并使它们显式地类型约束(类型的谓词)。

@ianlancetaylor

无论如何,在我看来,人们极不可能编写通用函数来调用许多其他通用函数,这些函数都使用不同的合约。 那怎么会自然发生呢?

为什么会不寻常? 如果我在“T”类型上定义一个函数,那么我将要在“T”上调用函数。 例如,如果我通过合同在“可添加类型”上定义一个“求和”函数。 现在我想构建一个调用 sum 的通用乘法函数? 编程中的许多事物都有一个总和/乘积结构(任何“组”)。

我不明白合约在语言上之后接口的目的是什么,看起来合约将用于相同的目的,以确保类型上有一组定义的方法。

@keean不寻常的情况是调用许多其他泛型函数的函数,这些函数都使用不同的合约。 您的反例仅调用一个函数。 请记住,我反对与 const-poisoning 的相似性。

@mrkaspa最简单的思考方式是,合约就像 C++ 模板函数,而接口就像 C++ 虚拟方法。 两者都有用途和目的。

@ianlancetaylor根据经验,出现了两个类似于 const 中毒的问题。 两者都是由于嵌套函数调用的树状性质而发生的。 第一个是当你想为一个深度嵌套的函数添加调试时,你必须添加从叶子一直到根的 printable,这可能涉及到多个第三方库。 二是可以在根部积累大量合约,导致函数签名难以阅读。 通常最好让编译器推断约束,就像 Haskell 对类型类所做的那样,以避免这两个问题。

@ianlancetaylor我对 c++ 不太了解,golang 中的接口和合同的用例是什么? 我什么时候应该使用接口或合同?

@keean这个子线程是关于 Go 语言的特定设计草案。 在 Go 中,所有值都是可打印的。 这不是需要在合同中表达的东西。 虽然我愿意看到许多合同可以为单个通用函数或类型积累的证据,但我不愿意接受这种断言会发生。 设计草案的重点是尝试编写使用它的真实代码。

设计草案尽可能清楚地解释了为什么我认为推断约束对于像 Go 这样专为大规模编程而设计的语言来说是一个糟糕的选择。

@mrkaspa例如,如果你有一个[]io.Reader那么你想要一个接口值,而不是一个合同。 合约要求切片中的所有元素都是同一类型。 只要所有类型都实现io.Reader ,接口将允许它们是不同的类型。

@ianlancetaylor据我所知,接口创建了一个新类型,同时合同约束了一个类型,但没有创建一个新类型,对吗?

@ianlancetaylor

你不能做类似下面的事情吗?

contract Reader(T) {
  T Read([]byte) (int, error)
}

func ReadAll(type T Reader)(readers []T) ([]byte, error) {
  // Use the readers...
}

现在ReadAll()应该像接受[]io.Reader一样接受[]*os.File ,不是吗? io.Reader似乎确实满足合同,我不记得草案中关于接口值不能用作类型参数的任何内容。

编辑:没关系。 我误解了。 这仍然是您使用界面的地方,因此它是对@mrkaspa问题的回答。 您只是没有在函数签名中使用接口; 你只在它被调用的地方使用它。

@mrkaspa是的,这是真的。

@ianlancetaylor如果我有 []io.Reader 和这份合同的清单:

contract Reader(T) {
  T Read([]byte) (int, error)
}

func ReadAll(type T Reader)(readers []T) ([]byte, error) {
  // Use the readers...
}

我可以通过每个接口调用 ReadAll,因为它们满足合同?

@ianlancetaylor确定事情是可打印的,但是很容易想出其他示例,例如记录到文件或网络,我们希望记录是通用的,因此我们可以在 null、本地文件、网络服务等之间更改日志目标。添加记录到叶函数需要将约束一直添加到根目录,包括必须修改使用的第三方库。

代码不是静态的,您也必须允许维护。 事实上,代码在“维护”中的时间比最初编写的时间要长得多,因此有一个很好的论点是我们应该设计语言以使维护、重构、添加功能等更容易。

真的,这些问题只会在大型代码库中体现出来,并且随着时间的推移会得到维护。 这不是你可以写一个快速的小例子来演示的东西。

这些问题也存在于其他通用语言中,例如 Ada。 您可以移植一些广泛使用泛型的 Go 大型 Ada 应用程序,但如果 Ada 中存在问题,我在 Go 中看不到任何可以缓解该问题的东西。

@mrkaspa是的。

在这一点上,我建议这个对话线程应该转移到 golang-nuts。 GitHub 问题跟踪器不适合这种讨论。

@keean也许你是对的。 时间会证明一切。 我们明确要求人们尝试为设计草案编写代码。 纯粹假设性的讨论没有什么价值。

@keean我不明白您的日志记录示例。 您描述的问题是您可以在运行时使用接口解决的问题,而不是在编译时使用泛型解决的问题。

@bserdar接口只有一个类型参数,所以你不能做一些事情,其中​​一个参数是要记录的东西,第二个类型参数是日志的类型。

@keean IMO 在该示例中,您将执行与今天相同的操作,根本没有任何类型参数:使用反射检查要记录的内容并使用context.Context传递日志的值。 我知道这些想法会让打字爱好者反感,但事实证明它们非常实用。 当然,受约束的类型参数是有价值的,这就是我们进行这次对话的原因——但我认为,你想到的案例是在当前的 Go 代码库中大规模运行的案例的原因,那些不是真正受益于额外严格类型检查的情况。 这又回到了伊恩斯的观点——这是否是一个在实践中表现出来的问题还有待观察。

@merovius如果由我决定,所有运行时反射都将被禁止,因为我不希望发布的软件在运行时生成可能影响用户的打字错误。 这允许更积极的编译器优化,因为您不必担心运行时模型与静态模型对齐。

在处理了从 JavaScript 到 TypeScript 的大规模项目迁移之后,根据我的经验,严格的类型化变得越重要,项目越大,团队规模越大。 这是因为在与大型团队合作时,您需要依赖代码块的接口/契约,而无需查看实现以保持效率。

旁白:当然,这取决于您如何实现规模化,现在我更喜欢 API-First 方法,从 OpenAPI/Swagger JSON 文件开始,然后使用代码生成来构建服务器存根和客户端 SDK。 因此,OpenAPI 实际上充当了微服务的类型系统。

@ianlancetaylor

可转换性、可分配性和可比性等约束以类型的形式表示

考虑到 Go 类型转换规则有这么多细节,要写一个自定义合约C来满足下面的通用切片转换函数真的很难:

func ConvertSlice(type In, Out C(In, Out)) (x []In) []Out {
    o := make([]Out, len(x))
    for i := range x {
        o[i] = Out(x[i])
    }
    return o
}

完美的C应该允许转换:

  • 在任何整数、浮点数值类型之间
  • 在任何复数类型之间
  • 基础类型相同的两种类型之间
  • 来自实现In的类型Out #$
  • 从通道类型到双向通道类型,并且两种通道类型具有相同的元素类型
  • 结构标签相关,...
  • ...

根据我的理解,我不能写这样的合同。 那么我们需要一个内置的convertible合约吗?

没有办法表达类型具有指定字段集的约束,但我怀疑这会经常出现

考虑到 Go 编程中经常使用类型嵌入,我认为这种需求并不罕见。

@keean这是一个有效的观点,但显然不是指导 Go 的设计和开发的观点。 要建设性地参与,请接受这一点并从我们所处的位置开始工作,并假设语言的任何发展都必须从现状逐渐改变。 如果你不能,那么有些语言更符合你的喜好,我觉得如果你在那里贡献你的能量,每个人——尤其是你——都会更快乐。

@merovius我准备接受 Go 的变化必须是渐进的,并接受现状。

作为对话的一部分,我只是在回复您的评论,同意我是打字爱好者。 我对运行时反射发表了看法,我并不建议 Go 应该放弃运行时反射。 我确实使用其他语言,在我的工作中使用多种语言。 我正在(缓慢地)开发我自己的语言,但我总是希望其他语言的发展将使这变得不必要。

@dotaheor我同意我们今天不能编写可兑换的一般合同。 我们将不得不看看这在实践中是否似乎是一个问题。

回复@ianlancetaylor

我认为目前还不清楚人们希望在常量值上参数化函数的频率。 最明显的情况是数组维度——但您已经可以通过将所需的数组类型作为类型参数传递来做到这一点。 除了这种情况,通过将 const 作为编译时参数而不是运行时参数传递,我们真正获得了什么?

在数组的情况下,仅将(整个)数组类型作为类型参数传递似乎是非常有限的,因为合约将无法分解数组维度或元素类型并对其施加约束。 例如,采用“整个数组类型”的合约是否需要数组类型的元素类型来实现某些方法?

但是您对非类型泛型参数如何有用的更具体示例的呼吁得到了很好的采纳,因此我扩展了博客文章以包含一个部分,其中涵盖了几个重要的示例用例类别和每个类别的一些具体示例。 几天过去了,又来了一篇博文:

对于 Go 2 泛型来说,只有类型参数泛型就足够了吗?

新部分的标题为“非类型的泛型有用的示例方法”。

作为一个快速的总结,矩阵和向量操作的契约可以对数组的维数和元素类型施加适当的约束。 例如,nxm 矩阵与 mxp 矩阵的矩阵乘法,每个都表示为一个二维数组,可以正确地将第一个矩阵的行数限制为等于第二个矩阵的列数,等等。

更一般地说,泛型可以使用非类型参数以多种方式启用编译时配置和代码和算法的专业化。 例如,math/big.Int 的通用变体可以在编译时配置为具有和/或有符号的特定位,从而以合理的效率满足对 128 位整数和其他非本机固定宽度整数的需求可能要好得多比现有的 big.Int 一切都是动态的。 big.Float 的泛型变体在编译时可能同样可特化为特定精度和/或其他编译时参数,例如,提供来自 IEEE 754-2008 的 binary16、binary128 和 binary256 格式的合理高效的泛型实现Go 本身不支持。 许多库算法可以根据对用户需求或正在处理的数据的特定方面的了解来优化其操作 - 例如,仅适用于非负边权重或仅适用于 DAG 或树的图算法优化,或矩阵处理优化依赖于矩阵是上三角或下三角,或密码学的大整数算法,有时需要在恒定时间内实现,有时不需要- 可以使用泛型使自己在编译时可配置,以依赖于可选的声明性信息,例如这一点,同时确保实现中这些编译时选项的所有测试通常通过常量传播编译出来。

@bford写道:

即泛型的参数在编译时绑定到常量。

这是我不明白的一点。 为什么你需要这个条件。
从理论上讲,可以重新定义体内的变量/参数。 没关系。
直观地说,我假设您想声明第一个函数应用程序必须在编译时发生。

但是对于这个要求,像 comp 或 comptime 这样的关键字会更合适。
此外,如果 golang 的语法最多只允许一个函数使用两个参数元组,那么这个关键字注释可以省略,因为类型和函数的第一个参数元组(在两个参数元组的情况下)将始终被评估在编译时。

另一点:如果 const 被扩展以允许运行时表达式(真正的单点登录)怎么办?

关于指针与值方法

如果在合约中列出了一个简单的方法T而不是*T ,那么它可能是指针方法或T的值方法。 为了避免担心这种区别,在泛型函数体中,所有方法调用都将是指针方法调用。 ...

这个怎么搭配接口实现呢? 如果T有一些指针方法(如示例中的MyInt ),可以将T分配给具有该方法的接口( Stringer在例子)?

允许它意味着有另一个隐藏地址操作& ,不允许它意味着合约和接口只能通过显式类型切换进行交互。 这两种解决方案对我来说似乎都不好。

(注意:如果导致混淆或代码不正确,我们应该重新考虑这个决定。)

我看到团队已经对指针方法语法中的这种歧义有所保留。 我只是补充一点,歧义也会影响接口实现(并隐含地添加我对它的保留)。

@fJavierZunzunegui没错,当前文本确实暗示将类型参数的值分配给接口类型时,可能需要隐式地址操作。 这可能是调用方法时不使用隐式地址的另一个原因。 我们得看看。

Parameterized types上,特别是关于嵌入为结构中的字段的类型参数:

考虑

type Lockable(type T) struct {
    T
    sync.Locker
}

如果T有一个名为LockUnlock的方法怎么办? 该结构不会编译。 合约不支持没有方法 X 的条件,因此我们有不违反合约的无效代码(违背合约的全部目的)。

如果您有多个嵌入式参数(例如T1T2 ),它会变得更加复杂,因为它们必须没有通用方法(同样,不是由合同强制执行的)。 此外,根据嵌入类型支持任意方法会导致对这些结构的类型开关的编译时间限制非常有限(与类型断言和开关非常相似)。

在我看来,有两个不错的选择:

  • 完全禁止嵌入类型参数:简单,但成本很低(如果需要该方法,则必须将其显式写入带有字段的结构中)。
  • 将可调用方法限制为合约方法:类似于嵌入接口。 这偏离了正常的 go(非目标),但没有任何成本(方法不需要在带有字段的结构中显式编写)。

该结构不会编译。

它会编译。 试试看。 无法编译的是对歧义方法的调用。 但是,您的观点仍然有效。

您的第二个解决方案,将可调用方法限制为合同中提到的方法,将不起作用:即使T上的合同指定LockUnlock ,您仍然不能不要打电话给他们Lockable

@jba感谢您对编译的见解。

通过第二种解决方案,我的意思是像我们现在处理接口一样处理嵌入类型参数,这样如果该方法不在合同中,则在嵌入后不能立即访问它。 在这种情况下,由于T没有合同,因此它被有效地视为interface{} ,因此即使T被实例化,它也不会与sync.Locker冲突具有这些方法的类型。 这可能有助于解释我的观点

无论哪种方式,我都更喜欢第一个解决方案(完全禁止嵌入),所以如果这是你的偏好,那么讨论第二个解决方案几乎没有意义! :笑脸:

@JavierZunzunegui提供的示例还涵盖了另一种情况。 如果T是具有noCopy noCopy字段的结构怎么办? 编译器也应该能够处理这种情况。

不确定这是否是完全正确的地方,但我想用一个具体的真实世界用例来评论泛型类型,它允许“对非类型值(如常量)进行参数化”,特别是对于数组的情况. 我希望这是有帮助的。

在我没有泛型的世界里,我写了很多看起来像这样的代码:

import "math/bits"

// SigEl is the element type used in variable length bit vectors, 
// can be any unsigned integer type
type SigEl = uint

// SigElBits is the number of bits storable in each SigEl
const SigElBits = 8 << uint((^SigEl(0)>>32&1)+(^SigEl(0)>>16&1)+(^SigEl(0)>>8&1))

// HammingDist counts the number bitwise differences between two
// bit vectors b1 and b2. I want this to be generic
// Function will panic at runtime if b1 and b2 aren't of equal length.
func HammingDist(b1, b2 []SigEl) (sum int) {
    // Give the compiler a hint so it won't need to bounds check the slices in loops
    _ = b1[len(b2)-1]  
        // This switch is optimized away because SigElBits is const
    switch SigElBits {   // Yay no golang generics!
    case 64:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount64(uint64(b1[x] ^ b2[x]))
        }
    case 32:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount32(uint32(b1[x] ^ b2[x]))
        }
    case 16:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount16(uint16(b1[x] ^ b2[x]))
        }
    case 8:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount8(uint8(b1[x] ^ b2[x]))
        }
    }
    return sum
}

这工作得很好,只有一个皱纹。 我经常需要数以亿计的[]SigEl s,它们的长度通常是 128-384 总位。 因为切片在底层数组的大小之上施加了固定的 192 位开销,当数组本身为 384 位或更少时,这会带来不必要的 50-150% 的内存开销,这显然是可怕的。

我的解决方案是分配Sig _arrays_ 的切片,然后将它们作为参数动态切片到上面的HammingDist

const SigBits = 256  // Any multiple of SigElBits is valid

// Sig is the bit vector array type
type Sig [SigBits/SigElBits]SigEl

bitVects := make([]Sig, 100000000)
// stuff happens ... 

// Note slicing below, just to make the arrays "generic" for the call 
dist := HammingDist(bitVects[x][:], bitVects[y][:])

我希望能够做的不是所有这些,而是​​定义一个通用的 Signature 类型并将上述所有内容重写为(类似于):

contract UnsignedInteger(T) {
    T uint, uint8, uint16, uint32, uint64
}

type Signature (type Element UnsignedInteger, n int) [n]Element

// HammingDist counts the number bitwise differences between two bit vectors
func HammingDist(b1, b2 *Signature) (sum int) {
    for x := range *b1 {
        // Assuming the std lib bits.OnesCount becomes generic over 
        // all UnsignedInteger types
        sum += bits.OnesCount(*b1[x] ^ *b2[x])
    }
    return sum
}

那么使用这个库:

type sigEl = uint   // Any unsigned int type
const sigElBits = 8 << uint((^SigEl(0)>>32&1)+(^SigEl(0)>>16&1)+(^SigEl(0)>>8&1))
const sigBits = 256  // Any multiple of SigElBits is valid
type sig Signature(sigEl, sigBits/sigElBits)

bitVects := make([]sig, 100000000)
// stuff happens ... 

dist := HammingDist(&bitVects[x], &bitVects[y])

工程师可以梦想......🤖

如果您知道最大位长度可能有多大,则可以使用以下内容:

contract uintArrayOfFixedLength(ElemType,ArrayType)
{
    ArrayType [1]ElemType,[2]ElemType,...,[maxBit]ElemType
    ElemType uint8,uint16,uint32,uint64
}

func HammingDist(type ElemType,ArrayType uintArrayOfFixedLength)(t1,t2 ArrayType) (sum int)
{

}

@vsivsi我不确定我是否理解您认为这将如何改善事情-您是否可能假设编译器将为每个可能的数组长度生成该函数的实例化版本? 因为 ISTM a) 这不太可能,所以 b) 你最终会得到与现在完全相同的性能特征。 最有可能的实现,IMO,仍然是编译器传递长度和指向第一个元素的指针,所以你实际上仍然会在生成的代码中传递一个切片(我的意思是,你不会传递容量,但我认为堆栈上的附加词并不重要)。

老实说,IMO 你所说的是过度使用泛型的一个很好的例子,在不需要它们地方——“不确定长度的数组”正是切片的用途。

@Merovius谢谢,我认为您的评论揭示了一些有趣的讨论点。

“不确定长度的数组”正是切片的用途。

是的,但在我的示例中,没有不确定长度的数组。 数组长度在_编译时_是一个已知常数。 这正是数组的用途,但它们在 golang IMO 中未被充分利用,因为它们非常不灵活。

说清楚,我不是建议

type Signature (type Element UnsignedInteger, n int) [n]Element

意味着n是一个运行时变量。 在与今天相同的意义上,它必须仍然是一个常数:

const n = 10
type nArray [n]uint               // works
type nSigInt Signature(uint, n)   // works 

var m = int(n)
type mArray [m]uint               // error
type mSigInt Signature(uint, m)   // error 

那么让我们看看基于切片的HammingDist函数的“成本”。 我同意将数组传递为bitVects[x][:]&bitVects[x]之间的差异很小(-ish,最大因子 3)。 真正的区别在于需要在该函数内部进行的代码和运行时检查。

在基于切片的版本中,运行时代码需要对切片访问进行边界检查以确保内存安全。 这意味着这个版本的代码可能会出现恐慌(或者需要明确的错误检查和返回机制来防止这种情况发生)。 NOP 分配 ( _ = b1[len(b2)-1] ) 通过向编译器优化器提示它不需要对循环中的每个切片访问进行边界检查,从而产生有意义的性能差异。 但是这些最小边界检查仍然是必要的,即使传递的底层数组总是相同的长度。 此外,编译器可能难以有利地优化 for/range 循环(例如通过unrolling )。

相比之下,基于通用数组的函数版本不会在运行时出现恐慌(不需要错误处理),并且绕过了对任何条件边界检查逻辑的需求。 我高度怀疑该函数的编译通用版本是否需要按照您的建议“传递”数组长度,因为它实际上是一个常量值,是编译时实例化类型的一部分。

此外,对于小数组维度(在我的情况下很重要),编译器很容易有利地展开甚至完全优化 for/range 循环以获得可观的性能提升,因为它会在编译时知道这些维度是什么.

代码的通用版本的另一大好处是它允许HammingDist模块的用户在他们自己的代码中确定 unsigned int 类型。 非通用版本需要修改模块本身以更改定义的类型SigEl ,因为没有办法将类型“传递”给模块。 这种差异的结果是,当不需要为每个 {8,16,32,64} 位 uint 情况编写单独的代码时,距离函数的实现会变得更简单。

基于切片的函数版本和需要修改库代码以设置元素类型的成本是避免必须实现和维护该函数的“NxM”版本所需的高度次优的让步。 对(常量)参数化数组类型的通用支持可以解决这个问题:

// With generics + parameterized constant array lengths:
type Signature (type Element UnsignedInteger, n int) [n]Element
func HammingDist(b1, b2 *Signature) (sum int) { ... }

// Without generics
func HammingDistL1Uint(b1, b2 [1]uint) (sum int) { ... }
func HammingDistL1Uint8(b1, b2 [1]uint8) (sum int) { ... }
func HammingDistL1Uint16(b1, b2 [1]uint16) (sum int) { ... }
func HammingDistL1Uint32(b1, b2 [1]uint32) (sum int) { ... }
func HammingDistL1Uint64(b1, b2 [1]uint64) (sum int) { ... }

func HammingDistL2Uint(b1, b2 [2]uint) (sum int) { ... }
func HammingDistL2Uint8(b1, b2 [2]uint8) (sum int) { ... }
func HammingDistL2Uint16(b1, b2 [2]uint16) (sum int) { ... }
func HammingDistL2Uint32(b1, b2 [2]uint32) (sum int) { ... }
func HammingDistL2Uint64(b1, b2 [2]uint64) (sum int) { ... }

func HammingDistL3Uint(b1, b2 [3]uint) (sum int) { ... }
func HammingDistL3Uint8(b1, b2 [3]uint8) (sum int) { ... }
func HammingDistL3Uint16(b1, b2 [3]uint16) (sum int) { ... }
func HammingDistL3Uint32(b1, b2 [3]uint32) (sum int) { ... }
func HammingDistL3Uint64(b1, b2 [3]uint64) (sum int) { ... }

// and L4, L5, L6 ... ad nauseum

避免上述噩梦,或者当前替代方案的真正成本对我来说似乎是“通用过度使用”的_相反_。 我同意@sighoya的观点,即枚举合同中所有允许的数组长度可能适用于非常有限的一组案例,但我认为即使对于我的案例来说这也太有限了,因为即使我将支持的上限截止低 384 个总比特,这将需要合同的ArrayType [1]ElemType,[2]ElemType,...,[maxBit]ElemType条款中的近 50 个条款来涵盖uint8案例。

是的,但在我的示例中,没有不确定长度的数组。 数组长度是编译时已知的常数。

我明白这一点,但请注意我也没有说“在运行时”。 您想编写忽略数组长度的代码。 切片已经可以做到这一点。

我高度怀疑该函数的编译通用版本是否需要按照您的建议“传递”数组长度,因为它实际上是一个常量值,是编译时实例化类型的一部分。

该函数的通用版本会 - 因为该类型的每个实例化都使用不同的常量。 这就是为什么我觉得您假设生成的代码不是通用的,而是针对每种类型进行扩展的原因。 即,您似乎假设将生成该函数的多个实例化,例如[1]Element[2]Element等。我是说,这对我来说似乎不太可能,它似乎更有可能将生成一个版本,它本质上等同于切片版本。

当然不一定非得如此。 所以,是的,你是对的,你不需要传递数组长度。 我只是强烈预测它将以这种方式实施,而且它不会的假设似乎是一个值得怀疑的假设。 (FWIW,我还认为,如果您愿意让编译器为单独的长度生成专门的函数体,它也可以对切片透明地执行此操作,但这是一个不同的讨论)。

代码通用版本的另一大好处

澄清一下:“通用版本”是指泛型的一般概念,例如在当前合同设计草案中实现的,还是更具体地指的是具有非类型参数的泛型? 因为您在本段中提到的优势也适用于当前的合同设计草案。

我并不是要在这里对泛型提出一般的反对意见。 我只是在解释为什么我认为您的示例不能表明我们需要除类型之外的其他参数类型。

// With generics + parameterized constant array lengths:
// Without generics

这是一种错误的二分法(而且很明显,我对你有点失望)。 还有“带有类型参数,但没有整数参数”:

contract Unsigned(T) {
    T uint, uint8, uint16, uint32, uint64
}
func HammingDist(type T Unsigned) (b1, b2 []T) (sum int) {
    if len(b1) != len(b2) {
        panic("slices of different lengths passed to HammingDist")
    }
    for i := range b1 {
        sum += bits.OnesCount(b1[i]^b2[i]) // Same assumption about OnesCount being generic you made above
    }
    return sum
}

这对我来说似乎很好。 它的类型安全性稍差,如果类型不匹配,则需要运行时恐慌。 但是,这就是我的观点,这是在您的示例中添加非类型泛型参数的唯一优势(这是一个已经很清楚的优势,IMO)。 您预测的性能提升依赖于非常强的假设,即一般泛型和非类型参数上的泛型具体是如何实现的。 根据我迄今为止从 Go 团队听到的消息,我个人认为不太可能。

我高度怀疑该函数的编译通用版本是否需要按照您的建议“传递”数组长度,因为它实际上是一个常量值,是编译时实例化类型的一部分。

您只是假设泛型会像 C++ 模板和重复的函数实现一样工作,但这是不对的。 该提案明确允许使用隐藏参数的单一实现。

我认为,如果您确实需要为少数数字类型提供模板化代码,那么使用代码生成器并没有那么大的负担。 泛型真的只值得容器类型之类的代码复杂性,因为使用原始类型可以带来可衡量的性能优势,但您不能合理地期望只提前生成少量代码模板。

我显然不知道 golang 维护者最终将如何实现任何东西,所以我将避免进一步猜测,并愉快地听从那些有更多内幕知识的人。

我所知道的是,对于我上面分享的示例实际问题,当前基于切片的实现与基于优化的通用数组实现之间的潜在性能差异是巨大的。

BenchmarkHD/256-bit_unrolled_array_HD-20            2000000000           1.05 ns/op        0 B/op          0 allocs/op
BenchmarkHD/256-bit_slice_HD-20                     300000000            5.10 ns/op        0 B/op          0 allocs/op

代码位于: https ://github.com/vsivsi/hdtest

对于 4x64 位情况(我的工作中的一个最佳点)来说,这是一个 5 倍的潜在性能差异,在数组情况下只需要一点循环展开(并且基本上没有额外的代码)。 这些计算在我的算法的内部循环中进行,实际上进行了数万亿次,因此 5 倍的性能差异是非常巨大的。 但是今天要实现这些效率提升,我需要为每个需要的元素类型和数组长度编写函数的每个版本。

但是,是的,如果维护者从未实现过诸如此类的优化,那么将参数化数组长度添加到泛型的整个练习将毫无意义,至少因为它可能有益于这个示例案例。

无论如何,有趣的讨论。 我知道这些是有争议的问题,所以感谢您保持文明!

@vsivsi FWIW,如果您没有手动展开循环(或者如果您也在切片上展开循环),那么您观察到的胜利就会消失 - 所以这实际上仍然不支持您的论点,即整数参数有帮助,因为它们允许编译器为你做展开。 对我来说,基于编译器对 X 变得任意聪明而对 Y 保持任意愚蠢,对我来说似乎是不科学的争论 X 与 Y。我不清楚为什么在循环数组的情况下会触发不同的展开启发式,但在编译时长度已知的切片上循环时不会触发。 您并没有展示某种风格的泛型相对于另一种风格的好处,而是展示了不同展开启发式的好处。

但无论如何,没有人真正争辩说,为通用函数的每个实例化生成专门的代码不会更快——只是在决定是否要这样做时需要考虑其他权衡

@Merovius我认为在这种示例中泛型的最强案例是编译时精化(因此为每个类型级整数发出一个独特的函数),其中要专门化的代码位于库中。 如果库用户将使用有限数量的函数实例化,那么他们将获得优化版本的优势。 因此,如果我的代码只使用长度为 64 的数组,我可以对长度为 64 的库函数进行优化处理。

在这种特定情况下,它取决于数组长度的频率分布,因为如果由于内存限制和页面缓存垃圾处理可能会使事情变慢,如果有数千个函数,我们可能不想详细说明所有可能的函数。 例如,如果小尺寸很常见,但可能更大(尺寸上的长尾分布),那么我们可以为具有展开循环(比如 1 到 64)的小整数制定专门的函数,然后提供一个带有隐藏的通用版本-其余的参数。

我不喜欢“任意聪明的编译器”的想法,并认为这是一个糟糕的论点。 我要等待这个任意聪明的编译器多久? 我特别不喜欢编译器更改类型的想法,例如将切片优化为数组,从而在具有反射的语言中进行隐藏的特化,因为当您反射该切片时,可能会发生意想不到的事情。

关于“通用困境”,我个人会选择“让编译器变慢/做更多工作”,但尝试通过使用良好的实现和单独的编译来尽可能快地实现。 Rust 似乎做得很好,在英特尔最近宣布之后,它似乎最终可以取代“C”成为主要的系统编程语言。 编译时间似乎甚至不是英特尔决定的一个因素,因为运行时内存和类似“C”的速度的并发安全性似乎是关键因素。 Rust 的“特征”是泛型类型类的合理实现,它们有一些恼人的极端情况,我认为这来自于它们的类型系统设计。

回顾我们之前的讨论,我必须小心地将关于泛型的讨论分开讨论,以及它们如何具体应用于 Go。 因此,我不确定 Go 是否应该有泛型,因为它使简单而优雅的语言变得复杂,就像“C”没有泛型一样。 我仍然认为市场上对于将通用实现作为核心功能但仍然简单而优雅的语言存在差距。

我想知道这方面是否有任何进展。

我可以尝试仿制药多久。 我已经等了很久

@Nsgj您可以查看此 CL: https ://go-review.googlesource.com/c/go/+/187317/

在当前的规范中,这可能吗?

contract Point(T) {
  T struct { X, Y float64 }
}

换句话说,该类型必须是具有两个字段 X 和 Y 的 float64 类型的结构。

编辑:示例用法

func generate(type T Point)() T {
  return T{X: randomFloat64(), Y: randomFloat64()}
}

@abuchanan-nr 是的,目前的设计草案允许这样做,尽管很难看出它会有什么用处。

我也不确定它是否有用,但我没有看到在合同类型列表中使用自定义结构类型的明确示例。 大多数示例使用内置类型。

FWIW,我在想象一个 2D 图形库。 您可能希望每个顶点具有许多特定于应用程序的字段,例如颜色、力等。但您可能还需要一个仅用于几何部分的通用方法和算法库,它仅真正依赖于 X、Y 坐标。 将您的自定义顶点类型传递到此库中可能会很好,例如

type MyVertex struct {
  X, Y float64
  Color color.Color
  OtherAttr int
}
p := geo.RandomPolygon(MyVertex)()

for _, vert := range p.Vertices() {
  p.Color = randColor()
}

同样,不确定这在实践中是否是一个好的设计,但这是我当时的想象所在:)

请参阅https://godoc.org/image#Image了解如何在今天的标准 Go 中完成此操作。

关于合同中的运营商/类型

这会导致许多通用方法的重复,因为我们需要它们以运算符格式( +==< 、...)和方法格式( Plus(T) T , Equal(T) bool , LessThan(T) bool , ...)。

我建议我们将这两种方法统一为一种方法,即方法格式。 为此,需要使用任意方法将预先声明的类型( intint64string 、...)强制转换为类型。 对于已经可能的(微不足道的)简单案例( type MyInt int; func (i MyInt) LessThan(o MyInt) bool {return int(i) < int(o)} ),但真正的价值在于复合类型( []int -> []MyIntmap[int]struct{} -> map[MyInt]struct{} ,等等通道,指针,...),这是不允许的(请参阅常见问题解答)。 允许这种转换本身就是一个重大的变化,所以我在Relaxed Type Conversion Proposal中扩展了技术细节。 这将允许泛型函数不处理运算符并且仍然支持所有类型,包括预先声明的类型。

请注意,此更改也有利于非预先声明的类型。 在当前提案下,给定type X struct{S string} (来自外部库,因此您不能向其添加方法),假设您有一个[]X并希望将其传递给通用函数期望[]T ,因为T满足Stringer合同。 这需要type X2 X; func(x X2) String() string {return x.S}[]X[]X2的深拷贝。 根据对此提议的提议更改,您将完全保存深层副本。

注意:提到的宽松类型转换提案需要挑战。

@JavierZunzunegui为基本的一元/二元运算符提供“方法格式”(或运算符格式)不是问题。 通过简单地允许运算符符号作为方法名称来引入诸如+(x int) int之类的方法是相当直接的,并将其扩展到内置类型(尽管由于右手运算符可以是任意整数类型——我们目前没有办法表达这一点)。 问题是这还不够。 合同需要表达的一件事是 $ X $ 类型的值x是否可以转换为类型参数T的类型,如T(x) (反之亦然)。 也就是说,需要为允许的转换发明一种“方法格式”。 此外,需要有一种方法来表示可以将无类型常量c分配给(或转换为)类型参数类型T的变量:分配是否合法,例如, 256 到t类型的T ? 如果Tbyte怎么办? 还有一些类似的事情。 可以为这些东西发明“方法格式”表示法,但它很快就会变得复杂,而且不清楚它是否更易于理解或可读。

我不是说不能做,但我们还没有找到一个令人满意和明确的方法。 另一方面,当前仅列举类型的设计草案非常容易理解。

@griesemer由于其他优先级,这在 Go 中可能很难,但总的来说这是一个很好解决的问题。 这是我认为隐式转换不好的原因之一。 还有其他原因,例如魔术发生,阅读代码的人看不到。

如果类型系统中没有隐式转换,那么我可以使用重载来精确控制接受类型的范围,而接口控制重载。

我倾向于使用接口来表达类型之间的相似性,因此像“+”这样的操作通常会表示为数字接口而不是类型上的操作。 你需要有类型变量和接口来表达参数和加法结果必须是相同类型的约束。

所以这里加法运算符被声明为对具有数值接口的类型进行操作。 这与数学很好地联系在一起,例如,“整数”和“加法”形成一个“组”。

你最终会得到类似的东西:

+(T Addable)(x T, y T) T

如果您允许隐式接口选择,那么“+”运算符可以只是 Numeric 接口的一个方法,但我认为这会导致 Go 中的方法选择问题?

@griesemer关于转换的观点:

合约需要表达的一件事是 X 类型的值 x 是否可以转换为 T(x) 中的类型参数 T 的类型(反之亦然)。 也就是说,需要为允许的转换发明一种“方法格式”

我可以看到这将是多么复杂,但我认为没有必要。 我认为这样的转换会在调用者的通用代码之外发生。 一个示例(根据设计草案使用Stringify ):

Stringify(int)([]int{1,2}) // does not compile
type MyInt int
func (i MyInt) String() string {...}
Stringify(MyInt)([]MyInt([]int{1,2})) // OK. Generic type MyInt could be inferred

上面,就Stringify而言,参数是类型[]MyInt并且符合合同。 泛型代码不能将泛型类型转换为其他任何东西(除了它们实现的接口,根据合同),正是因为他们的合同对此没有任何说明。

@JavierZunzunegui我看不到调用者如何在不将它们暴露在接口/合同中的情况下进行此类转换。 例如,我可能想实现一个对各种整数或浮点类型进行操作的通用数值算法(参数化函数)。 作为该算法的一部分,函数代码需要将常量值c1c2等分配给参数类型T的值。 如果不知道可以将这些常量分配给T类型的变量,我看不到代码是如何做到这一点的。 (当然不希望将这些常量传递给函数。)

func NumericAlgorithm(type T SomeContract)(vector []T) T {
   ...
   vector[i] = 3.1415  // <<< how do we know this is valid without the contract telling us?
   ...
}

需要将常量值c1c2等分配给参数类型T的值

@griesemer我会(在我看来/应该如何泛型)说上面是错误的问题陈述。 您要求将T定义为float32 ,但合同仅说明T可用的方法,而不是定义的方法。 如果需要,可以将vector保留为[]T并需要func(float32) T参数( vector[i] = f(c1) ),或者更好地保留vector[]float32并通过合同要求T有一个方法DoSomething(float32)DoSomething([]float32) ,因为我假设T和浮动必须在某些时候交互。 这意味着T可能会或可能不会被定义为type T float32 ,我们只能说它具有合同要求的方法。

@JavierZunzunegui我根本不是说T被定义为float32 - 它可能是float32float64 ,甚至是其中之一复杂的类型。 更一般地说,如果常量是一个整数,那么可能有多种整数类型可以有效地传递给这个函数,而有些则不是。 这当然不是“错误的问题陈述”。 问题是真实存在的——想要能够编写这样的函数当然不是人为的——而且问题并没有通过宣布它“错误”而消失。

@griesemer我明白了,我以为你只关心转换,我没有注册它处理无类型常量的关键元素。

您可以按照我上面的回答进行操作, T有一个方法DoSomething(X) ,并且该函数采用附加参数func(float64) X ,因此通用形式由两种类型定义( T,X )。 您描述问题X的方式通常是float32float64 ,函数参数是func(f float64) float32 {return float32(f)}func(f float64) float64 {return f}

更重要的是,正如您所强调的那样,对于整数情况,存在不太精确的整数格式对于给定常量可能不够的问题。 最安全的方法是保持两种类型的 ( T,X ) 通用函数私有并仅公开公开MyFunc32 / MyFunc64 /etc。

我承认MyFunc32(int32) / MyFunc64(int64) /etc。 不如单个MyFunc(type T Numeric)实用(相反是站不住脚的!)。 但这仅适用于依赖常量的通用实现,主要是整数常量 - 其中有多少? 对于其余部分,您可以获得额外的自由,不受T的一些内置类型的限制。

当然,如果函数不昂贵,您可能完全可以按照int64 / float64进行计算并仅公开它,使其既简单又不受T的限制

我们真的不能对人们说“你可以在任何类型的 T 上编写泛型函数,但那些泛型函数可能不使用无类型常量。” Go 首先是一门简单的语言。 像这样具有奇怪限制的语言并不简单。

任何时候提议的泛型方法变得难以用简单的方式解释,我们就必须放弃这种方法。 保持语言简单比在语言中添加泛型更重要。

@JavierZunzunegui参数化(通用)代码的有趣属性之一是编译器可以根据实例化代码的类型对其进行自定义。 例如,有一个人希望使用byte类型而不是int ,因为它可以节省大量空间(想象一个分配大量泛型类型切片的函数)。 所以简单地将代码限制为“足够大”的类型是一个不令人满意的答案,即使对于像 Go 这样的“自以为是”的语言也是如此。

此外,这不仅仅是关于使用可能不那么常见的“大”无类型常量的算法:以“无论如何都有多少”的问题来驳回此类算法只是挥手以转移确实存在的问题。 仅供您考虑:大量算法使用-1、0、1等整数常量似乎并非不合理。请注意,不能将-1与无类型整数一起使用,仅举一个简单的例子。 显然,我们不能忽视这一点。 我们需要能够在合同中指定这一点。

@ianlancetaylor @griesemer感谢您的反馈 - 我可以看到我提出的更改与无类型常量和负整数存在重大冲突,我将把它放在我身后。

我能否请您注意https://github.com/golang/go/issues/15292#issuecomment -546313279 中的第二点:

请注意,此更改也有利于非预先声明的类型。 根据当前的提议,给定类型 X struct{S string} (来自外部库,因此您不能向其添加方法),假设您有 []X 并希望将其传递给期望 [ ]T,对于满足 Stringer 合约的 T。 那将需要 X2 X 类型; func(x X2) String() 字符串 {return xS},以及 []X 的深拷贝到 []X2。 根据对此提议的提议更改,您将完全保存深层副本。

放宽转换规则(如果技术上可行)仍然有用。

@JavierZunzunegui如果允许B(a) (使用a类型A ),则讨论排序[]B([]A)的转换似乎主要与通用功能正交。 我认为我们不需要把它带到这里。

@ianlancetaylor我不确定这与 Go 有多大关系,但我不认为常量真的是无类型的,它们必须具有类型,因为编译器必须选择机器表示。 我认为一个更好的术语是不确定类型的常量,因为常量可以由几种不同的类型表示。 一种解决方案是使用联合类型,因此像27这样的常量将具有像int16|int32|float16|float32这样的类型,即所有可能类型的联合。 那么泛型类型中的T可以是这个联合类型。 唯一的要求是我们必须在某个时候将联合解析为单一类型。 最有问题的情况是print(27)之类的,因为从来没有一个类型可以解析,在这种情况下,联合中的任何类型都可以,我们可以根据空间/速度等优化参数进行选择.

@keean规范所谓的“无类型常量”的确切名称和处理在这个问题上是题外话。 让我们在别处进行讨论。 谢谢。

@ianlancetaylor我很高兴,但是这就是为什么我认为 Go 不能有一个干净/简单的泛型实现的原因之一,所有这些问题都是相互关联的,并且为 Go 做出的最初选择没有考虑到泛型编程。 我认为需要另一种语言,旨在通过设计使泛型变得简单,对于 Go,泛型总是会在以后添加到语言中,保持语言干净和简单的最佳选择可能是根本没有它们。

如果我今天要设计一种具有快速编译时间和相当灵活性的简单语言,我会选择通过 golang 接口而不是泛型的方法重载和结构多态性(子类型化)。 事实上,它允许在具有不同字段的不同匿名接口上重载。

选择泛型具有干净的代码可重用性的优点,但它会引入更多的噪音,如果添加约束会变得复杂,有时会导致难以理解的代码。
那么,如果我们有泛型,为什么不使用高级约束系统,如 where 子句、更高种类的类型或更高等级的类型以及依赖类型?
如果采用泛型,所有这些问题最终都会出现,迟早。

明确地说,我并不反对泛型,但我正在考虑它是否是 go 的方式来保护 go 的简单性。

如果在 go 中引入泛型是不可避免的,那么在单态化泛型函数时考虑对编译时间的影响是合理的。
框泛型不是一个很好的默认值,即为所有输入类型一起生成一个副本,并且只有在用户明确要求时才专门化,并且在定义或调用站点上有一些注释?

关于对运行时性能的影响,这会由于装箱/拆箱问题而降低性能,否则,有专家级 c++ 工程师推荐像 java 那样的装箱泛型,以减少缓存未命中。

@ianlancetaylor @griesemer我重新考虑了无类型常量和“非运算符”泛型的问题(https://github.com/golang/go/issues/15292#issuecomment-547166519)并找到了更好的处理方式用它。

给出数字类型( type MyInt32 int32type MyInt64 int64 ,...),它们有许多满足相同合同( Add(T) T ,...)的方法,但关键不是其他的会冒溢出func(MyInt64) FromI64(int64) MyInt64但不会溢出的风险 ~ func(MyInt32) FromI64(int64) MyInt32 ~。 这使得可以安全地使用数字常量(明确分配给它们所需的最低精度值) (1) ,因为低精度数字类型不会满足所需的协定,但所有更高的类型都会满足。 请参阅playground ,使用接口代替泛型。

在内置类型之外放宽数字泛型的一个优点(不是特定于这个最新版本,所以我应该在上周分享它)是它允许实例化具有溢出检查类型的泛型方法 - 请参见playground 。 溢出检查本身就是一个非常流行的请求/建议(https://github.com/golang/go/issues/31500 和相关问题)。


(1) : 无类型常量的非溢出编译时保证在同一个“分支”( int[8/16/32/64]uint[8/16/32/64] ) 内很强大。 跨越分支, uint[X]常量只能安全地实例化为int[2X+] ,而int[X]常量根本不能被任何uint[X]安全地实例化。 即使放宽这些(允许int[X]<->uint[X] )遵循一些最低标准也是简单和安全的,而且至关重要的是,任何复杂性都落在通用代码的编写者身上,而不是通用代码的用户身上(他只关心合约,并且可以期望任何满足它的数字类型都是有效的)。

泛型方法 - 是 Java 的垮台!

@ianlancetaylor我很高兴,但是这就是为什么我认为 Go 不能有一个干净/简单的泛型实现的原因之一,所有这些问题都是相互关联的,并且为 Go 做出的最初选择没有考虑到泛型编程。 我认为需要另一种语言,旨在通过设计使泛型变得简单,对于 Go,泛型总是会在以后添加到语言中,保持语言干净和简单的最佳选择可能是根本没有它们。

我同意 100%。 尽管我很想看到某种泛型的实现,但我认为你们目前正在做的事情会破坏 Go 语言的简单性。

目前扩展接口的想法是这样的:

type I1(type P1) interface {
        m1(x P1)
}

type I2(type P1, P2) interface {
        m2(x P1) P2
        type int, float64
}

func f(type P1 I1(P1), P2 I2(P1, P2)) (x P1, y P2) P2

对不起大家,但请不要这样做! 它丑化了围棋的美丽。

现在已经编写了近 10 万行 Go 代码,我可以不用泛型。

然而,像支持这样的小事

// Allow mulitple types in Slices and Maps declarations
func Reverse(s []<int,string>) {
    first := 0
    last := len(s) - 1
    for first < last {
        s[first], s[last] = s[last], s[first]
        first++
        last--
    }
}

//  Allow multiple types in variable declarations
func Index (s <string, []byte>, b byte) int {
    for i := 0; i < len(s); i++ {
        if s[i] == b {
            return i
        }
    }
    return -1
}

// Allow slices and maps declarations with interface values
func ToStrings (s []Stringer) []string {
    r := make([]string, len(s))
    for i, v := range s {
        r[i] = v.String()
    }
    return r
}

有助于。

能够将泛型与常规 Go 代码完全分离的语法建议

package graph

// Example how you would define generics completely separat from Go 1 code
contract (Node, Edge)G {
    Node Edges() []Edge
    Edge Nodes() (from, to Node)
}

type (type Node, Edge G) ( Graph )
func (type Node, Edge G) ( New )
const _ = (Node, Edge) Graph

// Unmodified Go 1 code
type Graph struct { ... }
func New(nodes []Node) *Graph { ... }
func (g *Graph) ShortestPath(from, to Node) []Edge { ... }

@martinrode但是,像支持这样的小事
... 允许在 Slices 和 Maps 声明中使用多种类型

这不能满足某些功能性通用切片函数的需求,例如head()tail()map(slice, func)filter(slice, func)

您可以为每个需要它的项目自己编写它,但此时由于复制粘贴重复而存在过时的风险,并鼓励 Go 代码复杂性以节省语言的简单性。

(在个人层面上,知道我有一组我想要实现的功能并且没有一种干净的方式来表达这些功能而不回答语言限制,这也是一种疲劳)

在当前的非通用 go 中考虑以下内容:

我有一个x externallib.Foo ,从我无法控制的库externallib获得。
我想将它传递给函数SomeFunc(fmt.Stringer) ,但externallib.Foo没有String() string方法。 我可以简单地做:

type MyFoo externallib.Foo
func (mf MyFoo) String() string {...}
// ...
SomeFunc(MyFoo(x))

考虑与泛型相同。

我有一个x []externallib.Foo 。 我想将它传递给AnotherFunc(type T Stringer)(s []T) 。 如果没有将切片进行昂贵的深度复制到新的[]MyFoo中,就无法完成。 如果不是切片而是更复杂的类型(例如,chan 或 map),或者该方法修改了接收器,那么它会变得更加低效和乏味,如果可能的话。

这在标准库中可能不是问题,但这只是因为它没有外部依赖项。 这是几乎没有其他项目所拥有的奢侈品。

我的建议是放宽转换,以允许[]Foo([]Bar{})对任何定义为type Foo BarFoo #$ 进行转换,反之亦然,同样适用于映射、数组、通道和指针,递归。 请注意,这些都是廉价的浅拷贝。 更多技术细节请参阅宽松类型转换提案


这最初是在https://github.com/golang/go/issues/15292#issuecomment -546313279 中作为次要功能提出的。

@JavierZunzunegui我认为这根本与泛型无关。 是的,您可以提供一个使用泛型的示例,但您可以提供一个类似的示例而不使用泛型。 我认为这个问题应该单独讨论,而不是在这里。 另见https://golang.org/doc/faq#convert_slice_with_same_underlying_type。 谢谢。

如果没有泛型,这种转换几乎没有任何价值,因为一般来说[]Foo不会遇到任何接口,或者至少不会遇到将其用作切片的接口。 例外是具有非常特定模式来使用它的接口,例如sort.Interface ,无论如何您都不需要转换切片。

上述( func AnotherFunc(type T Stringer)(s []T) )的非通用版本是

type SliceOfStringers interface {
  Len() int
  Get(int) fmt.Stringer
}
func AnotherFunc(s SliceOfStringers) {...}

它可能不如通用方法实用,但它可以很好地处理任何切片并且无需复制它就可以这样做,无论底层类型实际上是fmt.Stringer 。 就目前而言,泛型不能,尽管原则上它是更适合这项工作的工具。 当然,如果我们添加泛型,正是为了让切片、地图等在 API 中更常见,并用更少的样板来操作它们。 然而,它们引入了一个新问题,在仅接口的世界中没有等价性,这_可能_甚至不是不可避免的,而是由语言人为强加的。

您提到的类型转换在非泛型代码中经常出现,因此它是一个常见问题解答。 让我们把这个讨论移到别处。 谢谢。

这是什么状态? 任何更新的草稿? 我一直在等待仿制药
大约 2 年前。 我们什么时候会有泛型?

埃尔三月,2 月 4 日。 de 2020 a la(s) 13:28,伊恩·兰斯·泰勒 (Ian Lance Taylor) (
通知@github.com) escribió:

您提到的类型转换在非泛型代码中经常出现
这是一个常见问题解答。 让我们把这个讨论移到别处。 谢谢。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/15292?email_source=notifications&email_token=AJMFNBN3MFHDMENAFXIKBLDRBGXUTA5CNFSM4CA35RX2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEKYV5RI#issuecomment-582049477
或退订
https://github.com/notifications/unsubscribe-auth/AJMFNBO5UTKNPL3MSA3NESLRBGXUTANCNFSM4CA35RXQ
.

--
这是在 TripleMint 中使用的邮件签名测试

我们正在努力。 有些事情需要时间。

工作是离线完成的吗? 我希望看到它随着时间的推移而发展,以一种像我这样的“普通大众”无法评论以避免噪音的方式。

虽然它已经关闭以将泛型讨论保留在一个地方,但请查看#36177,其中@Griesemer链接到他正在研究的原型,并就他迄今为止对此事的想法发表了一些有趣的评论。

我认为我的说法是正确的,该原型目前只是处理“合同”提案草案的类型检查方面,但这项工作对我来说肯定听起来很有希望。

@ianlancetaylor任何时候提议的泛型方法变得难以以简单的方式解释,我们必须放弃这种方法。 保持语言简单比在语言中添加泛型更重要。

这是一个值得努力的伟大理想,但实际上软件开发有时天生就不是_简单解释_。

当语言受限于表达这种“不容易表达”的想法时,软件工程师最终会一次又一次地重新发明这些工具,因为这些该死的“难以表达”的想法有时对程序逻辑至关重要。

看看 Istio、Kubernetes、operator-sdk,以及某种程度上的 Terraform,甚至是 protobuf 库。 它们都通过使用反射来逃避 Go 类型系统,在 Go 之上使用接口和代码生成实现一个新的类型系统,或者这些的组合。

@omeid

看看 Istio、Kubernetes

你有没有想过他们做这些荒谬的事情的原因是因为他们的核心设计没有任何意义,因此他们不得不通过reflect游戏来实现它?

我坚持认为,更好的 golang 程序设计(无论是在设计阶段还是在 API 中)都不需要泛型。

请不要将它们添加到 golang。

编程很难。 Kubelet 是一个黑暗的地方。 泛型比美国政治更能分裂人们。 我想相信。

当语言被限制在表达这种不容易表达的想法时,软件工程师最终会一次又一次地重新发明这些工具,因为这些该死的难以表达的想法有时对程序逻辑来说是必不可少的。

看看 Istio、Kubernetes、operator-sdk,以及某种程度上的 Terraform,甚至是 protobuf 库。 它们都通过使用反射来逃避 Go 类型系统,在 Go 之上使用接口和代码生成实现一个新的类型系统,或者这些的组合。

我不认为这是一个有说服力的论点。 理想情况下,Go 语言应该易于阅读、编写和理解,同时仍然可以执行任意复杂的操作。 这与你所说的一致:你提到的工具需要做一些复杂的事情,而 Go 为他们提供了一种方法来做到这一点。

理想情况下,Go 语言应该易于阅读、编写和理解,同时仍然可以执行任意复杂的操作。

我同意这一点,但因为这些是多个目标,他们有时会相互紧张。 自然“希望”以通用风格编写的代码通常变得不像其他情况那样易于阅读,因为它不得不求助于反射等技术。

自然“希望”以通用风格编写的代码通常变得不像其他情况那样易于阅读,因为它不得不求助于反射等技术。

这就是为什么这个提案保持开放以及为什么我们有一个可能实现泛型的设计草案(https://blog.golang.org/why-generics)。

看看……甚至是 protobuf 库。 它们都通过使用反射来逃避 Go 类型系统,在 Go 之上使用接口和代码生成实现一个新的类型系统,或者这些的组合。

从使用 protobufs 的经验来看,在少数情况下泛型可以提高 API 的可用性和/或实现,但绝大多数逻辑不会从泛型中受益。 泛型假定具体类型信息在编译时是已知的。 对于 protobuf,大多数情况涉及类型信息仅在运行时才知道的情况。

总的来说,我注意到人们经常指出反射的任何使用,并声称这是需要泛型的证据。 不是这么简单。 一个关键的区别是类型信息是否在编译时已知。 在许多情况下,它基本上不是。

@dsnet有趣的谢谢,从来没有想过 protobuf 不是通用的。 总是假设每个生成样板代码的工具,例如基于预定义方案的 protoc,都能够使用当前的通用提案生成通用代码而无需反射。 您是否介意在规范中使用示例或在新的 go 博客文章中更详细地描述此问题来更新此问题?

你提到的工具需要做一些复杂的事情,而 Go 为他们提供了一种方法来做到这一点。

使用文本模板生成 Go 代码几乎不是一种设计工具,我认为它是一种临时创可贴,理想情况下,至少标准 ast 和解析器包应该允许生成任意 Go 代码。

你唯一可以争辩说 Go 提供了一个处理复杂逻辑的东西可能是反射,但这很快表明它的局限性,更不用说性能关键代码,即使在标准库中使用时也是如此,例如 Go 的 JSON 处理是原始的最好。

很难说使用文本模板或反射来做_已经很复杂的事情_符合以下理想:

任何时候,当一个提议的~泛型~复杂事物的方法变得难以用简单的方式解释时,我们必须放弃这种方法。

我认为项目提到的解决他们问题的解决方案太复杂了,不容易理解。 因此,在这方面,Go 缺乏允许用户以尽可能简单和直接的方式表达复杂问题的工具。

总的来说,我注意到人们经常指出反射的任何使用,并声称这是需要泛型的证据。

也许有这样一个普遍的误解,但是 protobuf 库,特别是新的 API 可以使用 _generics_ 或某种 _sum 类型_来实现更简单的飞跃。

新的 protobuf API 的作者之一刚刚说“绝大多数逻辑不会从泛型中受益”,所以我不确定你从哪里得到“特别是新的 API 可能会飞跃更多简单的泛型”。 这是基于什么? 你能提供任何证据证明它会简单得多吗?

作为在包括泛型(Java、C++)在内的几种语言中使用过 protobuf API 的人来说,我不能说我注意到 Go API 及其 API 的任何显着可用性差异。 如果你的断言是真的,我希望会有一些这样的差异。

@dsnet还说“在某些情况下,泛型可以提高 API 的可用性和/或实现”。

但是,如果您想要一个如何让事情变得更简单的示例,请从删除Value类型开始,因为它主要是一种临时求和类型。

@omeid这个问题是关于泛型的,而不是总和类型。 所以我不确定这个例子是如何相关的。

具体来说,我的问题是:泛型如何导致 protobuf 实现或 API 比新的(或旧的,就此而言)API“更简单”?

这似乎不符合我对@dsnet上面所说的内容的阅读,也不符合我对 Java 和 C++ protobuf API 的经验。

此外,您对 Go 中原始 JSON 处理的评论也让我觉得同样奇怪。 你能解释一下你认为encoding/json的 API 会如何被泛型改进吗?

AFAIK,Java 中 JSON 解析的实现使用反射(不是泛型)。 确实,大多数 JSON 库中的顶级 API 可能会使用泛型方法(例如Gson ),但该方法采用不受约束的泛型参数T并返回T类型的值与json.Unmarshal相比,提供很少的额外类型检查。 事实上,我认为在编译时没有被json.Unmarshal捕获的唯一额外错误场景是如果你传递了一个非指针值。 (另外,请注意 Gson 的 API 文档中的注意事项,即对泛型和非泛型类型使用不同的函数。同样,这表明泛型使它们的 API 复杂化,而不是简化它;在这种情况下,它是为了支持序列化/反序列化泛型类型)。

(C++ 中的 JSON 支持更糟糕;我知道的各种方法要么使用大量宏,要么涉及手动编写解析/序列化函数。同样,这不是)

如果您期望泛型能够为 Go 对 JSON 的支持添加大量内容,我担心您会感到失望。


@gertcuykens我所知道的每种语言的每个 protobuf 实现都使用代码生成,无论它们是否具有泛型。 这包括 Java、C++、Swift、Rust、JS(和 TS)。 我不认为拥有泛型会自动删除代码生成的所有用途(作为存在证明,我编写了生成 Java 代码和 C++ 代码的代码生成器); 期望任何泛型解决方案都能达到这个标准似乎是不合逻辑的。


绝对清楚:我支持向 Go 添加泛型。 但我认为我们应该清楚地知道我们将要从中得到什么。 我认为我们不会对 protobuf 或 JSON API 进行重大改进。

我不认为 protobuf 对于泛型来说是一个特别好的案例。 您不需要目标语言中的泛型,因为您可以直接生成专门的代码。 这也适用于 Swagger/OpenAPI 等其他类似系统。

泛型似乎对我有用,并且可以提供简化和类型安全的地方将是编写 protobuf 编译器本身。

您需要的是一种能够对其自己的抽象语法树进行类型安全表示的语言。 根据我自己的经验,这至少需要泛型和通用抽象数据类型。 然后,您可以为该语言本身编写一个类型安全的 protobuf 编译器。

泛型似乎对我有用,并且可以提供简化和类型安全的地方将是编写 protobuf 编译器本身。

我真的不明白怎么做。 go/ast包已经提供了 Go 的 AST 的表示。 Go protobuf 编译器不使用它,因为使用 AST 比仅仅发出字符串要麻烦得多,即使它更安全。

也许您有其他语言的 protobuf 编译器的示例?

@neild我确实首先说我不认为 protobuf 是一个很好的例子。 使用泛型可以获得一些好处,但它们在很大程度上取决于类型安全对您的重要性,而这将通过泛型实现的侵入性来抵消。 除非您犯了错误,否则理想的实现不会妨碍您,在这种情况下,优势将超过更多用例的成本。

查看 go/ast 包,它没有 AST 的类型表示,因为这需要泛型和 GADT。 例如,“添加”节点需要在添加的术语类型中具有通用性。 使用非类型安全的 AST,所有类型检查逻辑都必须手动编码,这会很麻烦。

使用良好的模板语法和类型安全的表达式,您可以让它像发出字符串一样简单,而且类型安全。 例如参见(更多关于解析方面): https ://stackoverflow.com/questions/11104536/how-to-parse-strings-to-syntax-tree-using-gadts

例如,将 JSX 视为 JavaScript 中 HTML Dom 的文字语法,而 TSX 视为 TypeScript 中 Dom 的文字语法。

我们可以编写专门用于最终代码的类型化泛型表达式。 像字符串一样容易编写,但类型检查(以它们的通用形式)。

代码生成器的关键问题之一是类型检查只发生在发出的代码上,这使得编写正确的模板变得困难。 使用泛型,您可以将模板编写为实际的类型检查表达式,因此检查直接在模板上完成,而不是发出的代码,这使得它更容易正确和维护。

当前设计中缺少可变参数类型参数,这看起来像是泛型功能的巨大缺失。 附加设计(可能)遵循当前的合同设计:

contract Comparables(Ts...) {
    if  len(Ts) > 0 {
        Comparables(Ts[1:]...)
    } else {
        Comparable(Ts[0])
    }
}

contract Comparable(T) {
    T int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

type Keys(type Ts ...Comparables) struct {
    fs ...Ts
}

type Metric(type Ts ...Comparables) struct {
    mu sync.Mutex
    m  map[Keys(Ts...)]int
}

func (m *Metric(Ts...)) Add(vs ...Ts) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[Keys(Ts...))]int)
    }
    m[Keys(Ts...){vs...}]++
}


// To use the metric

m := Metric(int, float64, string){m: make(map[Keys(int, float64, string)]int}
m.Add(1, 2.0, "variadic")

示例灵感来自这里

我不清楚这如何在仅使用interface{}之上增加任何安全性。 人们将不可比较的值传递给度量是否存在真正的问题?

我不清楚这如何在仅使用interface{}之上增加任何安全性。 人们将不可比较的值传递给度量是否存在真正的问题?

此示例中的Comparables要求Keys必须由一系列可比较的类型组成。 关键思想是展示可变参数类型参数的设计,而不是类型本身的含义。

我不想太拘泥于这个例子,但我之所以选择它,是因为我认为许多“类型扩展”的例子最终只是在没有增加任何实际安全性的情况下推动簿记。 在这种情况下,如果您在运行时看到错误的类型或可能使用 go vet,那么您可能会抱怨。

另外,我有点担心允许像这样的开放式类型会导致矛盾引用的问题,就像在二阶逻辑中发生的那样。 您能否将 C 定义为所有不在 C 中的类型的合同?

另外,我有点担心允许像这样的开放式类型会导致矛盾引用的问题,就像在二阶逻辑中发生的那样。 您能否将 C 定义为所有不在 C 中的类型的合同?

抱歉,但我不明白这个示例如何允许开放式类型并与罗素悖论相关, ComparablesComparable列表定义。

我不喜欢在合约中编写 Go 代码的想法。 如果我可以写一个if语句,我可以写一个for语句吗? 我可以调用一个函数吗? 我可以声明变量吗? 为什么不?

似乎也没有必要。 func F(a ...int)表示 a 是[]int 。 以此类推, func F(type Ts ...comparable)意味着列表中的每种类型都是comparable

在这些行中

type Keys(type Ts ...Comparables) struct {
    fs ...Ts
}

您似乎正在定义一个结构,其中包含多个名为fs的字段。 我不确定这应该如何工作。 除了使用反射之外,还有什么方法可以引用这个结构中的字段?

所以问题是:可以用可变类型参数做什么? 一个人想做什么?

在这里,我认为您正在使用可变类型参数来定义具有任意数量字段的元组类型。

还想做什么?

我不喜欢在合约中编写 Go 代码的想法。 如果我可以写一个if语句,我可以写一个for语句吗? 我可以调用一个函数吗? 我可以声明变量吗? 为什么不?

似乎也没有必要。 func F(a ...int)表示 a 是[]int 。 以此类推, func F(type Ts ...comparable)意味着列表中的每种类型都是comparable

一天后回顾了这个例子,我认为你是绝对正确的。 Comparables是一个愚蠢的想法。 该示例只想传达使用len(args)来确定参数数量的信息。 事实证明,对于函数, func F(type Ts ...Comparable)已经足够好了。

修剪示例:

contract Comparable(T) {
    T int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

type Keys(type Ts ...Comparable) struct {
    fs ...Ts
}

type Metric(type Ts ...Comparable) struct {
    mu sync.Mutex
    m  map[Keys(Ts...)]int
}

func (m *Metric(Ts...)) Add(vs ...Ts) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[Keys(Ts...))]int)
    }
    m[Keys(Ts...){vs...}]++
}


// To use the metric

m := Metric(int, float64, string){m: make(map[Keys(int, float64, string)]int}
m.Add(1, 2.0, "variadic")

您似乎正在定义一个结构,其中包含多个名为fs的字段。 我不确定这应该如何工作。 除了使用反射之外,还有什么方法可以引用这个结构中的字段?

所以问题是:可以用可变类型参数做什么? 一个人想做什么?

在这里,我认为您正在使用可变类型参数来定义具有任意数量字段的元组类型。

还想做什么?

如果我们使用...来定义可变类型参数,它的定义是针对元组的,这并不意味着元组是唯一的用例,但可以在任何结构和任何函数中使用它。

由于只有两个地方出现了可变参数类型参数:结构或函数,所以我们很容易对函数有什么之前清楚的:

func F(type Ts ...Comparable) (args ...Ts) {
    if len(args) > 1 {
        F(args[1:])
        return
    }
    // ... do stuff with args[0]
}

例如,可变参数Min函数在当前设计中是不可能的,但可以使用可变参数类型参数:

func Min(type T ...Comparable)(p1 T, pn ...T) T {
    switch l := len(pn); {
    case l > 1:
        return Min(pn[0], pn[1:]...)
    case l == 1:
        if p1 >= pn[0] { return pn[0] }
        return p1
    case l < 1:
        return p1
    }
}

要使用可变类型参数定义Tuple

type Tuple(type Ts ...Comparable) struct {
    fs ...Ts
}

当 'Ts` 实例化三个类型参数时,可以翻译成

type Tuple(type T1, T2, T3 Comparable) struct {
    fs_1 T1
    fs_2 T2
    fs_3 T3
}

作为中间表示。 要使用fs ,有几种方法:

  1. 参数解包
k := Tuple(int, float64, string){1, 2.0, "variadic"}
fs1, fs2, fs3 := k.fs // translated to fs1, fs2, fs3 := k.fs_1, k.fs_2, k.fs_3
println(fs1) // 1
println(fs2) // 2.0
println(fs3) // variadic
  1. 使用for循环
for idx, f := range k.fs {
    println(idx, ": ", f)
}
// Output:
// 0: 1
// 1: 2.0
// 2: variadic
  1. 使用索引(不确定人们是否认为这是数组/切片或映射的歧义)
k.fs[0] = ... // translated to k.fs_1 = ...
f2 := k.fs[1] // translated to f2 := k.fs_2
  1. 使用reflect包,基本上像数组一样工作
t := Tuple(int, float64, string){1, 2.0, "variadic"}

fs := reflect.ValueOf(t).Elem().FieldByName("fs")
val := reflect.ValueOf(fs)
if val.Kind() == reflect.VariadicTypes {
    for i := 0; i < val.Len(); i++ {
        e := val.Index(i)
        switch e.Kind() {
        case reflect.Int:
            fmt.Printf("%v, ", e.Int())
        case reflect.Float64:
            fmt.Printf("%v, ", e.Float())
        case reflect.String:
            fmt.Printf("%v, ", e.String())
        }
    }
}

与使用数组相比,没有什么新东西。

例如,可变参数 Min 函数在当前设计中是不可能的,但可以使用可变参数类型参数:

func Min(type T ...Comparable)(p1 T, pn ...T) T {
    switch l := len(pn); {
    case l > 1:
        return Min(pn[0], pn[1:]...)
    case l == 1:
        if p1 >= pn[0] { return pn[0] }
        return p1
    case l < 1:
        return p1
    }
}

这对我来说没有意义。 可变类型参数仅在类型可以是不同类型时才有意义。 但是在不同类型的列表上调用Min是没有意义的。 Go 不支持对不同类型的值使用>= 。 即使我们以某种方式允许这样做,我们也可能会被要求提供Min(int, string)(1, "a") 。 那没有任何答案。

虽然当前的设计确实不允许Min使用不同类型的可变参数,但它确实支持在相同类型的可变参数数量上调用Min 。 我认为这是使用Min的唯一合理方式。

func Min(type T comparable)(s ...T) T {
    if len(s) == 0 {
        panic("Min of no elements")
    }
    r := s[0]
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

对于https://github.com/golang/go/issues/15292#issuecomment -599040081 中的其他一些示例,请务必注意,在 Go 中,切片和数组具有相同类型的元素。 当使用可变类型参数时,元素是不同的类型。 所以它真的与切片或数组不一样。

虽然当前的设计确实不允许Min使用不同类型的可变参数,但它确实支持在相同类型的可变参数数量上调用Min 。 我认为这是使用Min的唯一合理方式。

func Min(type T comparable)(s ...T) T {
    if len(s) == 0 {
        panic("Min of no elements")
    }
    r := s[0]
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

真的。 Min是一个不好的例子。 从评论编辑历史可以看出,添加较晚,并没有明确的想法。 一个真实的例子是你忽略的Metric

需要注意的是,在 Go 中,切片和数组的元素都是相同类型的。 当使用可变类型参数时,元素是不同的类型。 所以它真的与切片或数组不一样。

看? 您是那些认为这是数组/切片或映射模棱两可的人。 正如我在https://github.com/golang/go/issues/15292#issuecomment -599040081 中所说,语法与数组/切片和映射非常相似,但它访问的是不同类型的元素。 真的有关系吗? 或者可以证明这是模棱两可的吗? Go 1 中可能的情况是:

m := map[interface{}]int{1: 2, "2": 3, 3.0: 4}
for i, e := range m {
    println(i, e)
}

i是否被视为同一类型? 显然,我们说 i 是interface{} ,相同类型。 但是接口真的表达了类型吗? 程序员必须手动检查可能的类型。 当使用for[]和 unpack 时,它们对用户来说真的很重要,因为它们没有访问相同的类型吗? 反对这一点的论据是什么? fs相同:

for idx, f := range k.fs {
    switch f.(type) { // compare to interface{}, here is zero overhead.
    case int:
        // ...
    case float64:
        // ...
    case string:
        // ...
    }
}

如果您必须使用类型开关来访问可变泛型类型的元素,我看不出有什么好处。 我可以看到通过一些编译技术的选择,它在运行时可能比使用interface{}更有效。 但我认为差异会相当小,我不明白为什么它会更加类型安全。 是否值得让语言变得更复杂并不是很明显。

我不打算忽略Metric示例,我只是还没有看到如何使用可变参数泛型类型来使其更易于编写。 如果我需要在Metric的主体中使用类型开关,那么我想我宁愿写Metric2Metric3

“使语言更复杂”的定义是什么? 我们都同意泛型是一个复杂的东西,它永远不会让语言比 Go 1 更​​简单。你已经付出了巨大的努力来设计和实现它,但是 Go 用户还不清楚:“feel like”的定义是什么写……去”? 是否有量化的指标来衡量它? 一个语言提案怎么能辩称它没有让语言变得更复杂呢? 在 Go 2 语言提案模板中,目标在第一印象中非常简单:

  1. 解决许多人的重要问题,
  2. 对其他人的影响最小,并且
  3. 提供清晰易懂的解决方案。

但是,问题可能是:“多少”是多少? 什么代表“重要”? 如何衡量对未知人群的影响? 当一个问题被充分理解时? 围棋正在主宰云,但主宰科学数值计算(例如机器学习)、图形渲染(例如巨大的 3D 市场)等其他领域会成为围棋的目标之一吗? 问题是否更适合“我宁愿在 Go 中做 A 而不是 B & 没有用例,因为我们可以用另一种方式做”或“没有提供 B,因此我们不使用 Go & 用例还没有,因为语言不能轻易表达”? ......我发现这些问题是痛苦和无止境的,有时甚至不值得回答。

回到Metric示例,它没有显示访问个人的任何需要。 在这里解包参数集似乎不是真正需要,尽管与现有语言“一致”的解决方案是使用[ ]索引和类型推导可以解决类型安全的问题:

f2 := k.fs[1] // f2 is a float64

@changkun如果有明确和客观的指标来决定哪些语言特性的好坏,我们就不需要语言设计师——我们可以编写一个程序来为我们设计一种最佳语言。 但是没有 - 它总是归结为某些人的个人喜好。 这也是,顺便说一句,为什么争论一种语言是否“好”是没有意义的——唯一的问题是你个人是否喜欢它。 在围棋的情况下,决定偏好的人是围棋团队的人,你引用的东西不是指标,他们是指导性的问题来帮助你说服他们。

就个人而言,FWIW,我觉得可变参数类型参数在这三个中的两个上失败。 我不认为它们解决了许多人的重要问题 - 指标示例可能会从中受益,但 IMO 只是轻微的,它是一个非常专业的用例。 而且我认为它们没有提供清晰且易于理解的解决方案。 我不知道有任何语言支持这样的东西。 但我可能错了。 如果有人有支持这一点的其他语言的例子,这肯定会有所帮助 - 它可以提供有关它通常是如何实现的信息,更重要的是,它是如何使用的。 也许它的使用范围比我想象的要广泛。

@Merovius Haskell 具有我们在 HList 论文中演示的多变量函数: http ://okmij.org/ftp/Haskell/polyvariadic.html#polyvar -fn
在 Haskell 中这样做显然很复杂,但并非不可能。

激励示例是类型安全的数据库访问,其中可以完成类型安全连接和投影之类的操作,以及用该语言声明的数据库模式。

例如,一个数据库表看起来很像一条记录,其中有列名和类型。 关系连接操作采用两条任意记录并生成一条包含两者类型的记录。 你当然可以手工做,但是容易出错,非常繁琐,用所有手工声明的记录类型混淆了代码的含义,当然SQL数据库的一大特点是它支持ad-hoc查询,因此您无法预先构建所有可能的记录类型,因为在您执行它们之前不一定知道您想要什么查询。

因此,记录和元组的类型安全关系连接运算符将是一个很好的用例。 我们在这里只考虑函数的类型——函数的实际作用取决于程序员,无论是两个元组数组的内存连接,还是生成 SQL 以在外部数据库上运行并编组结果以类型安全的方式返回。

这种东西在带有 LINQ 的 C# 中得到了更简洁的嵌入。 大多数人似乎认为 LINQ 是在 C# 中添加 lambda 函数和 monad,但它不适用于没有多变量的主要用例,因为您无法定义没有类似功能的类型安全连接运算符。

我认为关系运算符很重要。 在布尔、二进制、整数、浮点数和字符串类型的基本运算符之后,接下来可能是集合,然后是关系。

顺便说一句,C++ 也提供了它,尽管我们不想争论我们想要 Go 中的这个特性,因为 XXX 有它:)

我认为如果k.fs[0]k.fs[1]有不同的类型会很奇怪。 这不是其他可索引值在 Go 中的工作方式。

指标示例基于https://medium.com/@sameer_74231/go -experience-report-for-generics-google-metrics-api-b019d597aaa4。 我认为代码需要反射来检索值。 我认为如果我们要在 Go 中添加可变参数泛型,我们应该得到比反射更好的方法来检索值。 否则,它似乎并没有太大帮助。

我认为如果k.fs[0]k.fs[1]有不同的类型会很奇怪。 这不是其他可索引值在 Go 中的工作方式。

指标示例基于https://medium.com/@sameer_74231/go -experience-report-for-generics-google-metrics-api-b019d597aaa4。 我认为代码需要反射来检索值。 我认为如果我们要在 Go 中添加可变参数泛型,我们应该得到比反射更好的方法来检索值。 否则,它似乎并没有太大帮助。

好。 你请求的东西不存在。 如果你不喜欢[``] ,还有两个选择: ( ){``} ,我看到你可以争辩说括号看起来像一个函数调用和花括号看起来像变量初始化。 没有人喜欢args.0 args.1 ,因为这感觉不像 Go。 语法很简单。

实际上,我花了一些周末时间阅读《C++ 的设计和演变》这本书,尽管它是 1994 年写的,但有很多关于决策和教训的有趣见解:

_"[...] 回想起来,我低估了约束在可读性和早期错误检测中的重要性。"_ ==> 伟大的合约设计

"_函数语法乍一看也更好看,没有额外的关键字:_

T& index<class T>(vector<T>& v, int i) { /*...*/ }
int i = index(v1, 10);

_这种更简单的语法似乎存在一些烦人的问题。 这太聪明了。 在程序中发现模板声明相对困难,因为 [...] <...>括号被选择而不是括号,因为用户发现它们更易于阅读。 [...] 碰巧的是,Tom Pennello 证明括号会更容易解析,但这并没有改变(人类)读者更喜欢<...> _
" ==> 是不是类似于func F(type T C)(v T) T

_“但是,我确实认为我在指定模板功能时过于谨慎和保守。我本可以包含诸如 [...] 之类的功能。这些功能不会大大增加实施者的负担,并且用户会得到帮助。”_

为什么感觉这么熟悉?

索引可变参数类型参数(或元组)需要与运行时索引和编译时索引分开。 我猜您可能只是争辩说,缺乏对运行时索引的支持会使用户感到困惑,因为它与编译时索引不一致。 即使对于编译时索引,当前设计中也缺少非类型“模板”参数。

有了所有的证据,提案(除了经验报告)试图避免讨论这个特性,我开始相信它并不是要在 Go 中添加可变参数泛型,而只是被设计删除。

我同意 C++ 的设计与进化是一本好书,但 C++ 和 Go 有不同的目标。 最后的报价很好; Stroustrup 甚至没有提到语言用户的语言复杂性成本。 在围棋中,我们总是试图考虑这个成本。 Go 旨在成为一种简单的语言。 如果我们添加了所有可以帮助用户的功能,那就不简单了。 因为 C++ 并不简单。

有了所有的证据,提案(除了经验报告)试图避免讨论这个特性,我开始相信它并不是要在 Go 中添加可变参数泛型,而只是被设计删除。

对不起,我不知道你在这里是什么意思。

就个人而言,我一直在考虑可变泛型类型的可能性,但我从未花时间弄清楚它是如何工作的。 它在 C++ 中的工作方式非常微妙。 我想看看我们是否可以首先让非可变泛型工作。 如果可能,以后肯定有时间添加可变参数泛型。

当我批评早期的想法时,我并不是说不能做可变参数类型。 我指出我认为需要解决的问题。 如果它们无法解决,那么我不相信可变参数类型是值得的。

Stroustrup 甚至没有提到语言用户的语言复杂性成本。 在围棋中,我们总是试图考虑这个成本。 Go 旨在成为一种简单的语言。 如果我们添加了所有可以帮助用户的功能,那就不简单了。 因为 C++ 并不简单。

不是真正的国际海事组织。 必须注意,C++ 是第一个继承泛型的实践者(嗯,ML 是第一种语言)。 从我从书中读到的信息中,我得到的信息是 C++ 旨在成为一种简单的语言(一开始不提供泛型,语言设计的 Experiment-Simplify-Ship 循环,相同的历史)。 C++ 也有几年的特性冻结阶段,这就是我们在 Go“兼容性承诺”中所拥有的。 但是由于很多合理的原因,随着时间的推移,它变得有点失控,这对于 Go 是否在泛型发布后追上 C++ 的老路并不清楚。

如果可能,以后肯定有时间添加可变参数泛型。

给我同样的感觉。 在模板的第一个标准化版本中也缺少可变参数泛型。

我指出我认为需要解决的问题。 如果它们无法解决,那么我不相信可变参数类型是值得的。

我理解你的担忧。 但问题基本解决了,只是需要正确翻译成 Go(而且我猜没有人喜欢“翻译”这个词)。 我从您的历史泛型提案中读到的内容,它们基本上遵循 C++ 早期提案中失败的内容,并妥协于 Stroustrup 后悔的内容。 我对你对此的反驳很感兴趣。

我们将不得不对 C++ 的目标产生分歧。 也许最初的目标更相似,但看看今天的 C++,我认为很明显它们的目标与 Go 的目标非常不同,而且我认为至少 25 年都是如此。

在编写各种向 Go 添加泛型的提案时,我当然研究了 C++ 模板的工作原理,以及许多其他语言(毕竟,C++ 没有发明泛型)。 我没有看到 Stroustrup 后悔什么,所以如果我们来到同一个地方,那就太好了。 我的想法是 Go 中的泛型更像 Ada 或 D 中的泛型,而不是 C++。 即使在今天,C++ 也没有契约,他们称之为概念,但还没有添加到语言中。 此外,C++ 有意允许在编译时进行复杂的编程,实际上 C++ 模板本身就是一种图灵完备的语言(尽管我不知道这是否是有意的)。 我一直认为这对于 Go 来说是要避免的,因为复杂性是极端的(尽管由于方法重载和解析,它在 C++ 中比在 Go 中更复杂,而 Go 没有)。

在尝试了大约一个月的当前合约实现之后,我有点想知道现有内置函数的命运是什么。 所有这些都可以以通用方式实现:

func Append(type T)(slice []T, elems ...T) []T {...}
func Copy(type T)(dst, src []T) int {...}
func Delete(type K, V)(m map[K]V, k K) {...}
func Make(type T, I Integer(I))(siz ...I) T {...}
func New(type T)() *T {...}
func Close(type T)(c chan<- T) {...}
func Panic(type T)(v T) {...}
func Recover(type T)() T {...}
func Print(type ...T)(args ...T) {...}
func Println(type ...T)(args ...T) {...}

他们会在 Go2 中消失吗? Go 2 如何应对对现有 Go 1 代码库的巨大影响? 这些似乎是悬而未决的问题。

而且,这两个有点特别:

func Len(type T C)(t T) int {...}
func Cap(type T C)(t T) int {...}

如何用当前设计实现这样的契约C ,使得类型参数只能是通用切片[]Ts 、映射map[Tk]Tv和通道chan Tc T Ts Tk Tv Tc有什么不同?

@changkun我不认为“它们可以用泛型实现”是删除它们的令人信服的理由。 你提到了一个非常明确和强有力的理由,为什么它们不应该被删除。 所以我认为他们不会。 我认为这使其余的问题过时了。

@changkun我不认为“它们可以用泛型实现”是删除它们的令人信服的理由。 你提到了一个非常明确和强有力的理由,为什么它们不应该被删除。

是的,我同意这不是删除它们的说服力,这就是我明确表示的原因。 然而,将它们与泛型保持一致“违反”了 Go 的现有哲学,即语言特性是正交的。 兼容性是最受关注的问题,但添加合约可能会杀死当前大量“过时”的代码。

所以我认为他们不会。 我认为这使其余的问题过时了。

让我们尽量不要忽略这个问题,并将其视为合同的真实用例。 如果有人提出类似的要求,我们如何用当前的设计来实现它?

显然,我们不会摆脱现有的预先声明的功能。

虽然可以为deleteclosepanicrecoverprintprintln编写参数化函数签名

https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-contracts.md#appendAppendCopy的部分版本。 它不完整,因为appendcopy对于string类型的第二个参数有特殊情况,当前设计草案不支持这种情况。

请注意,根据当前的设计草案,上述Make的签名无效。 Newnew不太一样,但是足够接近。

对于当前的设计草案, LenCap必须采用interface{}类型的参数,因此不会是编译时类型安全的。

https://go-review.googlesource.com/c/go/+/187317

请不要使用.go2文件扩展名,我们有模块来做这种版本的事情吗? 我了解您是否将其作为临时解决方案以在尝试合同时使生活更轻松,但请确保最后go.mod文件将小心混合go pacakges 没有需要.go2文件扩展名。 对于努力确保模块尽可能好地工作的模块开发人员来说,这将是一个打击。 使用.go2文件扩展就像是说,不,我不关心你的模块东西无论如何都会按照我的方式去做,因为我不希望我 10 岁的预模块恐龙go编译器破坏.

@gertcuykens .go2 文件仅用于实验; 当泛型进入编译器时,它们将不会被使用。

(我将隐藏我们的评论,因为它们并没有真正添加到讨论中,而且它已经足够长了。)

最近在自己设计的K语言中探索了一种新的泛型语法,因为 K 借鉴了 Go 的很多语法,所以这个 Generic 语法对于 Go 也可能有一些参考价值。

identifier<T>的问题是它与比较运算符和位运算符冲突,所以我不同意这种设计。

Scala 的identifier[T]的观感比之前的设计更好,但是在解决了上面的冲突之后,它又和索引设计identifier[index]有了新的冲突。
为此,Scala 的索引设计已更改为identifier(index) 。 这对于已经使用[]作为索引的语言效果不佳。

在 Go 的草稿中,声明泛型使用(type T) ,不会造成冲突,因为type是关键字,但是编译器在调用解析identifier(type)(params)时仍然需要更多的判断

一次偶然的机会,我想起了 OC 中方法调用的特殊设计,这给了我一个新设计的灵感。

如果我们把标识符和泛型作为一个整体放在[]中会怎样?
我们可以得到[identifier T] 。 这种设计与索引不冲突,因为它必须至少有两个元素,用空格隔开。
当有多个泛型时,我们可以这样写[identifier T V] ,不会和现有的设计冲突。

将这种设计代入 Go 中,我们可以得到以下示例。
例如

type [Item T] struct {
    Value T
}

func (it [Item T]) Print() {
    println(it.Value)
}

func [TestGenerics T V]() {
    var a = [Item T]{}
    a.Print()
    var b = [Item V]{}
    b.Print()
}

func main() {
    [TestGenerics int string]()
}

这看起来很清楚。

使用[]的另一个好处是它继承了 Go 原始的 Slice 和 Map 设计,不会造成碎片感。

[]int  ->  [slice int]

map[string]int  ->  [map string int]

我们可以做一个更复杂的例子

var a map[int][]map[string]map[string][]string

var b [map int [slice [map string [map string [slice string]]]]]

这个例子还是保持了比较清晰的效果,同时对编译的影响也很小。

我已经在 K 中实现并测试了这个设计,它运行良好。

我觉得这个设计有一定的参考价值,可能值得讨论。

最近在自己设计的K语言中探索了一种新的泛型语法,因为 K 借鉴了 Go 的很多语法,所以这个 Generic 语法对于 Go 也可能有一些参考价值。

identifier<T>的问题是它与比较运算符和位运算符冲突,所以我不同意这种设计。

Scala 的identifier[T]的观感比之前的设计更好,但是在解决了上面的冲突之后,它又和索引设计identifier[index]有了新的冲突。
为此,Scala 的索引设计已更改为identifier(index) 。 这对于已经使用[]作为索引的语言效果不佳。

在 Go 的草稿中,声明泛型使用(type T) ,不会造成冲突,因为type是关键字,但是编译器在调用解析identifier(type)(params)时仍然需要更多的判断

一次偶然的机会,我想起了 OC 中方法调用的特殊设计,这给了我一个新设计的灵感。

如果我们把标识符和泛型作为一个整体放在[]中会怎样?
我们可以得到[identifier T] 。 这种设计与索引不冲突,因为它必须至少有两个元素,用空格隔开。
当有多个泛型时,我们可以这样写[identifier T V] ,不会和现有的设计冲突。

将这种设计代入 Go 中,我们可以得到以下示例。
例如

type [Item T] struct {
    Value T
}

func (it [Item T]) Print() {
    println(it.Value)
}

func [TestGenerics T V]() {
    var a = [Item T]{}
    a.Print()
    var b = [Item V]{}
    b.Print()
}

func main() {
    [TestGenerics int string]()
}

这看起来很清楚。

使用[]的另一个好处是它继承了 Go 原始的 Slice 和 Map 设计,不会造成碎片感。

[]int  ->  [slice int]

map[string]int  ->  [map string int]

我们可以做一个更复杂的例子

var a map[int][]map[string]map[string][]string

var b [map int [slice [map string [map string [slice string]]]]]

这个例子还是保持了比较清晰的效果,同时对编译的影响也很小。

我已经在 K 中实现并测试了这个设计,它运行良好。

我觉得这个设计有一定的参考价值,可能值得讨论。

伟大的

在反复阅读和反复阅读之后,我总体上支持当前 Go 中的 Contracts 设计草案。 我很感激为此付出的时间和精力。 虽然范围、概念、实现和大多数权衡似乎都是合理的,但我担心的是需要彻底检查语法以提高可读性。

我写了一系列提议的更改来解决这个问题:

关键点是:

  • 合约声明的方法调用/类型断言语法
  • “空合约”
  • 非括号分隔符

冒着抢先写这篇文章的风险,我将给出一些没有解释的语法,它们是从当前合同设计草案中的示例转换而来的。 请注意, F«T»形式的分隔符是说明性的,而不是规定性的; 有关详细信息,请参阅文章。

type List«type Element contract{}» struct {
    next *List«Element»
    val  Element
}

contract viaStrings«To, From» {
    To.Set(string)
    From.String() string
}

func SetViaStrings«type To, From viaStrings»(s []From) []To {
    r := make([]To, len(s))
    for i, v := range s {
        r[i].Set(v.String())
    }
    return r
}

func Keys«type K comparable, V contract{}»(m map[K]V) []K {
    r := make([]K, 0, len(m))
    for k := range m {
        r = append(r, k)
    }
    return r
}

k := maps.Keys(map[int]int{1:2, 2:4})

contract Numeric«T» {
    T.(int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        complex64, complex128)
}

func DotProduct«type T Numeric»(s1, s2 []T) T {
    if len(s1) != len(s2) {
        panic("DotProduct: slices of unequal length")
    }
    var r T
    for i := range s1 {
        r += s1[i] * s2[i]
    }
    return r
}

在没有真正改变引擎盖下的合同的情况下,这对我作为 Go 开发人员来说更具可读性。 我也更有信心将这种形式给正在学习围棋的人(尽管在课程后期)。

@ianlancetaylor根据您在https://github.com/golang/go/issues/36533#issuecomment -579484523 上的评论,我将在此线程中发布,而不是开始新问题。 它也列在泛型反馈页面上。 不确定我是否需要做任何其他事情才能“正式考虑”它(即Go 2 提案审查小组?),或者是否仍在积极收集反馈。

从合同设计草案:

为什么不像 C++ 和 Java 那样使用F<T>语法?
在解析函数中的代码时,例如v := F<T> ,在看到<时,我们看到的是类型实例化还是使用<运算符的表达式是模棱两可的。 解决这个问题需要有效的无限前瞻。 一般来说,我们努力使 Go 解析器保持简单。

与我的上一篇文章并不特别冲突: Go Contracts 的角括号分隔符

只是关于如何绕过解析器感到困惑的这一点的一些想法。 几个样本:

// Lifted from the design draft
func New<type K, V>(compare func(K, K) int) *Map<K, V> {
    return &Map{<K, V> compare: compare}
}

// ...

func (m *Map<K, V>) InOrder() *Iterator<K, V> {
    sender, receiver := chans.Ranger(<keyValue<K, V>>)
    var f func(*node<K, V>) bool
    f = func(n *node<K, V>) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue{<K, V> n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

本质上,在<可能不明确的情况下,类型参数的位置不同。

@toolbox关于您的尖括号评论。 谢谢,但对我个人而言,语法读起来就像首先决定我们必须对类型参数和类型参数使用尖括号,然后找出一种方法来敲入它们。我认为如果我们向 Go 添加泛型,我们需要瞄准适合干净轻松地融入现有语言的东西。 我不认为在大括号内移动尖括号可以实现这一目标。

是的,这是一个小细节,但我认为在语法方面小细节非常重要。 我认为如果我们要添加类型实参和参数,它们需要以简单直观的方式工作。

我当然不会声称当前设计草案中的语法是完美的,但我确实声称它很容易融入现有语言。 我们现在需要做的是编写更多示例代码,看看它在实践中的效果如何。 一个关键点是:人们实际上多久必须在函数声明之外编写类型参数,这些情况有多令人困惑? 我想我们不知道。

[]用于泛型类型,将()用于泛型函数是一个好主意吗? 这将更符合当前的核心泛型。

社区可以投票吗? 就我个人而言,我更喜欢 _anything_ 而不是添加更多括号,已经很难阅读闭包等的一些函数定义,这会增加更多的混乱

我不认为投票是设计语言的好方法。 尤其是在很难(可能不可能)确定和难以置信的大量合格选民的情况下。

我相信 Go 设计者和社区能够汇聚到最佳解决方案和
所以觉得没有必要在这次谈话中权衡任何事情。
然而,我不得不说我是多么意外地高兴
F«T» 语法的建议。

(其他 Unicode 括号:
https://unicode-search.net/unicode-namesearch.pl?term=BRACKET。)

干杯,

  • 鲍勃

2020 年 5 月 1 日星期五晚上 7:43,Matt Mc [email protected]写道:

在反复阅读和反复阅读之后,我总体上支持
当前 Go 中的 Contracts 设计草案。 我很感激时间
以及投入其中的努力。 而范围、概念、
实施,并且大多数权衡似乎是合理的,我担心的是
语法需要大修以提高可读性。

我写了一系列提议的更改来解决这个问题:

关键点是:

  • 合约声明的方法调用/类型断言语法
  • “空合约”
  • 非括号分隔符

冒着抢文章的风险,我给几张不支持的
语法,从当前合同设计草案中的示例转换而来。 笔记
F«T» 形式的分隔符是说明性的,而不是规定性的; 看
有关详细信息的文章。

类型列表«类型元素合同{}»结构{
下一个 *列表«元素»
val 元素
}

合同 viaStrings«To, From» {
To.Set(字符串)
From.String() 字符串
}
func SetViaStrings«type To, From viaStrings»(s []From) []To {
r := make([]To, len(s))
对于 i, v := 范围 s {
r[i].Set(v.String())
}
返回 r
}

func Keys«type K compatible, V contract{}»(m map[K]V) []K {
r := make([]K, 0, len(m))
对于 k := 范围 m {
r = 附加(r,k)
}
返回 r
}
k := maps.Keys(map[int]int{1:2, 2:4})

合同数字«T» {
T.(int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
浮动32,浮动64,
复杂64,复杂128)
}
func DotProduct«type T Numeric»(s1, s2 []T) T {
如果 len(s1) != len(s2) {
恐慌(“DotProduct:不等长度的切片”)
}
变量 r T
对于 i := 范围 s1 {
r += s1[i] * s2[i]
}
返回 r
}

在没有真正改变引擎盖下的合同的情况下,这远远不止
作为 Go 开发人员,我可以阅读。 我也感到更加自信
将这种形式给正在学习围棋的人(尽管在
课程)。

@ianlancetaylor https://github.com/ianlancetaylor根据您的评论
在#36533(评论)
https://github.com/golang/go/issues/36533#issuecomment-579484523我是
在此线程中发布而不是开始新问题。 也列出来了
在泛型反馈页面上
https://github.com/golang/go/wiki/Go2GenericsFeedback 。 不确定我是否
需要做任何其他事情才能“正式考虑”(即 Go 2
提案审查组https://github.com/golang/go/issues/33892 ?) 或者如果
仍在积极收集反馈。


您收到此消息是因为您订阅了此线程。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/15292#issuecomment-622657596 ,或
退订
https://github.com/notifications/unsubscribe-auth/AACQ2NJRBNLLDGY2XGCCQCLRPOCEHANCNFSM4CA35RXQ
.

我们都想要最好的 Go 语法。 设计草案使用括号是因为它可以与 Go 的其余部分一起使用,而不会导致明显的解析歧义。 我们一直和他们在一起,因为他们是当时我们心目中最好的解决方案,而且还有更大的鱼要炸。 到目前为止,它们(括号)表现得相当好。

归根结底,如果找到更好的表示法,那很容易改变,只要我们没有遵守兼容性保证(解析器经过微调,任何代码体都可以转换轻松使用 gofmt)。

@ianlancetaylor感谢您的回复,非常感谢。

你说的对; 该语法是“不要对类型参数使用括号”并选择我认为最好的候选者,然后进行更改以尝试缓解解析器的实现问题。

如果语法难以阅读,(很难一目了然地知道发生了什么)它真的很容易融入现有的语言吗? 这就是我认为立场不足的地方。

确实,正如您所提到的,类型推断可以大大减少需要在客户端代码中传递的类型参数的数量。 我个人认为库作者在使用他们的代码时应该努力要求传递类型的参数,但它会在实践中发生。

昨晚,一次偶然的机会,我遇到了 D 的模板语法,它在某些方面惊人地相似:

template Square(T) {
    T Square(T t) {
        return t * t;
    }
}

writefln("The square of %s is %s", 3, Square!(int)(3));

template TCopy(T) {
    void copy(out T to, T from) {
        to = from;
    }
}

int i;
TCopy!(int).copy(i, 3);

我看到有两个主要区别:

  1. 他们将!作为实例化运算符来使用模板。
  2. 他们的声明风格(没有多个返回值,方法嵌套在类中)意味着普通代码中的括号本来就更少,因此对类型参数使用括号不会产生相同的视觉歧义。

实例化运算符

使用合同时,主要的视觉歧义是在实例化和函数调用(或类型转换,或...?)之间。 这是有问题的部分原因是实例化是编译时的,而函数调用是运行时的。 Go 有很多视觉线索可以告诉读者每个子句属于哪个阵营,但是新的语法混淆了这些,所以如果你正在查看类型或程序流,这并不明显。

一个人为的例子:

// Instantiation with unexported types and then function call,
// or chained method call?
a := draw(square, ellipse)(canvas, color)

建议:使用实例化操作符来指定类型参数。 D 使用的!似乎完全可以接受。 一些示例语法:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map!(K, V) {
    return &Map!(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator!(K, V) {
    sender, receiver := chans.Ranger!(keyValue!(K, V))()
    var f func(*node!(K, V)) bool
    f = func(n *node!(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue!(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

从我个人的角度来看,上面的代码更容易阅读。 我认为这消除了视觉上和解析器的所有歧义。 此外,我发现自己想知道这是否可能是可以对合同进行的最重要的更改。

声明风格

在声明类型、函数和方法时,“运行时还是编译时?”更少了。 问题。 Gopher 看到以typefunc开头的行并知道他正在查看声明,而不是程序行为。

但是,仍然存在一些视觉上的歧义:

// Type-parameterized function,
// or function with multiple return values?
func Draw(cvs canvas, t tool)(canvas, tool) {
    // ...
}
func Draw(type canvas, tool)(cvs canvas, t tool) {
    // ...
}

// Type-parameterized struct, or function call?
func Set(elem constructible) rect {
    // ...
}
type Set(type Elem comparable) struct{
    // ...
}

// Method call, or type-parameterized function?
func Map(type Element)(s []Element, f func(Element) Element) (results []Element) {
    // ...
}
func (t Element) Map(s []Element, f func(Element) Element) (results []Element) {
    // ...
}

想法:

  • 我认为这些问题不如实例化问题重要。
  • 最明显的解决方案是更改用于类型参数的分隔符。
  • 可能在其中放置某种其他类型的运算符或字符( !可能会丢失, #呢?)可以消除歧义。

编辑: @griesemer感谢您的额外澄清!

谢谢。 只是提出一个自然的问题:为什么知道特定调用是在运行时还是在编译时评估很重要? 为什么这是关键问题?

@工具箱

// Instantiation with unexported types and then function call,
// or chained method call?
a := draw(square, ellipse)(canvas, color)

为什么这两种方式都重要? 对于普通读者来说,这是否是在编译时或运行时执行的一段代码并不重要。 对于其他人,他们只需看一眼函数的定义就知道发生了什么。 你后来的例子似乎一点也不模棱两可。

事实上,使用()作为类型参数是有道理的,因为看起来你正在调用一个返回函数的函数——这或多或少是正确的。 不同之处在于第一个函数接受类型,这些类型通常是大写的,或者是众所周知的。

在这个阶段,更重要的是弄清楚棚子的尺寸,而不是它的颜色。

我不认为@toolbox所说的实际上是编译时和运行时之间的区别。 是的,这是一个区别,但这不是重要的区别。 重要的是:这是函数调用还是类型声明? 您知道是因为它们的行为不同,并且您不想推断某个表达式是在进行两个函数调用还是一个,因为这是一个很大的区别。 即像a := draw(square, ellipse)(canvas, color)这样的表达式在不检查周围环境的情况下是模棱两可的。

能够直观地解析程序的控制流很重要。 我认为 Go 就是一个很好的例子。

谢谢。 只是提出一个自然的问题:为什么知道特定调用是在运行时还是在编译时评估很重要? 为什么这是关键问题?

抱歉,好像我的交流搞砸了。 这是我试图解决的关键点:

如果您正在查看类型或程序流程,这并不明显

(目前,一个在编译期间被整理出来,另一个在运行时发生,但这些是......特征,而不是关键点, @infogulch正确地选择了 - 谢谢!)


我在一些地方看到了草案中的泛型可以比作函数调用的观点:它是一种返回真实函数或类型的编译时函数。 虽然这作为编译过程中发生的事情的心理模型很有帮助,但它并不能在语法上进行翻译。 从语法上讲,它们应该被命名为函数。 这是一个例子:

// Example from the Contracts draft
Print(int)([]int{1, 2, 3})

// New naming that communicates behavior and intent
MakePrintFunc(int)([]int{1, 2, 3}) // Chained function call, great!

在那里,它实际上看起来像一个返回函数的函数; 我认为这很有可读性。

另一种解决方法是使用Type所有内容添加后缀,因此从名称中可以清楚地看出,当您“调用”该函数时,您将获得一个类型。 否则,(例如) Pair(...)生成结构类型而不是结构并不明显。 但是,如果该约定到位,则此代码变得清晰: a := drawType(square, ellipse)(canvas, color)

(我意识到一个先例是接口的“-er”约定。)

请注意,我并不特别支持将上述作为解决方案,我只是说明我认为“泛型作为函数”没有完全和明确地用当前语法表达。


同样, @infogulch 很好地总结了我的观点。 我支持在视觉上区分类型参数,以便清楚它们是type 的一部分

也许它的视觉部分将通过编辑器的语法突出显示来增强。

我不太了解解析器以及您如何不能做太多的前瞻。

从用户的角度来看,我不想在我的代码中看到另一个字符,所以«»不会得到我的支持(我没有在我的键盘上找到它们!)。

但是,看到圆括号后面跟着圆括号也不是很赏心悦目。

使用大括号简单如何?

a := draw{square, ellipse}(canvas, color)

不过,在Print(int)([]int{1,2,3})中,唯一的行为差异“编译时与运行时”。 是的, MakePrintFunc而不是Print会更加强调这种相似性,但是……这不是使用MakePrintFunc的论据吗? 因为它实际上隐藏了真正的行为差异。

FWIW,如果您似乎在争论对参数函数和参数类型使用不同的分隔符的话。 因为Print(int)实际上可以被认为等同于返回函数的函数(在编译时评估),而Pair(int, string)不能 - 它是返回类型的函数。 Print(int)实际上是一个有效的表达式,它的计算结果是一个func -value,而Pair(int, string)不是一个有效的表达式,它是一个类型规范。 因此,使用上的真正区别不是“泛型与非泛型函数”,而是“泛型函数与泛型类型”。 从那个 POV 来看,我认为至少有充分的理由将()用于参数函数,因为它强调了参数函数实际表示值的性质——也许我们应该使用<>用于参数类型。

我认为参数类型的()的论点来自函数式编程,其中这些函数返回类型是一个真正的概念,称为类型构造函数,实际上可以作为函数使用和引用。 FWIW,这也是为什么不会争辩不将()用于参数类型的原因。 就个人而言,我对这个概念非常满意,我更喜欢使用更少的分隔符,而不是从参数类型中消除参数函数的歧义——毕竟,我们对引用类型或值的纯标识符没有任何问题。 .

我不认为@toolbox所说的实际上是编译时和运行时之间的区别。 是的,这是一个区别,但这不是重要的区别。 重要的是:这是函数调用还是类型声明? 您_想_知道,因为它们的行为不同,并且您不想推断某个表达式是在进行两个函数调用还是一个函数调用,因为这是一个很大的区别。 即像a := draw(square, ellipse)(canvas, color)这样的表达式在不检查周围环境的情况下是模棱两可的。

能够直观地解析程序的控制流很重要。 我认为 Go 就是一个很好的例子。

类型声明很容易看到,因为它们都以关键字type开头。 你的例子显然不是其中之一。

也许它的视觉部分将通过编辑器的语法突出显示来增强。

我认为,理想情况下,无论它是什么颜色,语法都应该清晰。 Go 就是这种情况,我认为放弃该标准并不好。

使用大括号简单如何?

我相信这不幸与结构文字冲突。

不过,在Print(int)([]int{1,2,3})中,唯一的行为差异是“编译时与运行时”。 是的, MakePrintFunc而不是Print会更加强调这种相似性,但是……这不是不使用MakePrintFunc的论据吗? 因为它实际上隐藏了真正的行为差异。

好吧,首先,这就是为什么我会支持Print!(int)([]int{1,2,3})而不是MakePrintFunc(int)([]int{1,2,3}) 。 很明显,正在发生一些独特的事情。

但同样, @ianlancetaylor之前提出的问题:如果类型实例化/函数返回函数是编译时还是运行时,为什么这很重要?

考虑一下,如果您编写了一些函数调用并且编译器能够优化它们并在编译时计算它们的结果,那么您会为性能提升感到高兴! 相反,重要的方面是代码在做什么,行为是什么? 这应该一目了然。

当我看到Print(...)时,我的第一直觉是“这是一个写入某处的函数调用”。 它没有传达“这将返回一个函数”。 在我看来,这些中的任何一个都更好,因为它可以传达行为和意图:

  • MakePrintFunc(...)
  • Print!(...)
  • Print<...>

换句话说,这段代码“引用”或以某种方式“给了我”一个现在可以在下面的代码中调用的函数。

FWIW,如果您似乎在争论对参数函数和参数类型使用不同的分隔符的话。 ...

不,我知道最后几个例子是关于函数的,但我主张参数函数和参数类型的语法一致。 我不相信 Go 团队会将泛型添加到 Go 中,除非它们是具有统一语法的统一概念。

当我看到Print(...)时,我的第一直觉是“这是一个写入某处的函数调用”。 它没有传达“这将返回一个函数”。

func Print(…) func(…)也没有,当被称为Print(…)时。 然而,我们对此感到满意。 没有特殊的调用语法,如果函数返回func
Print(…)语法几乎可以准确地告诉您它今天所做的事情: Print是一个返回一些值的函数,这就是Print(…)的计算结果。 如果您对函数返回的类型感兴趣,请查看其定义。
或者,更可能的是,使用它实际上是Print(…)(…)的事实作为它返回函数的指示符。

考虑一下,如果您编写了一些函数调用并且编译器能够优化它们并在编译时计算它们的结果,那么您会为性能提升感到高兴!

当然。 我们已经有了。 而且我很高兴我不需要特定的语法注释来使它们变得特别,但可以相信编译器将提供不断改进的启发式方法来判断这些函数是什么。

在我看来,这些中的任何一个都更好,因为它可以传达行为和意图:

请注意,第一个至少 100% 与设计兼容。 它没有为使用的标识符规定任何形式,我希望你不要建议这样做(如果你这样做,我很想知道为什么相同的规则不适用于仅返回func )。

不,我知道最后几个例子是关于函数的,但我主张参数函数和参数类型的语法一致。

好吧,我同意,正如我所说:) 我只是说我不明白您提出的论点如何沿“通用与非通用”轴应用,因为两者之间没有重要的行为变化他们俩。 它们在“类型与函数”轴上有意义的,因为某个东西是类型规范还是表达式对于它可以使用的上下文非常重要。我仍然不同意,但至少我会理解他们 :)

@Merovius感谢您的评论。

func Print(…) func(…)也没有,当被称为Print(…)时。 然而,我们对此感到满意。 没有特殊的调用语法,如果一个函数返回一个 func。
Print(…)语法几乎可以准确地告诉您它今天所做的事情: Print是一个返回一些值的函数,这就是Print(…)的计算结果。 如果您对函数返回的类型感兴趣,请查看其定义。

我认为函数的名称应该与它的作用相关。 因此,我希望Print(...)打印一些东西,不管它返回什么。 我相信这是一个合理的期望,并且可以发现在大多数现有 Go 代码中都可以实现。

如果我看到Print(...)(...)它表明第一个()打印了一些东西,并且该函数返回了某种函数,第二个()正在执行该附加行为.

(如果这是一个不寻常或罕见的意见,我会感到惊讶,但我不会与一些调查结果争论。)

请注意,第一个至少 100% 与设计兼容。 它没有为使用的标识符规定任何形式,我希望您不要建议这样做(如果您这样做,我会很感兴趣为什么相同的规则不适用于仅返回一个 func)。

你说得对,我建议:)

看,我列出了我能想到的 3 种方法来解决函数和类型的类型参数引入的视觉歧义。 如果您没有看到任何歧义,那么您将不会喜欢任何建议!

我只是说我不明白您提出的论点如何沿“通用与非通用”轴应用,因为两者之间没有重要的行为变化。 它们在“类型与函数”轴上是有意义的,因为某个东西是类型规范还是表达式对于它可以使用的上下文非常重要。

请参阅以上关于歧义的观点和 3 个建议的解决方案。

类型参数是一个新事物。

  • 如果我们想将它们作为新事物进行推理,那么我建议更改分隔符或添加实例化运算符以将它们与常规代码完全区分开来:函数调用、类型转换等。
  • 如果我们想将它们作为另一个函数进行推理,那么我建议明确命名这些函数,以便identifier中的identifier(...)传达行为和返回值。

更喜欢前者。 如所讨论的,在这两种情况下,更改都将在类型参数语法中是全局的。

还有其他几种方法可以阐明这一点:

  1. 民意调查
  2. 教程

1. 调查

前言:这不是民主。 我确实认为决策是基于数据的,清晰的逻辑和广泛的调查数据都可以帮助决策过程。

我没有办法做到这一点,但我很想知道如果你对几千名 Gophers 进行“按清晰度排序”调查会发生什么。

基线:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map(K, V) {
    return &Map(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator(K, V) {
    sender, receiver := chans.Ranger(keyValue(K, V))()
    var f func(*node(K, V)) bool
    f = func(n *node(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

实例化运算符:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map!(K, V) {
    return &Map!(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator!(K, V) {
    sender, receiver := chans.Ranger!(keyValue!(K, V))()
    var f func(*node!(K, V)) bool
    f = func(n *node!(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue!(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

尖括号:(或双尖括号,无论哪种方式)

// Lifted from the design draft
func New<type K, V>(compare func(K, K) int) *Map<K, V> {
    return &Map<K, V>{compare: compare}
}

// ...

func (m *Map<K, V>) InOrder() *Iterator<K, V> {
    sender, receiver := chans.Ranger<keyValue<K, V>>()
    var f func(*node<K, V>) bool
    f = func(n *node<K, V>) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue<K, V>{n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

适当命名的函数:

// Lifted from the design draft
func NewConstructor(type K, V)(compare func(K, K) int) *MapType(K, V) {
    return &MapType(K, V){compare: compare}
}

// ...

func (m *MapType(K, V)) InOrder() *IteratorType(K, V) {
    sender, receiver := chans.RangerType(keyValueType(K, V))()
    var f func(*nodeType(K, V)) bool
    f = func(n *nodeType(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValueType(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

...有趣的是,我其实很喜欢最后一个。

(你认为这些在 Gophers @Merovius的广阔世界中会怎样?)

2.教程

我认为这将是一个非常有用的练习:为您最喜欢的语法编写一个适合初学者的教程,并让一些人阅读并应用它。 这些概念的交流有多容易? 常见问题解答是什么,您如何回答?

设计草案旨在将概念传达给经验丰富的 Gophers。 它遵循逻辑链,慢慢地让你沉浸其中。 简洁版是什么? 您如何在一篇易于理解的博文中解释合同的黄金法则?

与典型的反馈报告相比,这可能会呈现一种不同的角度或数据片段。

@toolbox我认为您尚未回答的是:为什么这是参数函数的问题,而不是返回func的非参数函数? 我今天可以写

func Print(a string) func(string) {
    return func(b string) {
        fmt.Println(a+b)
    }
}

func main() {
    Print("foo")("bar")
}

为什么这没关系,并且不会让您对歧义感到非常困惑,但是一旦Print采用类型参数而不是值参数,这就会变得难以忍受? 您是否会(撇开明显的兼容性问题不谈)是否还建议我们添加一个限制以使其正常运行,这是不可能的,除非将Print重命名为MakeXFunc对于某些X ? 如果不是,为什么不呢?

@toolbox当假设类型推断很可能消除为函数指定参数类型的需要时,这真的会成为一个问题,而只留下一个看起来很简单的函数调用吗?

@Merovius我认为问题不在于语法Print("foo")("bar")本身,因为它已经在 Go 1 中成为可能,正是因为它有一个可能的解释。 问题在于,在未修改的提案中,表达式Foo(X)(Y)现在是模棱两可的,可能意味着您正在进行两次函数调用(如 Go 1 中),或者可能意味着您正在使用类型参数进行一次函数调用. 问题是能够在本地推断程序做了什么,而这两种可能的语义解释是非常不同的。

@urandom我同意类型推断可能能够消除大量显式提供的类型参数,但我不认为仅仅因为它们很少使用就将所有认知复杂性推到语言的黑暗角落是一个好主意任何一个。 即使它非常罕见,以至于大多数人通常不会遇到它们,但有时他们仍然会遇到它,并且允许某些代码具有混乱的控制流,只要它不是“大多数”代码,就会在我的嘴里留下不好的味道。 特别是因为 Go 目前在阅读包括 stdlib 在内的“管道”代码时是如此平易近人。 也许类型推断太好了,以至于“稀有”变成了“从不”,而 Go 程序员保持高度自律,从不设计需要类型参数的系统; 那么这整个问题基本上是没有实际意义的。 但我不会打赌。

我认为@tooolbox的主要观点是,我们不应该轻率地用上下文相关的语义重载现有语法,而应该找到一些其他没有歧义的语法(即使它只是做一个小的补充,例如Foo(X)!(Y) 。)我认为这是考虑语法选项时的重要衡量标准。

在过去(~2008-2009),我使用并阅读了一些D代码,我必须说!总是让我绊倒。

让我用#$@来画这个棚子(因为它们在 Go 或 C 中没有任何意义)。
这可以打开使用花括号的可能性,而不会与地图、切片或结构混淆。

  • Foo@{X}(Y)
  • Foo${X}(Y)
  • Foo#{X}(Y)
    或方括号。

在这样的讨论中,查看真实代码是必不可少的。

例如,考虑到很少有人写Foo(X)(Y) 。 在 Go 中,类型名称、变量名称和函数名称看起来完全一样,但人们很少对他们所看到的内容感到困惑。 人们明白int64(v)是类型转换,而F(v)是函数调用,尽管它们看起来完全一样。

我们需要查看真实代码,看看类型参数在实践中是否真的令人困惑。 如果是,那么我们必须调整语法。 在没有真正的代码的情况下,我们根本不知道。

2020 年 5 月 6 日,星期三,13:00,伊恩·兰斯·泰勒 (Ian Lance Taylor) 写道:

人们明白int64(v)是一种类型转换,而F(v)是一种
函数调用,即使它们看起来完全相同。

我现在对提案没有任何意见
语法,但我认为这个特定的例子不是很好。 有可能
对于内置类型是正确的,但我实际上对此感到困惑
我自己几次确切的问题(我正在寻找一个功能
定义并且对代码之前的工作方式感到非常困惑
我意识到它可能是一种类型,但我找不到该功能,因为
这根本不是函数调用)。 不是世界末日,而且
对于喜欢花哨的 IDE 的人来说可能根本不是问题,但我已经
为此多次浪费了 5 分钟左右的时间。

——山姆

--
山姆·怀特

@ianlancetaylor我在您的示例中注意到的一件事是,您可以编写一个函数,该函数采用一种类型并返回具有相同含义的另一种类型,因此将类型称为int64(v)这样的基本类型转换在与strconv.Atoi(v)的意义相同。

但是虽然你可以做UseConverter(strconv.Atoi) ,但UseConverter(int64)在 Go 1 中是不可能的。如果泛型可用于以下类型的转换,则类型参数的括号可能会带来一些可能性:

func StrToNumber(type K)(s string) K {
  asInt := strconb.Atoi(s)
  return K(asInt)
}

为什么这没关系,并且不会让你对歧义感到超级困惑

你的例子不行。 我不在乎第一次调用是否接受参数或类型参数。 您有一个不打印任何内容的Print函数。 你能想象阅读/审查该代码吗? 省略第二组括号的Print("foo")看起来不错,但暗中是无操作的。

如果您在 PR 中将该代码提交给我,我会告诉您将名称更改为PrintFuncMakePrintFuncPrintPlusFunc或传达其行为的其他名称。

在过去(~2008-2009),我使用并阅读了一些 D 代码,我必须说! 总是绊倒我。

哈,有趣。 我对实例化运算符没有任何特别的偏好。 这些似乎是不错的选择。

在 Go 中,类型名称、变量名称和函数名称看起来完全一样,但人们很少对他们所看到的内容感到困惑。 人们理解 int64(v) 是一种类型转换,而 F(v) 是一个函数调用,尽管它们看起来完全一样。

我同意,人们通常可以快速区分类型转换和函数调用。 你认为这是为什么?

我个人的理论是类型通常是名词,功能通常是动词。 因此,当您看到Noun(...)时,很明显这是一个类型转换,而当您看到Verb(...)时,它是一个函数调用。

我们需要查看真实代码,看看类型参数在实践中是否真的令人困惑。 如果是,那么我们必须调整语法。 在没有真正的代码的情况下,我们根本不知道。

这就说得通了。

就个人而言,我之所以来到这个线程,是因为我阅读了合同草稿(可能 5 次,每次都会反弹,然后当我稍后回来时会更进一步)并且发现语法令人困惑和不熟悉。 当我最终摸索这些概念时,我很喜欢这些概念,但由于语法模棱两可,存在巨大的障碍。

合同草稿的底部有很多“真实代码”,处理所有这些常见用例,太棒了! 但是,我发现视觉解析很棘手; 我阅读和理解代码的速度较慢。 在我看来,我必须查看事物的参数和更广泛的上下文才能了解事物是什么以及控制流是什么,这似乎是常规代码的一个步骤。

让我们来看看这个真实的代码:

import "container/orderedmap"

var m = orderedmap.New(string, string)(strings.Compare)

func Add(a, b string) {
    m.Insert(a, b)
}

当我阅读orderedmap.New(时,我希望下面是New函数的参数,即有序映射需要运行的关键信息。 但这些实际上在第二组括号中。 我被这个扔了。 它使代码更难理解。

(这只是一个例子,并不是我所看到的一切都是模棱两可的,但很难就广泛的观点进行详细讨论。)

这是我的建议:

// Instantiation operator
var m = orderedmap.New!(string, string)(strings.Compare)
// Alternate delimiters -- notice I don't insist on any particular kind
var m = orderedmap.New<|string, string|>(strings.Compare)
// Appropriately named function
var m = orderedmap.MakeConstructor(string, string)(strings.Compare)

在前两个示例中,不同的语法打破了我的假设,即第一组括号包含New()的参数,因此代码不那么令人惊讶,而且流程更容易从高层观察到。

第三个选项使用命名来使流程不足为奇。 我现在期望第一组括号包含创建构造函数所需的参数,并且我期望返回值是构造函数,该构造函数又可以被调用以生成有序映射。


我可以肯定地阅读当前风格的代码。 我能够阅读合同草案中的所有代码。 它只是更慢,因为我需要更长的时间来处理它。 我已经尽力分析为什么会这样并报告它:除了orderedmap.New示例之外, https: //github.com/golang/go/issues/15292#issuecomment -623649521 有一个很好的总结,虽然我可能会想出更多。 歧义程度在不同示例之间有所不同。

我承认我不会得到所有人的同意,因为可读性和清晰度有些主观,可能会受到个人背景和喜欢的语言的影响。 不过,我确实认为 4 种解析歧义是我们遇到问题的一个很好的指标。

import "container/orderedmap"

var m = orderedmap.NewOf(string, string)(strings.Compare)

func Add(a, b string) {
    m.Insert(a, b)
}

我认为NewOfNew $ 读起来更好,因为New通常返回一个实例,而不是创建实例的泛型。


您有一个不打印任何内容的Print函数。

需要明确的是,由于存在一些自动类型推断,通用Print(foo)要么是通过推断进行的真实打印调用,要么是错误。 在今天的 Go 中,不允许使用裸标识符:

package main

import (
    "fmt"
)

func main() {
    fmt.Println
}

./prog.go:8:5: fmt.Println evaluated but not used

我确实想知道是否有某种方法可以使通用推理不那么混乱。

@工具箱

你的例子不行。 我不在乎第一次调用是否接受参数或类型参数。 您有一个不打印任何内容的 Print 功能。 你能想象阅读/审查该代码吗?

您在此处省略了相关的后续问题。 我同意你的观点,它不是真正可读的。 但是你正在争论这个约束的语言级别的执行。 我不是说“你对此没问题”的意思是“你对这个代码没问题”,而是说“你对允许该代码的语言没问题”。

这是我的后续问题。 您是否认为 Go 是一种更糟糕的语言,因为它没有对返回的函数进行名称限制 - func ? 如果不是,如果我们不对这些函数施加这种限制,为什么它会是一种更糟糕的语言,因为它们采用类型参数而不是值参数?

@Merovius

但是你正在争论这个约束的语言级别的执行。

不,他认为依赖命名标准是解决问题的潜在有效解决方案。 像“鼓励类型作者以一种不太容易与函数名称混淆的方式命名他们的泛型类型”这样的非正式规则是对歧义问题的有效解决方案,因为它可以从字面上解决个别情况下的问题。

他没有在任何地方暗示这个解决方案必须由语言强制执行,他说如果维护者决定保持当前提案不变,那么即使存在歧义问题也有潜在的实际解决方案。 他声称模棱两可的问题是真实存在的,值得考虑。

编辑:我认为我们有点偏离了方向。 我认为更多“真实”的示例代码对此时的对话非常有益。

不,他认为依赖命名标准是解决问题的潜在有效解决方案。

他们是吗? 我试图具体问:

请注意,第一个至少 100% 与设计兼容。 它没有为使用的标识符规定任何形式,我希望您不要建议这样做(如果您这样做,我会很感兴趣为什么相同的规则不适用于仅返回一个 func)。

你说得对,我建议:)

我同意“开处方”在这里并不是非常具体,但这至少是我想要的问题。 如果他们确实支持设计中内置的语言级别要求,我当然为误解表示歉意。 但我认为“规定”至少比“非正式规则”更强,我认为这是有道理的。 特别是如果放在他们提出的其他两个建议的上下文中(在相同的基础上),它们语言级别的构造,因为它们甚至不使用当前有效的标识符。

是否会有类似vgo的计划让社区尝试最新的通用提案?

在玩了一些支持契约的游乐场之后,我真的不明白需要区分类型参数和常规参数有什么大惊小怪的。

考虑这个例子。 我在所有函数上都保留了类型初始值设定项,即使我可以省略所有它们并且它仍然可以编译得很好。 这似乎表明绝大多数此类潜在代码甚至不会包含它们,这反过来不会引起任何混乱。

但是,如果包含这些类型参数,则可以进行某些观察:
a) 类型要么是内置的,每个人都知道并且可以立即识别
b) 类型是第 3 方,在这种情况下将是 TitleCased,这将使它们脱颖而出。 是的,虽然不太可能,但它可能是一个返回另一个函数的函数,并且第一次调用会消耗第 3 方导出的变量,但我认为这种情况极为罕见。
c) 类型是一些私有类型。 在这种情况下,它们看起来更像是常规变量标识符。 但是,由于它们没有被导出,这意味着读者正在查看的代码不是他们试图破译的某些文档的一部分,更重要的是,他们已经在阅读代码。 因此,他们可以做额外的步骤,只需跳转到函数的定义即可消除任何歧义。

大惊小怪是关于它在没有泛型的情况下的外观https://play.golang.org/p/7BRdM2S5dwQ以及对于像这样的 StackString、StackInt 等为每种类型编写单独的堆栈的新手来说更容易编程然后是当前通用语法建议中的 Stack(T)。 我毫不怀疑,如您的示例所示,当前的提案经过深思熟虑,但简单性和清晰性的价值因分配而降低。 我知道当务之急是通过测试来确定它是否有效,但是一旦我们同意当前的提案涵盖了大多数情况并且没有技术编译器困难,那么更高的优先级是让每个人都可以理解它,这始终是第一个原因Go 从一开始就成功。

@Merovius不,就像@infogulch所说,我的意思是在接口上创建一个约定-er 。 我在上面提到过,很抱歉造成混乱。 (顺便说一句,我是“他”。)

考虑这个例子。 我在所有函数上都保留了类型初始值设定项,即使我可以省略所有它们并且它仍然可以编译得很好。 这似乎表明绝大多数此类潜在代码甚至不会包含它们,这反过来不会引起任何混乱。

在泛型游乐场的分叉版本中,同样的例子怎么样?

我使用::<>作为类型参数子句,如果只有一个类型,您可以省略<> 。 尖括号上的解析器不应该有任何歧义,它使我很容易阅读代码——泛型和使用泛型的代码。 (如果类型参数被推断出来,那就更好了。)

正如我之前所说,对于类型实例化,我并没有被困在!上(而且我认为::在审查时看起来更好)。 它只对使用泛型有帮助,而不是在声明中。 所以这在某种程度上结合了两者,在不必要的地方省略了<> ,有点像如果只有一个函数返回参数则省略封闭()

示例摘录:

type Stack::<type E> []E

func (s Stack::E) Peek() E {
    return s[len(s)-1]
}

func (s *Stack::E) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack::E) Push(value E) {
    *s = append(*s, value)
}

type StackIterator::<type E> struct{
    stack Stack::E
    current int
}

func (s *Stack::E) Iter() Iterator::E {
    it := StackIterator::E{stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator::E) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator::E) Value() E { 
    if i.current < 0 {
        var zero E
        return zero
    }

    return i.stack[i.current]
}

// ...

var it Iterator::string = stack.Iter()

it = Filter::string(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::<string, string>(it, func(s string) string {
    return s + ":1"
})

it = Distinct::string(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

对于这个例子,我还调整了变量名,我认为“元素”的E比“类型”的T更具可读性。

正如我所说,通过使泛型看起来不同,底层的 Go 代码变得可见。 你知道你在看什么,控制流很明显,没有歧义等等。

更多类型推断也很好:

var it Iterator::string = stack.Iter()

it = Filter(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::<string, string>(it, func(s string) string {
    return s + ":1"
})

it = Distinct(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

@toolbox抱歉,然后,我们正在互相交谈:)

对像 StackString、StackInt 等每种类型都编写单独的 Stack 不熟悉的人比 Stack(T) 更容易编程

如果是这样的话,我真的会感到惊讶。 没有人是万无一失的,即使是一段简单的代码,第一个潜入的 bug 也会影响到该语句从长远来看是多么错误。

我的示例的重点是说明参数函数的用法以及它们与具体类型的实例化,这是本次讨论的关键,而不是示例Stack实现是否有任何好处。

我的示例的重点是说明参数函数的使用以及它们与具体类型的实例化,这是本次讨论的关键,而不是示例 Stack 实现是否有任何好处。

我不认为@gertcuykens打算敲你的 Stack 实现,似乎他觉得泛型语法不熟悉且难以理解。

但是,如果包含这些类型参数,则可以进行某些观察:
(A B C D)...

我看到你所有的观点,感谢你的分析,他们没有错。 你是对的,在大多数情况下,通过仔细检查代码,你可以确定它在做什么。 我不认为这反驳了 Go 开发人员的报告,他们说语法令人困惑、模棱两可或需要更长的时间才能阅读,即使他们最终可以阅读。

一般来说,语法处于一个恐怖谷中。 代码做了一些不同的事情,但它看起来与现有的结构非常相似,以至于你的期望被抛出并且可浏览性下降。 您也无法建立新的期望,因为(适当地)这些元素是可选的,无论是整体还是部分。

对于那些更具体的病理病例, @infogulch说得很好:

我不认为仅仅因为它们很少使用就将所有认知复杂性推到语言的黑暗角落也是一个好主意。 即使它非常罕见,以至于大多数人通常不会遇到它们,但有时他们仍然会遇到它,并且允许某些代码具有混乱的控制流,只要它不是“大多数”代码,就会在我的嘴里留下不好的味道。

我认为,在这一点上,我们在该主题的这个特定部分上达到了清晰度饱和度。 无论我们谈论多少,严峻的考验将是 Go 开发人员可以多快和多好地学习、阅读和编写它。

(是的,在指出之前,负担应该在图书馆作者身上,而不是客户开发者身上,但我认为我们不希望普通图书馆无法理解普通图书馆的提升效应。我也不我不想把 Go 变成一个通用的 Jamboree,但部分我相信设计的遗漏会限制普遍性。)

我们有一个操场,我们可以为其他语法创建分支,这太棒了。 也许我们需要更多的工具!

人们已经给出了反馈。 我确信需要更多反馈,也许我们需要更好或更精简的反馈系统。

@tooolbox你认为当你总是像这样省略<>type时可以解析代码吗? 也许需要一个更严格的建议来做什么,但也许值得权衡?

type Stack::E []E

func (s Stack::E) Peek() E {
    return s[len(s)-1]
}

func (s *Stack::E) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack::E) Push(value E) {
    *s = append(*s, value)
}

type StackIterator::E struct{
    stack Stack::E
    current int
}

func (s *Stack::E) Iter() Iterator::E {
    it := StackIterator::E{stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator::E) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator::E) Value() E { 
    if i.current < 0 {
        var zero E
        return zero
    }

    return i.stack[i.current]
}

// ...

var it Iterator::string = stack.Iter()

it = Filter::string(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::string, string (it, func(s string) string {
    return s + ":1"
})

it = Distinct::string(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

我不知道为什么,但这Map::string, string (...感觉很奇怪。 看起来这会创建 2 个标记,一个Map::string和一个string函数调用。

此外,即使这在 Go 中没有使用,使用“标识符::标识符”可能会给初次使用的用户带来错误的印象,认为有一个Filter类/命名空间和string函数。 将来自其他广泛采用的语言的标记重用于完全不同的东西会引起很多混乱。

当你总是省略 <> 并像这样输入时,你认为有可能解析代码吗? 也许需要一个更严格的建议来做什么,但也许值得权衡?

不,我不这么认为。 我同意@urandom的观点,即没有任何封闭的空格字符使它看起来像两个标记。 我个人也喜欢 Contracts 的范围,并且对更改其功能不感兴趣。

此外,即使在 Go 中没有使用它,使用“Identifier::Identifier”可能会给初次使用的用户留下错误的印象,认为其中有一个带有字符串函数的过滤器类/命名空间。 将来自其他广泛采用的语言的标记重用于完全不同的东西会引起很多混乱。

我实际上并没有使用::的语言,但我见过它。 也许!更好,因为它会匹配 D,尽管我确实发现::在视觉上看起来更好。

如果我们要走这条路,可能会有很多关于具体使用什么字符的讨论。 这是缩小我们正在寻找的内容的尝试:

  • 除了裸露的identifier()以外的东西,这样它看起来不像是函数调用。
  • 可以包含多个类型参数的东西,以便以括号的方式在视觉上将它们联合起来。
  • 看起来标识符相关联的东西,因此它看起来像一个单元。
  • 对解析器来说并不模棱两可的东西。
  • 与具有强大开发人员思想共享的不同概念不冲突的东西。
  • 如果可能的话,一些会影响泛型定义和使用的东西,所以它们也变得更容易阅读。

有很多东西可以适合。

  • identifier!(a, b)游乐场
  • identifier@(a, b)
  • identifier#(a, b)
  • identifier$(a, b)
  • identifier<:a, b:>
  • identifier.<a, b>就像一个类型断言!
  • identifier:<a, b>
  • 等等。

有人对如何进一步缩小潜力有任何想法吗?

快速说明一下,我们已经考虑了所有这些想法,我们也考虑了类似的想法

func F(T : a, b T) { }
func G() { F(int : 1, 2) }

但同样,布丁的证据在于吃。 在没有代码的情况下进行抽象讨论是值得的,但不会得出明确的结论。

(不确定这是否之前已经讨论过)我看到,如果我们收到一个结构,我们将无法在不破坏现有调用代码的情况下“扩展”现有 API 来处理泛型类型。

例如,给定这个非泛型函数

func Repeat(v, n int) []int {
    var r []int
    for i := n; i > 0; i-- {
        r = append(r, v)
    }
    return r
}

Repeat(4, 4)

我们可以在不破坏向后兼容性的情况下使其通用

func Repeat(type T)(v T, n int) []T {
    var r []T
    for i := n; i > 0; i-- {
        r = append(r, v)
    }
    return r
}

Repeat("a", 5)

但是如果我们想对一个接收通用struct的函数做同样的事情

type XY struct {
    X, Y int
}

func RangeRepeat(arr []XY) []int {
    var r []int
    for _, n := range arr {
        for i := n.Y; i > 0; i-- {
            r = append(r, n.X)
        }
    }
    return r
}

RangeRepeat([]XY{{1, 1}, {2, 2}, {3, 3}})

似乎调用代码需要更新

type XY(type T) struct {
    X T
    Y int
}

func RangeRepeat(type T)(arr []XY(T)) []T {
    var r []T
    for _, n := range arr {
        for i := n.Y; i > 0; i-- {
            r = append(r, n.X)
        }
    }
    return r
}

// error: cannot use generic type XY(type T any) without instantiation
// RangeRepeat([]XY{{1, 1}, {2, 2}, {3, 3}}) // error in old code
RangeRepeat([](XY(int)){{1, 1}, {2, 2}, {3, 3}}) // API changed
// RangeRepeat([]XY{{"1", 1}, {"2", 2}, {"3", 3}}) // error
RangeRepeat([](XY(string)){{"1", 1}, {"2", 2}, {"3", 3}}) // ok

能够从结构中派生类型也很棒。

@ianlancetaylor

合同草案提到methods may not take additional type arguments 。 但是,没有提及替换特定方法的合同。 根据参数类型绑定的合同,这样的功能对于实现接口非常有用。

你讨论过这种可能性吗?

合同草案的另一个问题。 类型析取是否仅限于内置类型? 如果没有,是否可以使用参数化类型,尤其是析取列表中的接口?

就像是

type Getter(T) interface {
    Get() T
}

contract(G, T) {
    G Getter(T)
}

将非常有用,不仅可以避免将方法集从接口复制到合同,而且还可以在类型推断失败时实例化参数化类型,并且您无权访问具体类型(例如,它没有导出)

@ianlancetaylor我不确定这是否已经讨论过,但是关于函数类型参数的语法,是否可以将参数列表连接到类型参数列表? 因此,对于图形示例,而不是

var g = graph.New(*Vertex, *FromTo)([]*Vertex{ ... })

你会用

var g = graph.New(*Vertex, *FromTo, []*Vertex{ ... })

本质上,参数列表的前 K 个参数对应于长度为 K 的类型参数列表。参数列表的其余部分对应于函数的常规参数。 这具有镜像语法的好处

make(Type, size)

它将 Type 作为第一个参数。

这将简化语法,但需要类型信息来了解类型参数的结束位置以及常规参数的开始位置。

@ smasher164他回了几句评论说他们考虑过(这意味着他们放弃了它,尽管我很好奇为什么)。

func F(T : a, b T) { }
func G() { F(int : 1, 2) }

这就是您的建议,但是用冒号分隔两种参数。 我个人比较喜欢它,虽然它是一张不完整的照片; 类型声明、方法、实例化等呢?

我想回到@Inuart说的话:

我们可以在不破坏向后兼容性的情况下使其通用

Go 团队会考虑以这种方式更改标准库以符合 Go 1 的兼容性保证吗? 例如,如果strings.Repeat(s string, count int) string被替换为Repeat(type S stringlike)(s S, count int) S怎么办? 您还可以在 $#$ bytes.Repeat $#$ 中添加//Deprecated注释,但将其留在那里以供遗留代码使用。 Go 团队会考虑这种情况吗?

编辑:要清楚,我的意思是,这通常会在 Go1Compat 中考虑吗? 如果您不喜欢它,请忽略具体示例。

@carlmjohnson不。此代码会中断: f := strings.Repeat ,因为如果不先实例化多态函数,就无法引用它们。

从那里开始,我认为类型参数和值参数的连接将是一个错误,因为它阻止了引用函数的实例化版本的自然语法。 如果 go 已经有咖喱会更自然,但事实并非如此。 让foo(int, 42)foo(int)成为表达式并且两者都有非常不同的类型看起来很奇怪。

@urandom是的,我们已经讨论了在单个方法的类型参数上添加额外约束的可能性。 这将导致参数化类型的方法集根据类型参数而变化。 这可能有用,也可能令人困惑,但有一件事似乎是肯定的:我们可以稍后添加它而不会破坏任何东西。 所以我们推迟了这个想法。 谢谢你提出来。

可以在允许的类型列表中列出的确切内容并不那么清楚。 我认为我们在那里还有更多工作要做。 请注意,至少在当前设计草案中,在类型列表中列出接口类型当前意味着类型参数可以是该接口类型。 这并不意味着类型参数可以是实现该接口类型的类型。 我认为目前还不清楚它是否可以是参数化类型的实例化实例。 不过,这是个好问题。

@smasher164 @toolbox在单个列表中查看组合类型参数和常规参数时要考虑的情况是如何将它们分开(如果它们是分开的)以及如何处理没有常规参数的情况(大概我们可以排除没有类型参数的情况)。 例如,如果没有正则参数,如何区分实例化函数不调用和实例化函数调用? 虽然显然后者是更常见的情况,但人们希望能够编写前一种情况是合理的。

如果类型参数与常规参数放在相同的括号内,那么@griesemer在#36177(他的第二篇文章)中说他非常喜欢使用分号而不是冒号作为分隔符,因为(结果自动分号插入)它允许人们以一种很好的方式将参数分布在多行中。

就个人而言,我也喜欢使用竖线( |..| )来包围类型参数,因为您有时会看到在其他语言(Ruby、Crystal 等)中使用竖线来包围参数块。 所以我们会有类似的东西:

func F(|T| a, b T) { }
func G() { F(|int| 1, 2) }

优点包括:

  • 它们在类型和常规参数之间提供了很好的视觉区别(至少在我看来)。
  • 您不需要使用type关键字。
  • 没有常规参数不是问题。
  • 当然,竖线字符在 ASCII 集中,因此应该可以在大多数键盘上使用。

您甚至可以在括号外使用它,但大概您会遇到与<...>[...]相同的解析困难,因为它可能被误认为是按位“或”运算符困难不会那么严重。

我不明白垂直条在没有常规参数的情况下有何帮助。 我不明白如何区分函数实例化和函数调用。

区分这两种情况的一种方法是在实例化函数时需要type关键字,但在调用它时不需要关键字,如前所述,这是更常见的情况。

我同意这可行,但似乎非常微妙。 我认为发生的事情对读者来说并不明显。

我认为在围棋中,我们需要有更高的目标,而不仅仅是有办法做某事。 我们需要瞄准直接、直观且与语言的其余部分完美契合的方法。 阅读代码的人应该能够轻松理解正在发生的事情。 当然,我们不能总是实现这些目标,但我们应该尽我们所能。

@ianlancetaylor除了辩论语法本身就很有趣之外,我想知道我们作为一个社区是否可以做任何事情来帮助您和团队解决这个问题。

例如,我知道您想要更多以提案风格编写的代码,以便更好地评估提案,无论是语法还是其他方面? 和/或其他事情?

@toolbox是的。 我们正在开发一种工具来简化它,但它还没有准备好。 现在很快。

你能多说一下这个工具吗? 它允许执行代码吗?

这个问题是泛型反馈的首选位置吗? 它似乎比 wiki 更活跃。 一项观察是该提案有很多方面,但 GitHub 问题将讨论折叠成线性格式。

F(T:) / G() { F(T:)}语法对我来说看起来不错。 我不认为看起来像函数调用的实例化对于没有经验的读者来说是直观的。

我不明白关于向后兼容性的担忧是什么。 我认为草案中对宣布合同有限制,除非是在顶层。 如果允许的话,可能值得权衡(和测量)实际上会破坏多少代码。 我的理解只是使用contract关键字的代码,这似乎没有多少代码(无论如何都可以通过在旧文件顶部指定go1来支持)。 权衡这与程序员数十年的更多权力。 一般来说,用这种机制保护旧代码似乎很简单,尤其是在广泛使用 go 著名工具的情况下。

进一步关于这个限制,我怀疑禁止在函数体内声明方法是接口没有被更多使用的原因——它们比传递单个函数要麻烦得多。 很难说合同的顶级限制是否会像方法限制一样令人恼火——可能不会——但请不要以方法限制为先例。 对我来说,这是一个语言缺陷。

我还想看看合同如何帮助减少if err != nil冗长的例子,更重要的是,它们在哪些方面不够用。 像F() (X, error) {return IfError(foo(), func(i, j int) X { return X(i*j}), Identity )}这样的东西可能吗?

我还想知道,一旦 Map、Filter 和朋友可用,Go 团队是否预计隐式函数签名会感觉像是缺少的功能。 在为合同语言添加新的隐式类型功能时,是否需要考虑这一点? 或者以后可以添加吗? 或者它永远不会成为语言的一部分?

期待尝试该建议。 抱歉话题太多了。

就我个人而言,我很怀疑很多人愿意在函数体中编写方法。 今天在函数体中定义类型是非常罕见的。 声明方法将更加罕见。 也就是说,请参阅#25860(与泛型无关)。

我看不到泛型如何帮助处理错误(本身已经是一个非常冗长的话题)。 我不明白你的例子,对不起。

#21498 是一个较短的函数文字语法,也没有连接到泛型。

当我昨晚发布时,我没有意识到可以玩选秀
执行 (!!)。 哇,终于能写出更多抽象代码真是太好了。 我对语法草案没有任何问题。

继续上面的讨论...


人们不在函数体中编写类型的部分原因是因为它们
不能为他们写方法。 此限制可以将类型捕获在
定义它的块,因为它不能简洁地转换为
elsewere 的使用界面。 Java 允许匿名类满足其版本
接口,并且它们被大量使用。

我们可以在#25860 中进行接口讨论。 我只想说在那个时代
合同,方法将变得更加重要,所以我建议在
授权本地类型和喜欢编写闭包的人的一面,而不是
削弱他们。

(重申一下,请不要使用严格的 go1 兼容性 [vs virtual
99.999% 的兼容性,据我所知] 作为决定这一点的一个因素
特征。)


关于错误处理,我怀疑泛型可能允许抽象
处理(T1, T2, ..., error)返回元组的常见模式。 我不
有任何详细的想法。 像type ErrPair(type T) struct{T T; Err Error}这样的东西可能对于将动作链接在一起很有用,比如Promise in
Java/TypeScript。 或许有人更深思熟虑。 一次尝试
编写一个辅助库和使用该库的代码可能值得一看
如果您正在寻找真正的用法。

通过一些实验,我最终得到了以下结果。 我想试试这个
技术在一个更大的例子中,看看使用ErrPair(T)是否真的有帮助。

type result struct {min, max point}

// with a generic ErrPair type and generic function errMap2 (like Java's Optional#map() function).
func minMax2(msg *inputTimeSeries) (result, error) {
    return errMap2(
        MakeErrPair(time.Parse(layout, msg.start)).withMessage("bad start"),
        MakeErrPair(time.Parse(layout, msg.end)).withMessage("bad end"),
        func(start, end time.Time) (result, error) {
            min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
                return float64(p.value)
            })
            mkPoint := func(ip inputPoint) point {
                return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
            }
            return result{mkPoint(*min), mkPoint(*max)}, nil
        }).tuple()
}

// without generics, lots of if err != nil 
func minMax(msg *inputTimeSeries) (result, error) { 
    start, err := time.Parse(layout, msg.start)
    if err != nil {
        return result{}, fmt.Errorf("bad start: %w", err)
    }
    end, err := time.Parse(layout, msg.end)
    if err != nil {
        return result{}, fmt.Errorf("bad end: %w", err)
    }
    min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
        return float64(p.value)
    })
    mkPoint := func(ip inputPoint) point {
        return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
    }
    return result{mkPoint(*min), mkPoint(*max)}, nil
}

// Most languages look more like this.
func minMaxWithThrowing(msg *inputTimeSeries) result {
    start := time.Parse(layout, msg.start)) // might throw
    end := time.Parse(layout, msg.end)) // might throw
    min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
        return float64(p.value)
    })
    mkPoint := func(ip inputPoint) point {
        return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
    }
    return result{mkPoint(*min), mkPoint(*max)}
}

(完整的示例代码可在此处获得)


对于一般实验,我尝试编写一个 S-Expression 包
在这里
我在尝试执行实验时遇到了一些恐慌
使用像Form([]*Form(T))这样的复合类型。 我可以提供更多反馈
在解决这个问题之后,如果它有用的话。

我也不太确定如何编写原始类型 -> 字符串函数:

contract PrimitiveType(T) {
    T bool, int, int8, int16, int32, int64, string, uint, uint8, uint16, uint32, uint64, float32, float64, complex64, complex128
    // string(T) is not a contract
}

func primitiveString(type T PrimitiveType(T))(t T) string  {
    // I'm not sure if this is an artifact of the experimental implementation or not.
    return string(t) // error: `cannot convert t (variable of type T) to string`
}

我试图编写的实际函数是这个:

// basicFormAdapter implements FormAdapter() for the primitive types.
type basicFormAdapter(type T PrimitiveType) struct{}


func (a *basicFormAdapter(T)) Format(e T, fc *FormatContext) error {
    //This doesn't work: fc.Print(string(e)) -- cannot convert e (variable of type T) to string
    // This also doesn't work: cannot type switch on non-interface value e (type int)
    // switch ee := e.(type) {
    // case int: fc.Print(string(ee))
    // default: fc.Print(fmt.Sprintf("!!! unsupported type %v", e))
    // }
    // IMO, the proposal to allow switching on T is most natural:
    // switch T.(type) {
    //  case int: fc.Print(string(e))
    //  default: fc.Print(fmt.Sprintf("!!! unsupported type %v", e))
    // }

    // This can't be the only way, right?
    rv := reflect.ValueOf(e)
    switch rv.Kind() {
    case reflect.Bool: fc.Print(fmt.Sprintf("%v", e))
    case reflect.Int:fc.Print(fmt.Sprintf("%v", e))
    case reflect.Int8: fc.Print(fmt.Sprintf("int8:%v", e))
    case reflect.Int16: fc.Print(fmt.Sprintf("int16:%v", e))
    case reflect.Int32: fc.Print(fmt.Sprintf("int32:%v", e))
    case reflect.Int64: fc.Print(fmt.Sprintf("int64:%v", e))
    case reflect.Uint: fc.Print(fmt.Sprintf("uint:%v", e))
    case reflect.Uint8: fc.Print(fmt.Sprintf("uint8:%v", e))
    case reflect.Uint16: fc.Print(fmt.Sprintf("uint16:%v", e))
    case reflect.Uint32: fc.Print(fmt.Sprintf("uint32:%v", e))
    case reflect.Uint64: fc.Print(fmt.Sprintf("uint64:%v", e))
    case reflect.Uintptr: fc.Print(fmt.Sprintf("uintptr:%v", e))
    case reflect.Float32: fc.Print(fmt.Sprintf("float32:%v", e))
    case reflect.Float64: fc.Print(fmt.Sprintf("float64:%v", e))
    case reflect.Complex64: fc.Print(fmt.Sprintf("(complex64 %f %f)", real(rv.Complex()), imag(rv.Complex())))
    case reflect.Complex128:
         fc.Print(fmt.Sprintf("(complex128 %f %f)", real(rv.Complex()), imag(rv.Complex())))
    case reflect.String:
        fc.Print(fmt.Sprintf("%q", rv.String()))
    }
    return nil
}

我还尝试创建类似类型的“结果”

type Result(type T) struct {
    Value T
    Err error
}

func NewResult(type T)(value T, err error) Result(T) {
    return Result(T){
        Value: value,
        Err: err,
    }
}

func then(type T, R)(r Result(T), f func(T) R) Result(R) {
    if r.Err != nil {
        return Result(R){Err: r.Err}
    }

    v := f(r.Value)
    return  Result(R){
        Value: v,
        Err: nil,
    }
}

func thenTry(type T, R)(r Result(T), f func(T)(R, error)) Result(R) {
    if r.Err != nil {
        return Result(R){Err: r.Err}
    }

    v, err := f(r.Value)
    return  Result(R){
        Value: v,
        Err: err,
    }
}

例如

    r := NewResult(GetInput())
    r2 := thenTry(r, UppercaseAndErr)
    r3 := thenTry(r2, strconv.Atoi)
    r4 := then(r3, Add5)
    if r4.Err != nil {
        // handle err
    }
    return r4.Value, nil

理想情况下,您将then函数作为 Result 类型的方法。

草案中的绝对差异示例似乎也无法编译。
我认为以下几点:

func (a ComplexAbs(T)) Abs() T {
    r := float64(real(a))
    i := float64(imag(a))
    d := math.Sqrt(r * r + i * i)
    return T(complex(d, 0))
}

应该:

func (a ComplexAbs(T)) Abs() ComplexAbs(T) {
    r := float64(real(a))
    i := float64(imag(a))
    d := math.Sqrt(r * r + i * i)
    return ComplexAbs(T)(complex(d, 0))
}

我有点担心使用多个contract绑定一个类型参数的能力。

在 Scala 中,通常定义如下函数:

def compute[A: PointLike: HasTime: IsWGS](points: Vector[A]): Map[Int, A] = ???

PointLikeHasTimeIsWGS是一些小的contract (Scala 称它们type class )。

Rust 也有类似的机制:

fn f<F: A + B>(a F) {}

我们可以在定义函数时使用匿名接口。

type I1 interface {
    A()
}
type I2 interface {
    B()
}
func f(a interface{
    I1
    I2
})

IMO,匿名接口是一种不好的做法,因为interface是一个真正的类型,这个函数的调用者可能必须声明一个具有这种类型的变量。 但是contract只是对类型参数的约束,调用者总是使用一些真实类型或只是另一个类型参数,我认为在函数定义中允许匿名合同是安全的。

对于库开发者来说,如果只在少数地方使用一些合约的组合,定义一个新的contract是很不方便的,会弄乱代码库。 对于图书馆的用户,他们需要深入了解定义以了解它的真正需求。 如果用户定义了很多函数来调用库中的函数,他们可以定义一个命名合约以便于使用,如果他们需要,他们甚至可以在这个新合约中添加更多的合约,因为这是有效的

contract C1(T) {
    T A()
}
contract C2(T) {
    T B()
}
contract C3(T) {
    T C()
}

contract PART(T) {
    C1(T)
    C2(T)
}

contract ALL(T) {
    C1(T)
    C2(T)
    C3(T)
}

func f1(type A PART) (a A) {}

func f2(type A ALL) (a A) {
    f1(a)
}

我已经在草稿编译器上尝试过这些,所有这些都无法进行类型检查。

func f(type A C1, C2)(x A)

func f1(type A contract C(A1) {
    C1(A)
    C2(A)
}) (x A)

func f2(type A ((type A1) interface {
    I1(A1)
    I2(A1)
})(A)) (x A)

根据 CL 中的注释

受多个合约约束的类型参数将无法获得正确的类型绑定。

我认为这个奇怪的片段在这个问题解决后是有效的

func f1(type A C1, _ C2(A)) (x A)

以下是我的一些想法:

  • 如果我们将contract视为类型参数的类型type a A <=> var a A ,我们可以添加类似type a { A1(a); A2(a) }的语法糖来定义匿名迅速签约。
  • 否则,我们可以将类型列表的最后一部分视为需求列表, type a, b, A1(a), A2(a), A3(a, b) ,这种样式就像使用interface来约束类型参数一样。

@bobotu在 Go 中使用嵌入来组合功能很常见。 以与结构或接口相同的方式编写合约似乎很自然。

@azunymous就我个人而言,我不知道我对整个 Go 社区从多重回报转变为Result的感觉如何,尽管合同提案似乎会在某种程度上实现这一点。 Go 团队似乎回避了会损害语言“感觉”的语言更改,我同意这一点,但这似乎是这些更改之一。

只是一个想法; 我想知道在这一点上是否有任何看法。

@tooolbox我认为实际上不可能在您只是传递值的情况之外广泛使用像单个Result类型的东西,除非您有大量通用Result参数计数和返回类型的每个组合的 s 和函数。 使用大量编号的函数或使用闭包,您将失去可读性。

我认为你更有可能看到相当于errWriter的东西,你会在合适的时候偶尔使用类似的东西,命名为用例。

就我个人而言,我不知道我对整个 Go 社区从多次返回转变为结果的感觉如何

我不认为这会发生。 就像@azunymous所说,许多函数有多种返回类型和一个错误,但结果不能同时包含所有其他返回值。 参数多态性并不是做这种事情所需的唯一特性。 你还需要元组和解构。

谢谢! 就像我说的,这不是我深思熟虑的事情,但很高兴知道我的担心是错误的。

@toolbox我不是打算引入一些新语法,这里的关键问题是缺乏像匿名接口一样使用匿名合约的能力。

在草稿编译器中,似乎不可能写出这样的东西。 我们可以在函数定义中使用匿名接口,但我们不能对合约做同样的事情,即使是详细的样式。

func f1(type A, B, C, D contract {
    C1(A)
    C2(A, B)
    C3(A, C)
}) (a A, b B, c C, d D)

// Or a more verbose style

func f2(type A, B, C, D (contract (_A, _B, _C) {
    C1(_A)
    C2(_A, _B)
    C3(_A, _C)
})(A, B, C)) (a A, b B, c C, d D)

IMO,这是对现有语法的自然扩展。 这仍然是类型参数列表末尾的合约,我们仍然使用嵌入来组合功能。 如果 Go 可以像第一个代码片段一样提供一些糖来自动生成合约的类型参数,那么代码将更容易阅读和编写。

func fff(type A C1(A), B C2(B, A), C C3(B, C, A)) (a A, b B, c C)

// is more verbose than

func fff(type A, B, C contract {
    C1(A)
    C2(B, A)
    C3(B, C, A)
}) (a A, b B, c C)

当我尝试在没有动态方法调用的情况下实现惰性迭代器时遇到了一些麻烦,就像 Rust 的迭代器一样。

我想定义一个简单的Iterator合约

contract Iterator(T, E) {
    T Next() (E, bool)
}

因为 Go 没有type member的概念,我需要将E声明为输入类型参数。

收集结果的函数

func Collect(type I, E Iterator) (input I) []E {
    var results []E
    for {
        e, ok := input.Next()
        if !ok {
            return results
        }
        results = append(results, e)
    }
}

映射元素的函数

contract MapIO(I, E, O, R) {
    Iterator(I, E)
    Iterator(O, R)
}

func Map(type I, E, O, R MapIO) (input I, f func (e E) R) O {
    return &lazyIterator(I, E, R){
        parent: input,
        f:      f,
    }
}

我这里有两个问题:

  1. 我不能在这里返回lazyIterator ,编译器说cannot convert &(lazyIterator(I, E, R) literal) (value of type *lazyIterator(I, E, R)) to O
  2. 我需要声明一个名为MapIO的新合同,它需要 4 行,而Map只需要 6 行。 用户很难阅读代码。

假设Map可以进行类型检查,我希望我可以写类似

type staticIterator(type E) struct {
    elem []E
}

func (it *(staticIterator(E))) Next() (E, bool) { panic("todo") }

func main() {
    inpuit := &staticIterator{
        elem: []int{1, 2, 3, 4},
    }
    mapped := Map(input, func (i int) float32 { return float32(i + 1) })
    fmt.Printf("%v\n", Collect(mapped))
}

不幸的是,编译器抱怨它不能推断类型。 在我将代码更改为

func main() {
    input := &staticIterator(int){
        elem: []int{1, 2, 3, 4},
    }
    mapped := Map(*staticIterator(int), int, *lazyIterator(*staticIterator(int), int, float32), float32)(input, func (i int) float32 { return float32(i + 1) })
    result := Collect(*lazyIterator(*staticIterator(int), int, float32), float32)(mapped)
    fmt.Printf("%v\n", result)
}

代码很难读写,重复的类型提示太多。

顺便说一句,编译器会恐慌:

panic: interface conversion: ast.Expr is *ast.ParenExpr, not *ast.CallExpr

goroutine 1 [running]:
go/go2go.(*translator).instantiateTypeDecl(0xc000251950, 0x0, 0xc0001af860, 0xc0001a5dd0, 0xc00018ac90, 0x1, 0x1, 0xc00018bca0, 0x1, 0x1, ...)
        /home/tuzi/go-tip/src/go/go2go/instantiate.go:191 +0xd49
go/go2go.(*translator).translateTypeInstantiation(0xc000251950, 0xc000189380)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:671 +0x3f3
go/go2go.(*translator).translateExpr(0xc000251950, 0xc000189380)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:518 +0x501
go/go2go.(*translator).translateExpr(0xc000251950, 0xc0001af990)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:496 +0xe3
go/go2go.(*translator).translateExpr(0xc000251950, 0xc00018ace0)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:524 +0x1c3
go/go2go.(*translator).translateExprList(0xc000251950, 0xc00018ace0, 0x1, 0x1)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:593 +0x45
go/go2go.(*translator).translateStmt(0xc000251950, 0xc000189840)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:419 +0x26a
go/go2go.(*translator).translateBlockStmt(0xc000251950, 0xc00018d830)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:380 +0x52
go/go2go.(*translator).translateFuncDecl(0xc000251950, 0xc0001c0390)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:373 +0xbc
go/go2go.(*translator).translate(0xc000251950, 0xc0001b0400)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:301 +0x35c
go/go2go.rewriteAST(0xc000188280, 0xc000188240, 0x0, 0x0, 0xc0001f6280, 0xc0001b0400, 0x1, 0xc000195360, 0xc0001f6280)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:122 +0x101
go/go2go.RewriteBuffer(0xc000188240, 0x7ffe07d6c027, 0xa, 0xc0001ec000, 0x4fe, 0x6fe, 0x0, 0xc00011ed58, 0x40d288, 0x30, ...)
        /home/tuzi/go-tip/src/go/go2go/go2go.go:132 +0x2c6
main.translateFile(0xc000188240, 0x7ffe07d6c027, 0xa)
        /home/tuzi/go-tip/src/cmd/go2go/translate.go:26 +0xa9
main.main()
        /home/tuzi/go-tip/src/cmd/go2go/main.go:64 +0x434

我还发现不可能定义一个与Iterator返回特定类型一起使用的函数。

type User struct {}

func UpdateUsers(type A Iterator(A, User)) (it A) bool { 
    // Access `User`'s field.
}

// And I found this may be possible

contract checkInts(A, B) {
    Iterator(A, B)
    B int
}

func CheckInts(type A, B checkInts) (it A) bool { panic("todo") }

第二个片段可以在某些情况下工作,但很难理解,并且未使用的B类型看起来很奇怪。

确实,我们可以使用一个接口来完成这个任务。

type Iterator(type E) interface {
    Next() (E, bool)
}

我只是想探索 Go 的设计的表现力。

顺便说一句,我指的 Rust 代码是

fn main() {
    let input = vec![1, 2, 3, 4];
    let mapped = input.iter().map(|x| x * 3);
    let result = f(mapped);
    println!("{:?}", result.collect::<Vec<_>>());
}

fn f<I: Iterator<Item = i32>>(it: I) -> impl Iterator<Item = f32> {
    it.map(|i| i as f32 * 2.0)
}

// The definition of `map` in stdlib is
pub struct Map<I, F> {
    iter: I,
    f: F,
}

fn map<B, F: FnMut(Self::Item) -> B>(self, f: F) -> Map<Self, F>

这是https://github.com/golang/go/issues/15292#issuecomment -633233479 的总结

  1. 我们可能需要一些东西来表达existential type for func Collect(type I, E Iterator) (input I) []E

    • 通用量化参数E的实际类型无法推断,因为它只出现在返回列表中。 由于缺少type member使E默认存在,我想我们可能会在很多地方遇到这个问题。

    • 或许我们可以使用最简单的existential type像 Java 的通配符?来解析func Consume(type I, E Iterator) (input I)的类型推断。 我们可以使用_来替换Efunc Consume(type I Iterator(I, _)) (input I)

    • 但它仍然无法解决Collect的类型推断问题,我不知道是否难以推断E ,但 Rust 似乎能够做到这一点。

    • 或者我们可以使用_作为编译器可以推断的类型的占位符,并手动填充缺失的类型,例如Collect(_, float32) (...)在 float32 的迭代器上进行收集。

  1. 由于缺乏返回existential type的能力,我们也遇到了诸如func Map(type I, E, O, R MapIO) (input I, f func (e E) R) O之类的问题

    • Rust 通过使用impl Iterator<E>来支持这一点。 如果 Go 可以提供类似的东西,我们可以在没有装箱的情况下返回一个新的迭代器,这对于一些性能关键的代码可能很有用。

    • 或者我们可以简单地返回一个装箱的对象,这就是 Rust 在返回位置支持existential type之前解决这个问题的方法。 但是问题是contractinterface之间的关系,也许我们需要定义一些转换规则,让编译器自动转换它们。 否则,对于这种情况,我们可能需要使用相同的方法定义contractinterface

    • 否则我们只能使用 CPS 将类型参数从返回位置移动到输入列表。 例如, func Map(type I, E, O, R MapIO) (input I, f func (e E) R, f1 func (outout O)) 。 但这在实践中是没有用的,因为当我们将函数传递给Map $ 时,我们必须编写O的实际类型。

我刚刚赶上了这个讨论,很明显类型参数的语法困难仍然是提案草案的主要困难。 有一种方法可以完全避免类型参数并实现大多数泛型功能:#32863——也许现在是考虑该替代方案的好时机? 如果有任何机会采用这种设计,我很乐意尝试修改 Web 组装游乐场以允许对其进行测试。

我的感觉是,当前的重点是确定当前提案语义的正确性,而不管语法如何,因为语义很难改变。

我刚刚看到一篇关于Featherweight Go的论文发表在 Arxiv 上,是 Go 团队和几位类型论专家的合作。 看起来在这方面还有更多的计划论文。

为了跟进我之前的评论,Haskell 的 Phil Wadler 和该论文的一位作者计划于 6 月 8 日星期一太平洋夏令时间上午 7 点/美国东部时间上午 10 点在“轻量级围棋”上发表演讲:http: //chalmersfp.org/ . 优酷链接

@rcoreilly我认为只有当人们有更多的写作经验,更重要的是阅读根据设计草案编写的代码时,我们才会知道“语法困难”是否是一个主要问题。 我们正在研究让人们尝试的方法。

如果没有这一点,我认为语法只是人们首先看到并首先评论的内容。 这可能是个大问题,也可能不是。 我们还不知道。

为了跟进我之前的评论,Haskell 名声的 Phil Wadler 和该论文的一位作者安排了周一在“轻量级围棋”上的演讲

Phil Wadler 的演讲非常平易近人和有趣。 我对看似毫无意义的长达一小时的时间限制感到恼火,这阻止了他进入单态化。

值得注意的是,派克邀请瓦德勒加入; 显然他们是从贝尔实验室认识的。 对我来说,Haskell 有一套非常不同的价值观和范式,有趣的是看看它(创建者?首席设计师?)如何看待 Go 和 Go 中的泛型。

该提案本身的语法非常接近 Contracts,但省略了 Contracts 本身,仅使用类型参数和接口。 一个关键的区别是能够采用泛型类型并在其上定义比类型本身具有更具体约束的方法。

显然 Go 团队正在研究或拥有这个原型! 那会很有趣。 与此同时,这看起来如何?

package graph

type Node(type e) interface{
    Edges() []e
}

type Edge(type n) interface{
    Nodes() (from n, to n)
}

type Graph(type n Node(e), e Edge(n)) struct { ... }
func New(type n Node(e), e Edge(n))(nodes []n) *Graph(n, e) { ... }
func (g *Graph(type n Node(e), e Edge(n))) ShortestPath(from, to n) []e { ... }

我有这个权利吗? 我认同。 如果我这样做......其实还不错。 不能完全解决口吃的括号问题,但似乎有所改善。 我内心的一些无名的动荡被平息了。

@urandom的堆栈示例怎么样? (将interface{}别名为Any并使用一定数量的类型推断。)

package main

type Any interface{}

type Stack(type t Any) []t

func (s Stack(type t Any)) Peek() t {
    return s[len(s)-1]
}

func (s *Stack(type t Any)) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack(type t Any)) Push(value t) {
    *s = append(*s, value)
}

type StackIterator(type t Any) struct{
    stack Stack(t)
    current int
}

func (s *Stack(type t Any)) Iter() *StackIterator(t) {
    it := StackIterator(t){stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator(type t Any)) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator(type t Any)) Value() t {
    if i.current < 0 {
        var zero t
        return zero
    }

    return i.stack[i.current]
}

type Iterator(type t Any) interface {
    Next() bool
    Value() t
}

func Map(type t Any, u Any)(it Iterator(t), mapF func(t) u) Iterator(u) {
    return mapIt(t, u){it, mapF}
}

type mapIt(type t Any, u Any) struct {
    parent Iterator(t)
    mapF func(t) u
}

func (i mapIt(type t Any, u Any)) Next() bool {
    return i.parent.Next()
}

func (i mapIt(type t Any, u Any)) Value() u {
    return i.mapF(i.parent.Value())
}

func Filter(type t Any)(it Iterator(t), predicate func(t) bool) Iterator(t) {
    return filter(t){it, predicate}
}

type filter(type t Any) struct {
    parent Iterator(t)
    predicateF func(t) bool
}

func (i filter(type t Any)) Next() bool {
    if !i.parent.Next() {
        return false
    }

    n := true
    for n && !i.predicateF(i.parent.Value()) {
        n = i.parent.Next()
    }

    return n
}

func (i filter(type t Any)) Value() t {
    return i.parent.Value()
}

func Distinct(type t comparable)(it Iterator(t)) Iterator(t) {
    return distinct(t){it, map[t]struct{}{}}
}

type distinct(type t comparable) struct {
    parent Iterator(t)
    set map[t]struct{}
}

func (i distinct(type t Any)) Next() bool {
    if !i.parent.Next() {
        return false
    }

    n := true
    for n {
        _, ok := i.set[i.parent.Value()]
        if !ok {
            i.set[i.parent.Value()] = struct{}{}
            break
        }
        n = i.parent.Next()
    }


    return n
}

func (i distinct(type t Any)) Value() t {
    return i.parent.Value()
}

func ToSlice(type t Any)(it Iterator(t)) []t {
    var res []t

    for it.Next() {
        res = append(res, it.Value())
    }

    return res
}

func ToSet(type t comparable)(it Iterator(t)) map[t]struct{} {
    var res map[t]struct{}

    for it.Next() {
        res[it.Value()] = struct{}{}
    }

    return res
}

func Reduce(type t Any)(it Iterator(t), id t, acc func(a, b t) t) t {
    for it.Next() {
        id = acc(id, it.Value())
    }

    return id
}

func main() {
    var stack Stack(string)
    stack.Push("foo")
    stack.Push("bar")
    stack.Pop()
    stack.Push("alpha")
    stack.Push("beta")
    stack.Push("foo")
    stack.Push("gamma")
    stack.Push("beta")
    stack.Push("delta")


    var it Iterator(string) = stack.Iter()

    it = Filter(string)(it, func(s string) bool {
        return s == "foo" || s == "beta" || s == "delta"
    })

    it = Map(string, string)(it, func(s string) string {
        return s + ":1"
    })

    it = Distinct(string)(it)

    println(Reduce(it, "", func(a, b string) string {
        if a == "" {
            return b
        }
        return a + ":" + b
    }))


}

类似的东西,我想。 我意识到该代码中实际上没有合同,因此它不能很好地代表 FGG 风格的处理方式,但我可以马上解决这个问题。

印象:

  • 我喜欢方法中类型参数的风格与类型声明的风格相匹配。 即说“类型”并明确说明类型, ("type" param paramType, param paramType...)而不是(param, param) 。 它使它在视觉上保持一致,因此代码更加一目了然。
  • 我喜欢将类型参数设为小写。 Go 中的单字母变量表示非常本地化的使用,但大写表示它是导出的,并且放在一起看起来相反。 小写感觉更好,因为类型参数的范围是函数/类型。

好吧,合同呢?

好吧,我喜欢的一件事是Stringer没有受到影响; 你不会有一个Stringer接口和一个Stringer合约。

type Stringer interface {
    String() string
}

func Stringify(type t Stringer)(s []t) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String())
    }
    return ret
}

我们还有viaStrings示例:

type ToString interface {
    Set(string)
}

type FromString interface {
    String() string
}

func SetViaStrings(type to ToString, from FromString)(s []from) []to {
    r := make([]to, len(s))
    for i, v := range s {
        r[i].Set(v.String())
    }
    return r
}

有趣的。 在那种情况下,我实际上并不是 100% 确定合同给我们带来了什么。 也许其中的一部分是一个函数可以有多个类型参数但只有一个合约的规则。

论文/谈话中涵盖了平等:

contract equal(T) {
    T Equal(T) bool
}

// becomes

type equal(type t equal(t)) interface{
    Equal(t) bool
}

等等。 我对语义很感兴趣。 类型参数是接口,因此关于实现接口的相同规则适用于可用作类型参数的内容。 它只是在运行时没有“装箱”——除非你明确地向它传递一个接口,我想,你可以自由地使用它。

我没有提到的最重要的事情是替代 Contracts 指定一系列原始类型的能力。 好吧,我肯定为此制定策略以及许多其他事情:

8 - 结论

这是故事的开始,而不是结束。 在未来的工作中,我们计划研究除单态化之外的其他实现方法,特别是考虑基于传递类型的运行时表示的实现,类似于用于 .NET 泛型的实现。 有时使用单态化并有时传递运行时表示的混合方法可能是最好的,再次类似于用于 .NET 泛型的方法。

Featherweight Go 仅限于 Go 的一小部分。 我们计划了一个其他重要特性的模型,例如赋值、数组、切片和包,我们将其命名为 Bantamweight Go; 以及基于“goroutines”和消息传递的 Go 创新并发机制模型,我们将其称为 Cruiserweight Go。

羽量级围棋对我来说看起来很棒。 让一些类型理论专家参与进来的好主意。 这看起来更像是我在这个话题上进一步提倡的那种东西。

很高兴听到类型理论专家正在积极研究这个!

它甚至看起来与我的旧提案“合同是接口”相似(除了语法略有不同) https://github.com/cosmos72/gomacro/blob/master/doc/generics-cti.md

@工具箱
通过允许具有与实际类型不同的约束(以及完全不同的类型)的方法,FGG 开辟了许多在当前合同草案中不可行的可能性。 例如,使用 FGG,应该能够同时定义 Iterator 和 ReversibleIterator,并让中间迭代器和终止迭代器(map、filter reduce)同时支持(例如,使用 Next() 和 NextFromBack() 用于可逆) ,取决于父迭代器是什么。

我认为重要的是要记住,FGG 并不是 Go 中的泛型最终会出现的地方。 这是从外面对他们的一种看法。 它明确地忽略了一堆最终使最终产品复杂化的事情。 另外,我没有看报纸,只是看了演讲。 考虑到这一点:据我所知,FGG 在合同草案中增加表达能力有两种重要方式:

  1. 它允许向方法添加新的类型参数(如演讲中的“列表和映射”示例所示)。 AFAICT 这将允许实施Functor (事实上​​,如果我没记错的话,那是他的 List 示例), Monad和他们的朋友。 我不认为 Gophers 对这些特定类型感兴趣,但有一些有趣的用例(例如, Flume的 Go 端口或类似概念可能会受益)。 就个人而言,我觉得这是一个积极的变化,尽管我还没有看到对反思等有什么影响。 我确实觉得使用它的方法声明开始变得难以阅读 - 特别是如果泛型类型的类型参数也必须在接收器中列出。
  2. 它允许类型参数对泛型类型的方法有比对类型本身更严格的限制。 正如其他人所提到的,这允许您让相同的泛型类型实现不同的方法,具体取决于它被实例化的类型。 就个人而言,我不确定这是一个好的变化。 让Map(int, T)Map(string, T)没有的方法结束,这似乎是一个混乱的秘诀。 至少,如果发生这种情况,编译器需要提供出色的错误消息。 同时,好处似乎相对较小——特别是考虑到谈话(单独编译)的激励因素与 Go 不是超级相关:因为方法必须在与其接收器类型相同的包中声明,并且包是单元编译时,您不能真正单独扩展类型。 我知道谈论编译是谈论更抽象的好处的一种具体方式,但我仍然认为这种好处对 Go 没有多大帮助。

无论如何,我期待着接下来的步骤:)

我认为重要的是要记住,FGG 并不是 Go 中的泛型最终会出现的地方。

@Merovius你为什么这么说?

@arl
FG 更像是一篇关于可以做什么的研究论文。 没有人明确表示这就是多态性在未来将如何在 Go 中工作。 尽管论文中列出了 2 个 Go 核心开发人员作为作者,但这并不意味着这将在 Go 中实现。

我认为重要的是要记住,FGG 并不是 Go 中的泛型最终会出现的地方。 这是从外面对他们的一种看法。 它明确地忽略了一堆最终使最终产品复杂化的事情。

是的,非常好的观点。

另外,我会注意到 Wadler 作为团队的一员工作,最终的产品建立在合同提案的基础上并且非常接近合同提案,这是核心开发人员多年工作的结果。

通过允许具有与实际类型不同的约束(以及完全不同的类型)的方法,FGG 开辟了许多在当前合同草案中不可行的可能性。 ...

@urandom我很好奇那个迭代器的例子是什么样的; 你介意把东西扔在一起吗?

另外,我对泛型除了映射、过滤器和函数之外还能做什么感兴趣,更好奇它们如何使像 k8s 这样的项目受益。 (并不是说他们现在会去重构,但我听说缺乏泛型需要一些花哨的步法,我认为自定义资源?更熟悉该项目的人可以纠正我。)

我确实觉得使用它的方法声明开始变得难以阅读 - 特别是如果泛型类型的类型参数也必须在接收器中列出。

也许gofmt可以在某种程度上有所帮助? 也许我们需要多线。 也许值得一玩。

正如其他人所提到的,这允许您让相同的泛型类型实现不同的方法,具体取决于它被实例化的类型。

我明白你在说什么@Merovius

它被 Wadler 称为不同之处,它让他解决了他的表达式问题,但你提出了一个很好的观点,Go 的密封包似乎限制了你可以/应该做的事情。 你能想到任何你想要这样做的实际案例吗?

正如其他人所提到的,这允许您让相同的泛型类型实现不同的方法,具体取决于它被实例化的类型。

我明白你在说什么@Merovius

它被 Wadler 称为不同之处,它让他解决了他的表达式问题,但你提出了一个很好的观点,Go 的密封包似乎限制了你可以/应该做的事情。 你能想到任何你想要这样做的实际案例吗?

具有讽刺意味的是,我的第一个想法是它可以用来解决本文中描述的一些挑战: https ://blog.merovius.de/2017/07/30/the-trouble-with-optional-interfaces.html

@工具箱

另外,我对泛型在地图和过滤器以及功能性事物之外可以做的事情感兴趣,

FWIW,应该澄清的是,这是一种卖“地图、过滤器和功能性东西”的短文。 例如,我个人不希望mapfilter超过我的代码中的内置数据结构(我更喜欢 for 循环)。 但这也可能意味着

  1. 提供对任何第三方数据结构的通用访问。 即mapfilter可以在泛型树或排序映射上工作,或者……也可以。 因此,您可以交换映射的内容,以获得更多功能。 更重要的是
  2. 您可以换掉的映射方式。 例如,您可以构建一个Compose版本,它可以为每个函数生成多个 goroutine,并使用通道同时运行它们。 这将使运行并发数据处理管道和自动扩展瓶颈变得容易,而只需要编写func(A) B s。 或者,您可以将相同的功能放入一个框架中,该框架在集群中运行数千个程序副本,并在它们之间安排批量数据(这就是我在上面链接到Flume时提到的)。

因此,虽然能够编写MapFilterReduce表面上看起来很无聊,但相同的技术为使可扩展计算更容易开辟了一些真正令人兴奋的可能性。

@ChrisHines

具有讽刺意味的是,我的第一个想法是它可以用来解决本文中描述的一些挑战: https ://blog.merovius.de/2017/07/30/the-trouble-with-optional-interfaces.html

这是一个有趣的想法,当然感觉应该如此。 但我还不知道怎么做。 如果您以ResponseWriter为例,看起来这可能使您能够编写通用的类型安全的包装器,根据包装的ResponseWriter支持的不同方法使用不同的方法。 但是,即使您可以对不同的方法使用不同的界限,您仍然必须将它们写下来。 因此,虽然它可以使情况类型安全,因为您不添加您不支持的方法,但您仍然需要枚举您可以支持的所有方法,因此中间件可能仍会掩盖一些可选接口只是不知道他们。 同时,您也可以(即使没有此功能)

type Middleware (type RW http.ResponseWriter) struct {
    RW
}

并覆盖您关心的选择性方法 - 并提升RW的所有其他方法。 因此,您甚至不必编写包装器,甚至可以透明地获取您不知道的那些方法。

因此,假设我们获得了嵌入在泛型结构中的类型参数的提升方法(我希望我们这样做),那么该方法似乎可以更好地解决问题。

我认为 http.ResponseWriter 的具体解决方案类似于errors.Is/As 。 不需要更改语言,只需添加一个库即可创建一个标准的 ResponseWriter 包装方法,以及一种查询链中是否有任何 ResponseWriters 可以处理的方法,例如 wPush。 我怀疑泛型是否适合这样的事情,因为重点是在可选接口之间进行运行时选择,例如 Push 仅在 http2 中可用,而不是在我启动 http1 本地开发服务器时不可用。

浏览 Github,我不认为我曾经为这个想法创建过问题,所以也许我现在会这样做。

编辑:#39558。

@工具箱
我的猜测是它看起来像这样,以及它的内部单态代码:

package iter

type Any interface{}

type Iterator(type T Any) interface {
    Next() bool
    Value() T
}

type ReversibleIterator(type T Any) interface {
    Iterator(T)
    NextBack() bool
}

type mapIt(type I Iterator(T), T Any, U Any) struct {
    parent I
    mapF func(T) U
}

func (i mapIt(type I Iterator(T))) Next() bool {
    return i.parent.Next()
}

func (i mapIt(type I Iterator(T), T Any, U Any)) Value() U { 
    return i.mapF(i.parent.Value())
}

func (i mapIt(type I ReversibleIterator(T))) NextBack() bool { 
    return i.parent.NextBack()
}

// Monomorphisation
type mapIt<OnlyForward, int, float64> struct {
    parent OnlyForward,
    mapF func(int) float64
}

func (i mapIt<OnlyForward, int, float64>) Next() bool {
    return i.parent.Next()
}

func (i mapIt<OnlyForward, int, float64>) Value() float64 {
    return i.mapF(i.parent.Value())
}

type mapIt<Slice, int, string> struct {
    parent Slice,
    mapF func(int) string
}

func (i mapIt<Slice, int, string>) Next() bool {
    return i.parent.Next()
}

func (i mapIt<Slice, int, string>) Value() string {
    return i.mapF(i.parent.Value())
}

func (i mapIt<Slice, int, string>) NextBack() bool {
    return i.parent.NextBack()
}



我的猜测是它看起来像这样,以及它的内部单态代码:

FWIW 这是我几年前的一条推文,探讨了迭代器如何在 Go 中与泛型一起工作。 如果您进行全局替换以将<T>替换为(type T) ,那么您将获得与当前提案相距不远的东西: https ://twitter.com/rogpeppe/status/425035488425037824

FWIW,应该澄清的是,这是一种卖“地图、过滤器和功能性东西”的短文。 例如,我个人不希望在我的代码中映射和过滤内置数据结构(我更喜欢 for 循环)。 但这也可能意味着...

我明白您的观点并且不反对,是的,我们将从您的示例所涵盖的内容中受益。
但是我仍然想知道像 k8s 这样的东西会如何受到影响,或者另一个具有“通用”数据类型的代码库,其中正在执行的操作类型不是映射或过滤器,或者至少超出了这些。 我想知道 Contracts 或 FGG 在这种情况下如何提高类型安全性和性能。

想知道是否有人可以指出一个代码库,希望比 k8s 更简单,适合这种类别?

@urandom哇。 因此,如果您使用实现ReversibleIteratorparent实例化mapIt ,则mapIt具有NextBack()方法,如果没有,则不会吨。 我读对了吗?

考虑一下,从图书馆的角度来看,这似乎很有用。 你有一些非常开放的通用结构类型( Any类型参数),它们有很多方法,受各种接口的限制。 因此,当您在自己的代码中使用该库时,您嵌入到结构中的类型使您能够调用一组特定的方法,因此您获得了该库的一组特定功能。 这组功能是什么,是在编译时根据您的类型所具有的方法计算出来的。

...看起来确实有点像@ChrisHines提出的,您可以根据您的类型实现的内容编写具有或多或少功能的代码,但话又说回来,这实际上是可用方法集增加或减少的问题,不是单一方法的行为,所以是的,我看不出 http2 劫持者的事情是如何解决这个问题的。

总之,非常有趣。

并不是说我会这样做,但我认为这是可能的:

type OverrideX interface {
    GetX() int
}

type OverrideY interface {
    GetY() int
}

type Inheritor(type child Any) struct {
    Parent
    c child
}

func (i Inheritor(type child OverrideX)) GetX() int {
    return i.c.GetX()
}

func (i Inheritor(type child OverrideY)) GetY() int {
    return i.c.GetY()
}

type Parent struct {
    x, y int
}

func (p Parent) GetX() int {
    return p.x
}

func (p Parent) GetY() int {
    return p.y
}

type Child struct {
    x int
}

func (c Child) GetX() int {
    return c.x
}

func main() {
    i := Inheritor(Child){Parent{5, 6}, Child{3}}
    x, y := i.GetX(), i.GetY() // 3, 6
}

同样,主要是一个笑话,但我认为探索可能的极限是件好事。

编辑:嗯,确实显示了如何根据类型参数设置不同的方法集,但产生的效果与在Child中嵌入Parent完全相同。 再一次,愚蠢的例子;)

我不喜欢只有给定特定类型才能调用的方法。 鉴于@tooolbox的示例,测试可能会很痛苦,因为某些方法只能在给定某些特定子项的情况下调用 - 测试人员可能会错过某些情况。 还不清楚哪些方法可用,并且需要 IDE 提供建议不是 Go 应该要求的。 但是,您可以通过在方法中进行类型断言,仅使用结构给出的类型来实现这一点。

func (i Inheritor(type child Any)) GetX() int {
    if c, ok := i.c.(OverrideX); ok {
        return c.GetX()
    }
    return i.Parent.GetX()
}

func (i Inheritor(type child Any)) GetY() int {
    if c, ok := i.c.(OverrideY); ok {
        return c.GetY()
    }
    return i.Parent.GetY()
} 

此代码也是类型安全的、清晰的、易于测试的,并且可能与原始代码相同地运行而不会造成混淆。

@TotallyGamerJet
该特定示例是类型安全的,但其他示例不是,并且将需要使用不兼容类型的运行时恐慌。

另外,我不确定测试人员怎么可能错过任何情况,因为它们很可能是首先编写通用代码的人。 此外,它是否清晰有点主观,尽管它绝对不需要 IDE 来推断。 请记住,这不是函数重载,该方法可以被调用也可以不被调用,所以它不像某些情况可以被意外跳过。 任何人都可以看到此方法存在于某种类型,他们可能需要再次阅读以了解所需的类型,但仅此而已。

@urandom我并不一定是指那个特定的例子有人会错过一个案例——它很短。 我的意思是,当您有大量方法时,只能在给定特定类型的情况下调用。 所以我坚持不使用子类型(我喜欢这样称呼它)。 甚至可以在不使用类型断言或子类型的情况下解决“表达式问题”。 就是这样:

type Any interface {}

type Evaler(type t Any) interface {
    Eval() t
}

type Num struct {
    value int
}

func (n Num) Eval() int {
    return n.value
}

type Plus(type a Evaler(type t Any)) struct {
    left a
    right a
}

func (p Plus(type a Evaler(type t Any)) Eval() t {
    return p.left.Eval() + p.right.Eval()
}

func (p Plus(type a Evaler(type t Any)) String() string {
    return fmt.Sprintf("(%s+%s)", p.left, p.right)
}

type Expr interface {
    Evaler
    fmt.Stringer
}

func main() {
    var e Expr = Plus(Num){Num{1}, Num{2}}
    var v int = e.Eval() // 3
    var s string = e.String() // "(1+2)"
}

由于不允许使用不实现加法的类型在 Plus 上调用 Eval,因此应在编译时捕获对 Eval 方法的任何滥用。 虽然,可能会不正确地使用 String()(可能添加结构),但良好的测试应该能捕捉到这些情况。 Go 通常更倾向于简单而不是“正确性”。 子类型化的唯一好处是在文档和使用中更加混乱。 如果您可以提供一个需要子类型的示例,我可能更倾向于认为这是一个好主意,但目前,我不相信。
编辑:修正错误并改进

@TotallyGamerJet在您的示例中, String 方法应该递归调用 String ,而不是 Eval

@TotallyGamerJet在您的示例中, String 方法应该递归调用 String ,而不是 Eval

@神奇
我不确定你的意思。 Plus 结构的类型是 Evaler,它不能确保满足 fmt.Stringer。 在两个 Evaler 上调用 String() 需要类型断言,因此不是类型安全的。

@TotallyGamerJet
不幸的是,这就是 String 方法的想法。 它应该递归地调用其成员上的任何 String 方法,否则没有意义。 但是您已经看到,如果您无法确保 Plug 类型上的方法需要具有 String 方法的类型a ,则它将需要类型断言和恐慌

@urandom
你是对的! 令人惊讶的是,Sprintf 会为您执行该类型断言。 因此,您可以同时发送左右字段。 虽然如果 Plus 中的类型没有实现 Stringer 仍然会出现恐慌,但我对此很好,因为可以通过使用%v动词打印出结构来避免恐慌(它将调用 String( ) 如果可供使用的话)。 我认为这个解决方案很清楚,任何其他不确定性都应该记录在代码中。 所以我仍然不相信为什么需要子类型化。

@TotallyGamerJet
如果允许使用具有不同约束的方法,我个人仍然看不到会出现什么问题。 该方法仍然存在,并且代码清楚地描述了需要哪些参数(以及特殊情况下的接收者)。
就像有一个方法,接受一个string参数,或一个MyType接收器,是清晰易读和明确的,所以下面的定义也是如此:

func (rec MyType(type T SomeInterface(T)) Foo() T

要求在签名本身中清楚地标明。 IE 它是MyType(type T SomeInterface(T)) ,仅此而已。

更改https://golang.org/cl/238003提到了这个问题: design: add go2draft-type-parameters.md

更改https://golang.org/cl/238241提到这个问题: content: add generics-next-step article

圣诞节来得早!

  • 我可以看到为使设计文档平易近人付出了很多努力,它显示出来了,非常棒,非常感谢。
  • 在我看来,这次迭代是一个重大改进,我可以看到这是按原样实现的。
  • 同意几乎所有的推理和逻辑。
  • 像这样,如果您为单个类型参数指定约束,则必须对所有参数都执行此操作。
  • 比较好听。
  • 接口中的类型列表还不错; 同意它比运算符方法更好,但在我看来,这可能是进一步讨论的最大领域。
  • 类型推断(仍然)很棒。
  • 单参数类型参数化约束的推断似乎是聪明而不是清晰。
  • 我喜欢图表示例中的“我们并没有声称这很简单”。 没关系。
  • (type *T constraint)看起来是解决指针问题的好方法。
  • 完全同意func(x(T))更改。
  • 我认为我们想要立即对复合文字进行类型推断? 😄

感谢 Go 团队! 🎉

https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#comparable -types-in-constraints

我相信可比性更像是内置类型而不是接口。 我相信这是提案草案中的一个小错误。

type ComparableHasher interface {
    comparable
    Hash() uintptr
}

需要

type ComparableHasher interface {
    type comparable
    Hash() uintptr
}

操场似乎也表明它需要是type comparable
https://go2goplay.golang.org/p/mhrl0xYsMyj

编辑:Ian Lance Taylor 和 Robert Griesemer 正在修复 go2go 工具(go2go 翻译器中的小错误,而不是草稿。设计草稿是正确的)

有没有想过让人们编写自己的通用哈希表等? 目前这非常有限的 ISTM(尤其是与内置地图相比)。 基本上,内置映射有comparable作为键约束,但当然, ==!=不足以实现哈希表。 像ComparableHasher这样的接口只将编写散列函数的责任传递给调用者,它没有回答它实际上看起来如何的问题(另外,调用者可能不应该对此负责;编写好的散列函数很难)。 最后,使用指针作为键可能从根本上是不可能的 - 将指针转换为uintptr以用作索引会冒 GC 移动指针对象并因此改变存储桶的风险(除非这个问题,暴露一个预先声明的func hash(type T comparable)(v T) uintptr可能是一个 - 可能不是理想的 - 解决方案)。

我可以很好地接受“这不是真的可行”作为答案,我只是想知道您是否考虑过:)

@gertcuykens我已经对 go2go 工具进行了修复,以按预期处理comparable

@Merovius我们希望编写通用哈希表的人将提供他们自己的哈希函数,可能还有他们自己的比较函数。 在编写自己的哈希函数时, https://golang.org/pkg/hash/mapash/包可能很有用。 您是正确的,指针值的散列必须取决于该指针指向的值; 它不能依赖于转换为uintptr的指针的值。

不确定这是否是该工具当前实现的限制,但尝试返回受接口约束的泛型类型会返回错误:
https://go2goplay.golang.org/p/KYRFL-vrcUF

昨天为泛型实现了一个真实的用例。 它是一个通用的管道抽象,允许独立扩展管道的阶段并支持取消和错误处理(它不在操场上运行,因为它依赖于errgroup ,但使用 go2go 工具运行它似乎工作)。 一些观察:

  • 这很有趣。 通过将设计缺陷转化为类型错误,在迭代设计时,拥有一个功能正常的类型检查器实际上有很大帮助。 最终结果是 ~100 LOC,包括评论。 所以,总的来说,编写通用代码的经验是令人愉快的,IMO。
  • 这个用例至少可以顺利地与类型推断一起工作,不需要显式实例化。 我认为这对推理设计来说是个好兆头。
  • 我认为这个示例将受益于具有额外类型参数的方法的能力。 需要Compose的顶级函数意味着管道的构建是反向进行的 - 需要构建管道的后期阶段以将其传递给构建早期阶段的函数。 如果方法可以具有类型参数,则可以将Stage设置为具体类型并执行func (s *Stage(A, B)) Compose(type C)(n int, f func(B) C) *Stage(A, C) 。 并且构建管道的顺序与管道的顺序相同(请参阅操场上的评论)。 当然,我看不到的现有草案中可能还有更优雅的 API - 很难证明是否定的。 我有兴趣看到一个可行的例子。

总的来说,我喜欢新草案 FWIW :) IMO 放弃合同是一种改进,通过类型列表指定所需运算符的新方法也是如此。

[编辑:修复了我的代码中的一个错误,如果管道阶段失败,可能会发生死锁。 并发很难]

工具分支的一个问题:它会跟上最后一个 go 版本(所以 v1.15、v1.15.1、...)吗?

@urandom :请注意,您在代码中返回的值是 Foo(T) 类型的。 每个
这种类型实例化会产生一个新定义的类型,在本例中为 Foo(T)。
(当然,如果你的代码中有多个 Foo(T),它们都是一样的
定义类型)。

但是你的函数的结果类型是V,它是一个类型参数。 笔记
类型参数受 Valuer 接口约束,但它是
_not_ 一个接口(甚至那个接口)。 V 是一个类型参数,它是
一种新的类型,我们知道它的约束所描述的事物。
关于可分配性,它的行为类似于一个名为 V 的已定义类型。

因此,您尝试将 Foo(T) 类型的值分配给 V 类型的变量
(既不是 Foo(T) 也不是 Valuer(T),它仅具有由
估价师(T))。 因此分配失败。

(顺便说一句,我们仍在完善对类型参数的理解
最终需要足够精确地拼出它,以便我们可以写一个
规格。 但请记住,每个类型参数实际上都是一个新的
我们只知道它的类型约束指定的定义类型。)

也许您打算这样写: https://go2goplay.golang.org/p/8Hz6eWSn8Ek?

@Inuart如果工具分支是指 dev.go2go 分支:这是一个原型,它的构建考虑到了权宜之计,并用于实验目的。 我们确实希望人们使用它并尝试编写代码,但在生产软件中_依赖_翻译器并不是一个好主意。 很多事情都可以改变(甚至语法,如果需要的话)。 当我们从反馈中学习时,我们将修复错误并调整设计。 跟上最新的 Go 版本似乎不太重要。

我昨天为泛型实现了一个真实的用例。 它是一个通用的管道抽象,允许独立扩展管道的各个阶段并支持取消和错误处理(它不在操场上运行,因为它依赖于 errgroup,但使用 go2go 工具运行它似乎可以工作)。

我喜欢这个例子。 我只是完整地阅读了它,最让我绊倒的事情(甚至不值得解释)与所涉及的泛型无关。 我认为没有泛型的相同结构不会更容易掌握。 它也绝对是您希望编写一次,通过测试,以后不必再愚弄的东西之一。

可能有助于可读性和审查的一件事是,如果 Go 工具有一种显示通用代码的单态版本的方法,那么您可以看到事情的结果。 可能是不可行的,部分原因是函数在最终的编译器实现中甚至可能不是单态的,但我认为如果它完全可以实现,那将是有价值的。

我认为这个示例将受益于具有额外类型参数的方法的能力。

我也在你的操场上看到了这条评论; 绝对替代调用语法似乎更具可读性和直接性。 你能更详细地解释一下吗? 几乎没有把我的头放在你的示例代码上,我在跳转时遇到了麻烦:)

因此,您尝试将 Foo(T) 类型的值分配给 V 类型的变量
(既不是 Foo(T) 也不是 Valuer(T),它仅具有由
估价师(T))。 因此分配失败。

很好的解释。

...否则,很遗憾看到 HN 帖子被 Rust 人群劫持。 如果能从 Gophers 那里获得更多关于该提案的反馈,那就太好了。

Go 团队的两个问题:

  • 您想使用这个 github 问题来解决有关泛型原型的问题/错误,还是您更喜欢另一个论坛?
  • 我在玩类型列表:示例: https ://go2goplay.golang.org/p/AAwSof_wT6t

这两者之间有区别,还是go2游乐场的错误? 第一个编译,第二个报错

type Addable interface {
    type int, float64
}

func Add(type T Addable)(a, b T) T {
  return a + b
}
type Addable interface {
    type int, float64, string
}

func Add(type T Addable)(a, b T) T {
  return a + b
}

失败: invalid operation: operator + not defined for a (variable of type T)

嗯,这是一个最出人意料的惊喜。 我一直希望有一种方法可以在某个时候实际尝试一下,但我没想到会很快。

首先,发现一个bug: https ://go2goplay.golang.org/p/1r0NQnJE-NZ

其次,我构建了一个迭代器示例,并且有点惊讶地发现该类型推断不起作用。 我可以让它直接返回一个接口类型,但我认为它无法推断出那个类型,因为它需要的所有类型信息都来自参数。

编辑:另外,正如多人所说,我认为允许在方法声明期间添加新类型将非常有用。 就接口实现而言,您可以根本不允许接口实现,仅在接口还调用泛型( type Example interface { Method(type T someConstraint)(v T) bool } )时才允许实现,或者,如果_any_可能,您可以让它实现接口它的变体实现了接口,然后如果通过接口调用它,则将其限制为接口想要的。 例如,

```去
类型接口接口{
获取(字符串)字符串
}

类型示例(类型 T)结构 {
v T
}

// 这只会起作用,因为 Interface.Get 比 Example.Get 更具体。
func (e Example(T)) Get(type R)(v R) T {
return fmt.Sprintf("%v: %v", v, ev)
}

func DoSomething(接口间){
// 底层是 Example(string) 和 Example(string).Get(string) 是假设的,因为它是必需的。
fmt.Println(inter.Get("example"))
}

功能主要(){
// 允许,因为 Example(string).Get(string) 是可能的。
DoSomething(Example(string){v: "一个例子。"})
}

@DeedleFake您报告的第一件事不是错误。 你现在需要写https://go2goplay.golang.org/p/qo3hnviiN4k 。 这记录在设计草案中。 在参数列表中,写入a(b)被解释为a (b)a的括号类型b )以实现向后兼容。 我们可能会在未来改变这一点。

迭代器的例子很有趣——乍一看它确实像一个错误。 请提交一个错误(博客文章中的说明)并将其分配给我。 谢谢。

@Kashomon博客文章 (https://blog.golang.org/generics-next-step) 建议使用邮件列表进行讨论并为错误提交单独的问题。 谢谢。

我认为+的问题已经解决了。

@工具箱

可能有助于可读性和审查的一件事是,如果 Go 工具有一种显示通用代码的单态版本的方法,那么您可以看到事情的结果。 可能是不可行的,部分原因是函数在最终的编译器实现中甚至可能不是单态的,但我认为如果它完全可以实现,那将是有价值的。

go2go 工具可以做到这一点。 不要使用go tool go2go run x.go2 ,而是编写go tool go2go translate x.go2 。 这将生成一个带有翻译代码的文件 x.go。

也就是说,我不得不说阅读起来相当具有挑战性。 不是不可能,但也不容易。

@griesemer

我知道返回参数可以是一个接口,但我真的不明白为什么它不能是泛型类型本身。

例如,您可以使用相同的泛型类型作为输入参数,并且效果很好:
https://go2goplay.golang.org/p/LuDrlT3zLRb
这是否有效,因为该类型已被实例化?

@urandom写道:

我知道返回参数可以是一个接口,但我真的不明白为什么它不能是泛型类型本身。

从理论上讲,它可以,但是当返回类型不是泛型时,将返回类型设为泛型是没有意义的,因为它是由功能块(即返回值)确定的。

通常,泛型参数要么完全由参数值元组确定,要么由调用站点处的函数应用程序的类型确定(确定泛型返回类型的实例化)。

从理论上讲,您还可以允许泛型类型参数不是由参数值元组确定的,并且必须显式提供,例如:

func f(type S)(i int) int
{
    s S =...
    return 2
}

不知道这有多大意义。

@urandom我并不一定是指那个特定的例子有人会错过一个案例——它很短。 我的意思是,当您有大量方法时,只能在给定特定类型的情况下调用。 所以我坚持不使用子类型(我喜欢这样称呼它)。 甚至可以在不使用类型断言或子类型的情况下解决“表达式问题”。 就是这样:

type Any interface {}

type Evaler(type t Any) interface {
  Eval() t
}

type Num struct {
  value int
}

func (n Num) Eval() int {
  return n.value
}

type Plus(type a Evaler(type t Any)) struct {
  left a
  right a
}

func (p Plus(type a Evaler(type t Any)) Eval() t {
  return p.left.Eval() + p.right.Eval()
}

func (p Plus(type a Evaler(type t Any)) String() string {
  return fmt.Sprintf("(%s+%s)", p.left, p.right)
}

type Expr interface {
  Evaler
  fmt.Stringer
}

func main() {
  var e Expr = Plus(Num){Num{1}, Num{2}}
  var v int = e.Eval() // 3
  var s string = e.String() // "(1+2)"
}

由于不允许使用不实现加法的类型在 Plus 上调用 Eval,因此应在编译时捕获对 Eval 方法的任何滥用。 虽然,可能会不正确地使用 String()(可能添加结构),但良好的测试应该能捕捉到这些情况。 Go 通常更倾向于简单而不是“正确性”。 子类型化的唯一好处是在文档和使用中更加混乱。 如果您可以提供一个需要子类型的示例,我可能更倾向于认为这是一个好主意,但目前,我不相信。
编辑:修正错误并改进

我不知道,为什么不使用'<>'?

@99云
请查看更新草案中包含的常见问题解答

为什么不使用语法 F\像 C++ 和 Java?
在函数内解析代码时,例如 v := F\,在看到 < 时,我们看到的是类型实例化还是使用 < 运算符的表达式是模棱两可的。 解决这个问题需要有效的无限前瞻。 一般来说,我们努力保持 Go 解析器的效率。

@urandom一个通用函数体总是经过类型检查,没有实例化(*); 通常(例如,如果它被导出)我们不知道它将如何被实例化。 在进行类型检查时,它只能依赖可用的信息。 如果结果类型是类型参数并且返回表达式是不兼容赋值的不同类型,则返回不起作用。 或者换句话说,如果使用(可能推断的)类型参数调用泛型函数,则函数体不会再次使用这些类型参数进行类型检查。 它只检查类型参数是否满足泛型函数的约束(在使用这些类型参数实例化函数签名之后)。 希望有帮助。

(*) 更准确地说,泛型函数是用自己的类型参数实例化的,因此它是经过类型检查的; 类型参数是真实类型; 我们只知道它们的约束条件告诉我们的程度。

请让我们在别处继续讨论。 如果您对一段您认为应该可以工作的代码有更多疑问,请提出问题,以便我们在那里讨论。 谢谢。

似乎没有办法使用函数来创建通用结构的零值。 以这个函数为例:

func zero(type T)() T {
    var zero T
    return zero
}

它似乎适用于基本类型(int、float32 等)。 但是,当您有一个具有通用字段的结构时,事情会变得很奇怪。 举个例子:

type Opt(type T) struct {
    val T
}

func (o Opt(T)) Do() { /*stuff*/ }

一切似乎都很好。 但是,在执行时:

opt := zero(Opt(int))
opt.Do() 

它不会编译给出错误: opt.Do undefined (type func() Opt(int) has no field or method Do)我可以理解是否不可能这样做,但是当 int 应该是 Opt 类型的一部分时认为它是一个函数很奇怪。 但更奇怪的是可以这样做:

opt := zero(Opt)      //  But somehow this line compiles
opt(int).Do()         // This will panic

我不确定哪一部分是错误,哪一部分是有意的。
代码: https ://go2goplay.golang.org/p/M0VvyEYwbQU

@TotallyGamerJet

您的函数zero()没有参数,因此不会进行类型推断。 您必须实例化zero函数,然后调用它。

opt := zero(Opt(int))()
opt.Do()

https://go2goplay.golang.org/p/N6ip-nm1BP-

@工具箱
没错。 我以为我提供了类型,但我忘记了第二组括号来实际调用该函数。 我仍然习惯这些泛型。

我一直明白在 Go 中没有泛型是一个设计决定而不是疏忽。 它使 Go 变得如此简单,我无法理解对一些简单复制重复的过度偏执。 在我们公司,我们编写了大量的 Go 代码,但从未找到一个我们更喜欢泛型的实例。

对我们来说,它肯定会让 Go 感觉不那么 Go,看起来炒作的人群终于设法将 Go 的发展影响到一个错误的方向。 他们不能只让 Go 保持简单的美丽,不,他们必须不断地抱怨和抱怨,直到他们终于如愿以偿。

对不起,这并不是要贬低任何人,但这就是破坏设计精美的语言的开始。 下一步是什么? 如果我们像很多人想要的那样不断改变东西,我们最终会得到“C++”或“JavaScript”。

就按照他们本来的样子离开吧!

@iio7我是所有人中智商最低的,我的未来取决于确保我能阅读其他人的代码。 炒作刚刚开始不仅仅是因为泛型,而是因为新设计不需要在当前提案中更改语言,所以我们都很高兴有一个窗口可以让事情保持简单并且仍然有一些通用和功能性的好东西。 不要误会我的意思,我知道团队中总会有人像火箭科学家一样编写代码,而猴子应该就这样理解它? 所以你现在看到的例子是来自火箭科学家的例子,老实说,是的,我需要一些时间来阅读它,但最后通过一些试验和错误,我知道他们正在尝试编程什么。 我要说的是相信 Ian 和 Robert 以及其他人,他们还没有完成设计。 在一年左右的时间里不会感到惊讶,有一些工具可以帮助编译器说完美的简单猴子语言,不管你扔给它的火箭通用代码有多么困难。 您可以提供的最佳反馈是重写一些示例并指出某些东西是否过度设计,以便他们可以确保编译器会抱怨它或自动被 vet 工具之类的东西重写。

我阅读了有关<>的常见问题解答,但对于像我这样的愚蠢的人来说,如果它看起来像v := F<T>而不是v := F(T) ,那么解析器如何更难确定它是否是通用调用

最重要的是,我认为解析器当然应该保持快速,但我们也不要忘记程序员最容易阅读哪个与 IMO 同样重要。 是否更容易理解v := F(T)的作用? 还是v := F<T>更容易? 同样重要的是要考虑:)

不支持也不反对v := F<T> ,只是提出一些可能值得考虑的想法。

这是今天合法的围棋

    f, c, d, e := 1, 2, 3, 4
    a, b := f < c, d > (e)
    fmt.Println(a, b) // true false

讨论尖括号是没有意义的,除非你提出如何处理尖括号(break back compat?)。 出于所有意图和目的,这是一个死问题。 Go 团队采用尖括号的可能性实际上为零。 请讨论其他任何事情。

编辑添加:抱歉,如果此评论过于生硬。 Reddit 和 HN 上有很多关于尖括号的讨论,这让我很沮丧,因为很长一段时间以来,关心泛型的人都知道后向兼容性问题。 我理解为什么人们更喜欢尖括号,但如果没有重大改变,这是不可能的。

感谢您的评论@iio7。 事情失控总是存在非零风险。 这就是为什么我们在此过程中一直非常谨慎。 我相信我们现在拥有的设计比去年更干净、更正交。 我个人希望我们可以让它变得更简单,尤其是在类型列表方面——但随着我们了解更多,我们会发现。 (有点讽刺的是,设计变得越正交和简洁,它就会越强大,并且可以编写更复杂的代码。)最后的话还没有说出来。 去年,当我们有了第一个可能可行的设计时,很多人的反应和你一样:“我们真的想要这个吗?” 这是一个很好的问题,我们应该尽可能地回答它。

@gertcuykens的观察也是正确的 - 自然,玩 go2go 原型的人正在尽可能地探索它的极限(这是我们想要的),但在此过程中也会产生可能无法在适当的生产中通过集合的代码环境。 到目前为止,我已经看到了很多很难破译的通用代码。

在某些情况下,通用代码显然会胜出; 我正在考虑通用并发算法,它允许我们将一些微妙的代码放入库中。 当然有各种各样的容器数据结构,以及诸如排序之类的东西。可能绝大多数代码根本不需要泛型。 与其他语言相比,通用特性是该语言中许多功能的核心,在 Go 中,通用特性只是 Go 工具集中的另一个工具。 不是构建其他一切的基本构建块。

比较一下:在 Go 的早期,我们都倾向于过度使用 goroutine 和通道。 需要一段时间来了解它们什么时候合适,什么时候不合适。 现在我们或多或少地制定了一些指导方针,我们只在真正合适的时候使用它们。 我希望如果我们有仿制药也会发生同样的情况。

谢谢。

从关于基于[T]的语法的设计草案部分:

该语言通常允许逗号分隔列表中的尾随逗号,因此如果 A 是泛型类型,则应允许 A[T,],但通常不允许索引表达式。 但是解析器无法知道 A 是泛型类型还是切片、数组或映射类型的值,所以直到类型检查完成后才能报告此解析错误。 同样,可解决但复杂。

仅通过使尾随逗号在索引表达式中完全合法然后仅使用gofmt删除它就不能很容易地解决这个问题吗?

@DeedleFake可能。 那肯定是一个简单的出路。 但从语法上看,它也有点难看。 我不记得所有细节,但早期版本支持 [type T] 样式类型参数。 请参阅 dev.go2go 分支,提交 3d4810b5ba,其中删除了支持。 人们可以再次挖掘它并进行调查。

是否可以将每个[]列表中的泛型参数的长度限制为大多数以避免此问题,就像内置泛型类型一样:

  • [N]吨
  • []T
  • 地图[K]T
  • 陈T

请注意,内置泛型类型中的最后一个参数都没有包含在[]中。
通用声明语法如下: https ://github.com/dotaheor/unify-Go-builtin-and-custom-generics#the -generic-declaration-syntax

@dotaheor我不确定您到底在问什么,但显然有必要支持泛型类型的多个类型参数。 例如, https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#containers

@ianlancetaylor
我的意思是每个类型参数都包含在[]中,因此链接中的类型可以声明为:

type Map[type K][type V] struct

使用时是这样的:

var m Map[string]int

未包含在[]中的类型参数表示泛型类型的使用结束。

在考虑将数组 #39355 与泛型一起排序时,我发现“可比较”在当前的泛型草案中被特殊处理(可能是由于无法轻松列出类型列表中的所有可比较类型)作为预先声明的类型约束.

如果将泛型草案更改为也定义“ordered”/“orderable”,类似于“comparable”的预定义方式,那就太好了。 它是同一类型值的相关常用关系,这将允许 go 语言的未来扩展定义更多类型(数组、结构、切片、总和类型、检查枚举等)的排序,而不会遇到复杂情况并非所有有序类型都可以在“comparable”之类的类型列表中列出。

我并不是建议在语言规范中决定应该为更多类型排序,但是对泛型的这种更改使其与这种更改更加向前兼容(约束。有序代码不必是稍后生成的魔术编译器或如果使用类型列表将被弃用)。 排序包可以从预先声明的类型约束“ordered”开始,然后可以“仅”与例如数组一起工作,如果曾经更改并且不修复所使用的约束。

@martisch我认为这只需要扩展有序类型后发生。 目前, constraints.Ordered可以列出所有类型(这不适用于comparable ,因为指针、结构、数组……是可比较的,所以这必须是神奇的。但是ordered目前仅限于一组有限的内置底层类型),用户可以依赖它。 如果我们将排序扩展到数组(例如),我们仍然可以添加一个新的神奇ordered约束,然后将其嵌入到constraints.Ordered中。 这意味着constraints.Ordered的所有用户都将自动从新约束中受益。 当然,编写自己的显式类型列表的用户不会受益 - 但如果我们现在添加ordered ,对于不嵌入的用户来说也是一样的。

因此,IMO 将其推迟到真正有意义之前并没有什么损失。 我们不应该添加任何可能的约束集作为预先声明的标识符 -更不用说任何潜在的未来约束集:)

如果我们将排序扩展到数组(例如),我们仍然可以添加一个新的神奇ordered约束,然后将其嵌入到constraints.Ordered中。

@Merovius这是我没有想到的一个好点。 这允许在未来以一致的方式扩展constraints.Ordered 。 如果还会有constraints.Comparable ,那么它很适合整体结构。

@martisch ,请注意ordered - 不像comparable - 作为接口类型不是连贯的,除非我们还在具体类型之间定义(全局)总顺序,或者禁止非泛型代码使用<ordered类型的变量,或禁止将comparable用作一般的运行时接口类型。

否则,“实现”的传递性就会失效。 考虑这个程序片段:

    var x constraints.Ordered = int(0)
    var y constraints.Ordered = string("0")
    fmt.Println(x < y)

它应该输出什么? (答案是直观的还是任意的?)

@bcmills
fun (<)(type T Ordered)(t1 T,t2 T) Bool?怎么样

比较不同类型的算术类型:

如果任何算术S只为 S<:T 实现Ordered(T) S<:T ,则:

//Isn't possible I think
interface SorT(S,T)
{ 
type S,T
}

fun (<)(type R SorT(S,T), S Ordered(R), T Ordered(R))(s S, t T) Bool

应该是唯一的。

对于运行时多态性,您需要 Ordered 是可参数化的。
或者:
您以元组类型对 Ordered 进行分区,然后将(<)重写为:

//but isn't supported that either
fun(<)(type R Ordered)(s R.0,t R.1)

你好!
我有个问题。

有没有办法使类型约束只传递具有一个类型参数的泛型类型?
仅通过Result(T) / Option(T) /etc 而不仅仅是T的东西。
我试过

type Box(type T) interface {
    Val() (T, bool)
}

但它需要Val()方法

type Box(type T) interface{}

类似于interface{} ,即Any

也试过 https://go2goplay.golang.org/p/lkbTI7yppmh -> 编译失败

type Box(type T) interface {
       type Box(T)
}

https://go2goplay.golang.org/p/5NsKWNa3E1k -> 编译失败

type Box(type T) interface{}

type Generic(type T) interface {
    type Box(T)
}

https://go2goplay.golang.org/p/CKzE2J-YOpD -> 不起作用

type Box(type T) interface{}

type Generic(type T Box(T)) interface {}

这种行为是预期的还是只是类型检查错误?

@tdakkota约束适用于类型参数,它们适用于类型参数的完全实例化形式。 没有办法编写对类型参数的非实例化形式提出任何要求的类型约束。

请查看更新草案中包含的常见问题解答

为什么不使用语法 F像 C++ 和 Java?
在函数内解析代码时,例如 v := F,在看到 < 时,我们看到的是类型实例化还是使用 < 运算符的表达式是模棱两可的。 解决这个问题需要有效的无限前瞻。 一般来说,我们努力保持 Go 解析器的效率。

@TotallyGamerJet随便!

如何处理泛型类型的零值? 没有枚举,我们如何处理可选值。
例如: vector的泛型版本和名为First的函数返回第一个元素,如果它的长度 > 0 否则泛型类型的值为零。
我们如何编写这样的代码? 因为我们不知道向量中的哪种类型,如果chan/slice/map ,我们可以return (nil, false) ,如果structprimitive typestring , int , bool , 怎么处理呢?

@leaxoy

var zero T应该足够了

@leaxoy

var zero T应该足够了

nil这样的全局魔法变量?

@leaxoy
var zero T应该足够了

nil这样的全局魔法变量?

关于这个主题有一个正在讨论的提案 - 请参阅提案:Go 2: Universal zero value with type inference #35966

它检查了表达式(不是var zero T这样的语句)的几种新的替代语法,这些语法将始终返回类型的零值。

零值目前看起来可行,但可能是在堆栈或堆上占用空间? 我们是否应该考虑使用enum Option一步完成。
否则,如果零值不占用空间,那就更好了,不需要添加枚举。

零值目前看起来可行,但可能是在堆栈或堆上占用空间?

从历史上看,我相信 Go 编译器已经优化了这些情况。 我不太担心。

可以在 C++ 模板中指定默认类型值。 是否为 go 泛型类型参数考虑了类似的构造? 这可能会在不破坏现有代码的情况下改造现有类型。

例如,考虑现有的asn1.ObjectIdentifier类型,它是[]int 。 这种类型的一个问题是它不符合 ASN.1 规范,该规范规定每个 sub-oid 可能是任意长度的 INTEGER(例如*big.Int )。 可能ObjectIdentifier可以修改为接受通用参数,但这会破坏很多现有代码。 如果有一种方法可以指定int是默认参数值,那么也许这可以改造现有代码。

type SignedInteger interface {
    type int, int32, int64, *big.Int
}
type ObjectIdentifier(type T SignedInteger) []T
// type ObjectIdentifier(type T SignedInteger=int) []T  // `int` would be the default instantiation type.

// New code with generic awareness would compile in go2.
var oid1 ObjectIdentifier(int) = ObjectIdentifier(int){1, 2, 3}

// But existing code would fail to compile:
var oid1 ObjectIdentifier = ObjectIdentifier{1, 2, 3}

为了清楚起见,上面的asn1.ObjectIdentifier只是一个例子。 我并不是说使用泛型是解决 ASN.1 合规性问题的唯一方法或最佳方法。

此外,是否有任何计划允许可参数化的有限界面边界?

type Ordable(type T, S) interface {
    type S, type T
}

如何支持类型参数的 where 条件。
我们能不能写出这样的代码:

type Vector(type T) struct {
    vec []T
}

func (v Vector(T)) Sum() T where T: Summable {
      //
}

func (v Vector(T)) First()  (T, bool) {
     //
}

Sum方法仅在类型参数TSummable时有效,否则我们无法在 Vector 上调用Sum

@leaxoy

你可以写一些像https://go2goplay.golang.org/p/pRznN30Qu8V

type Addable interface {
    type int, uint
}

type SummableVector(type T Addable) Vector(T)

func (v SummableVector(T)) Sum() T {
    var r T
    for _, i := range v.vec {
        r = r + i
    }
    return r
}

我认为where子句看起来不像 Go 并且很难解析它,它应该类似于

type Vector(type T) struct {
    vec []T
}

func (v Vector(T Summable)) Sum() T {
      //
}

func (v Vector(T)) First()  (T, bool) {
     //
}

但这似乎是方法专业化。

@sebastien-rosset 我们没有考虑泛型类型参数的默认类型。 该语言没有函数参数的默认值,也不清楚为什么泛型会有所不同。 在我看来,使现有代码与添加泛型的包兼容的能力不是优先事项。 如果重写包以使用泛型,则可以要求更改现有代码,或者简单地使用新名称引入泛型代码。

@sighoya

此外,是否有任何计划允许可参数化的有限界面边界?

对不起,我不明白这个问题。

我想提醒人们,博客文章 (https://blog.golang.org/generics-next-step) 建议关于泛型的讨论发生在 golang-nuts 邮件列表上,而不是问题跟踪器上。 我会继续阅读这个问题,但它有近 800 条评论,而且非常笨拙,除了问题跟踪器的其他困难,例如没有评论线程。 谢谢。

反馈:我听了最近的 Go Time 播客,我不得不说@griesemer关于尖括号问题的解释是我第一次真正明白,即“解析器上的无限前瞻”实际上是什么意思去吗? 非常感谢那里提供的额外细节。

另外,我赞成方括号。 😄

@ianlancetaylor

博客文章建议关于泛型的讨论发生在 golang-nuts 邮件列表上,而不是问题跟踪器上

在最近的一篇博文 [1] 中, @ddevault指出 Google 群组(该邮件列表所在的位置)需要一个 Google 帐户。 您需要一个帖子才能发布,显然有些团体甚至需要一个帐户才能阅读。 我有一个 Google 帐户,所以这对我来说不是问题(我也不是说我同意该博客文章中的所有内容),但我同意如果我们想要一个更公正的 golang 社区,并且如果我们想避免回声室,最好不要有这种要求。

我对 Google 群组一无所知,如果 golang-nuts 有一些例外,请接受我的道歉并忽略这一点。 对于它的价值,我从阅读这个线程中学到了很多东西,而且我也相当确信(在使用 golang 六年多之后)泛型是该语言的错误方法。 不过,这只是我个人的看法,感谢您为我们带来了我非常喜欢的语言!

干杯!

[1] https://drewdevault.com/2020/08/01/pkg-go-dev-sucks.html

@purpleidea任何 Google 群组都可以用作邮件列表。 您无需拥有 Google 帐户即可加入和参与。

@ianlancetaylor

任何 Google Group 都可以用作邮件列表。 您无需拥有 Google 帐户即可加入和参与。

当我去:

https://groups.google.com/forum/#!forum/golang -nuts

在私人浏览器窗口中(隐藏我登录的谷歌帐户),然后单击“新主题”,它将我重定向到谷歌登录页面。 没有 Google 帐户如何使用它?

@purpleidea通过给[email protected]写一封电子邮件。 这是一个邮件列表。 只有网络界面需要 Google 帐户。 这似乎很公平 - 鉴于它是一个邮件列表,您需要一个电子邮件地址,而群组显然只能从 gmail 帐户发送邮件。

我想大多数人不明白什么是邮件列表。

无论如何,您也可以使用任何公共邮件列表镜像,例如https://www.mail-archive.com/[email protected]/

这一切都很好,但是当人们链接到
Google 网上论坛上的主题(经常发生)。 太不可思议了
试图从 URL 中的 ID 中查找消息很烦人。

——山姆

2020 年 8 月 2 日星期日 19:24,Ahmed W. 写道:
>
>

我想大多数人不明白什么是邮件列表。

无论如何,您也可以使用任何公共邮件列表镜像,例如
https://www.mail-archive.com/[email protected]/

— 您收到此消息是因为您订阅了此线程。
直接回复此邮件,在 GitHub 上查看
https://github.com/golang/go/issues/15292#issuecomment-667738419 ,或
退订
https://github.com/notifications/unsubscribe-auth/AAD5EPNQTEUF5SPT6GMM4JLR6XYUBANCNFSM4CA35RXQ
.

--
山姆·怀特

这不是真正进行讨论的地方。

对此有何更新? 🤔

@Imperatorn有过,只是这里没有讨论过。 已决定方括号[ ]将是所选语法,并且在编写泛型类型/函数时不需要“类型”一词。 空接口还有一个新的别名“any”。

最新的仿制药草案设计在这里
另请参阅此评论:关于此主题的讨论。 谢谢。

我想提醒人们,博客文章 (https://blog.golang.org/generics-next-step) 建议关于泛型的讨论发生在 golang-nuts 邮件列表上,而不是问题跟踪器上。 我会继续阅读这个问题,但它有近 800 条评论,而且非常笨拙,除了问题跟踪器的其他困难,例如没有评论线程。 谢谢。

在这一点上,虽然我尊重 Go 团队出于实际原因希望将此类讨论从问题中移出,但似乎 GitHub 上确实有很多社区成员不在 golang-nuts 上。 我想知道 GitHub 的新讨论功能是否适合? 🤔 显然,它有线程。

@toolbox这个论点也可以从另一个方向提出——有些人没有 github 帐户(并且拒绝获得一个)。 您也不必订阅golang-nuts就可以在那里发帖和参与。

@Merovius我真正喜欢 GitHub 问题的其中一个功能是,我可以订阅我感兴趣的问题的通知。我不知道如何使用 Google 群组做到这一点?

我确信有充分的理由偏爱其中一个。 当然可以讨论首选论坛应该是什么。 但是,我认为不应该在这里讨论。 这个问题已经够吵了。

@toolbox这个论点也可以从另一个方向提出——有些人没有 github 帐户(并且拒绝获得一个)。 您也不必订阅 golang-nuts 就可以在那里发帖和参与。

我明白你在说什么,这是真的,但你错过了标记。 我并不是说应该告诉 golang-nuts 用户去 GitHub,(就像现在反过来发生的那样)我是说 GitHub 用户有一个讨论论坛会很好。

我确信有充分的理由偏爱其中一个。 当然可以讨论首选论坛应该是什么。 但是,我认为不应该在这里讨论。 这个问题已经够吵了。

我同意这对于这个问题来说是非常离题的,我很抱歉提出这个问题,但我希望你能看到讽刺意味。

@keean @Merovius @toolbox和未来的人们。

仅供参考:此类讨论存在一个未解决的问题,请参阅#37469。

你好,

首先,感谢 Go。 语言绝对精彩。 对我来说,Go 最令人惊奇的事情之一就是可读性。 我是这门语言的新手,所以我仍处于发现的早期阶段,但到目前为止,它已经令人难以置信地清晰、清晰、切中要害。

我想提出的一点反馈是,从我对泛型提案的最初扫描来看, [T Constraint]对我来说并不容易快速解析,至少不像为泛型指定的字符集那么容易. 我知道 C++ 风格F<T Constraint>由于 go 的多返回范式的性质是不可行的。 任何非 ascii 字符都将是一件绝对的苦差事,所以我真的很感谢你否定了这个想法。

请考虑使用字符组合。 我不确定按位运算是否会被误解或混淆解析水域,但我认为F<<T Constraint>>会很好。 不过,任何符号组合都足够了。 虽然它可能会增加一些初始的眼睛扫描税,但我认为这可以通过FireCodaIosevka等字体连字轻松解决。 没有很多方法可以清楚而轻松地区分Map[T Constraint]map[string]T之间的区别。

我毫不怀疑人们会训练他们的思维来根据上下文区分[]的两种应用。 我只是怀疑它会使学习曲线变陡。

感谢您的注意。 不要错过明显的,但是map[T1]T2Map[T1 Constraint]可以区分,因为前者没有约束,而后者有一个必需的约束。

语法已经在 golang-nuts 上进行了广泛的讨论,我认为它已经解决了。 我们很高兴听到基于实际数据的评论,例如解析歧义。 对于基于感受和偏好的评论,我认为是时候不同意和承诺了。

再次感谢。

@ianlancetaylor够公平的。 我敢肯定你已经厌倦了听到它的吹毛求疵:) 对于它的价值,我的意思是很容易区分扫描明智的。

无论如何,我期待使用它。 谢谢你。

reflect.MakeFunc的通用替代方案将为 Go 检测带来巨大的性能优势。 但我看不出用当前提案分解函数类型的方法。

@Julio-Guerra 我不确定“分解函数类型”是什么意思。 您可以在一定程度上对参数和返回类型进行参数化: https ://go2goplay.golang.org/p/RwU11S4gC59

package main

import (
    "fmt"
)

func Call[In, Out any](f func(In) Out, v In) Out {
    return f(v)
}

func main() {
    triple := func(i int) int {
        return 3 * i
    }
    fmt.Println(Call(triple, 23))
}

这仅在两者的数量不变的情况下才有效。

@Julio-Guerra 我不确定“分解函数类型”是什么意思。 您可以在一定程度上对参数和返回类型进行参数化: https ://go2goplay.golang.org/p/RwU11S4gC59

实际上,我指的是您所做的,但概括为任何函数参数和返回类型列表(类似于反射.MakeFunc 的参数数组和返回类型)。 这将允许拥有通用的函数包装器(而不是使用工具代码生成)。

此页面是否有帮助?
0 / 5 - 0 等级