英文:
What would generics in Go be?
问题
最近对Go语言进行了很多批评,因为它不支持泛型。那么泛型具体是什么意思呢?如果有人来自于动态类型语言(比如Ruby),对于这个概念可能不太熟悉,你会如何解释呢?
英文:
There's been a lot of criticism about Go recently because it doesn't have support for generics. What exactly does that mean? How would you explain that to someone coming from a dynamically typed language like Ruby where this isn't a familiar concept?
答案1
得分: 7
在动态类型语言中,你不关心列表的具体类型,只要它是一个列表即可。然而,在静态类型语言中,你需要关心列表的具体类型,因为类型是“一个A类型的列表”,其中“A”是某种类型。也就是说,list A
和list B
是不同的类型。
因此,当谈到泛型时,使用类型为A -> B
的某个函数对列表的每个项进行操作,意味着列表必须是list A
类型。但是...如果使用泛型,你就不需要声明A
是什么类型,可以在以后的某个时间填充它。因此,你建立了一个契约,即给定一个list C
和一个函数A -> B
,为了使其编译通过,必须满足A === C
。这大大减少了样板代码。
在Go语言中,由于缺乏泛型和声明此类类型契约的能力,你必须编写一个操作int列表、double列表、string列表等的函数。你不能以一种“通用”的方式定义事物。
英文:
In a dynamically typed language, you don't care what type of list it is, just that it's a list. However, in a statically typed language, you do care what type of list it is because the type is "a list of A" where "A" is some type. That is, a list A
is a different type from list B
.
So when you speak of generics, calling some function of type A -> B
each item of a list with a foreach
means that the list must be a list A
. But... if you use generics, then you don't have to declare what A
is, you can just have it be filled in at a later date. Thus, you establish the contract whereby given a list C
and a function A -> B
, A === C
in order for it to compile. This reduces boilerplate considerably.
In Go, given the lack of generics and the ability to declare such a type contract, you have to write a function that operates on a list of int, a list of double, a list of string, etc. You can't just define things in a "generic" manner.
答案2
得分: 3
2022年第一季度更新:
Go 1.18正式支持泛型特性。Go 1.18包括了根据类型参数提案实现的泛型特性。这些变化对语言来说是重大的,但完全向后兼容。
可以参考nwillc/genfuncs
中的大量示例:
// Keys返回映射中的所有键的切片。
func Keys[K comparable, V any](m map[K]V) []K {
keys := make([]K, len(m))
var i int
for k, _ := range m {
keys[i] = k
i++
}
return keys
}
2014年:
William B. Yager的博文提醒我们,Go语言中的“泛型”部分是不够的:
你可以很容易地编写泛型函数。假设你想编写一个打印可哈希对象哈希码的函数。你可以定义一个接口,以便在静态类型安全的情况下执行此操作,如下所示:
type Hashable interface {
Hash() []byte
}
func printHash(item Hashable) {
fmt.Println(item.Hash())
}
现在,你可以将任何
Hashable
对象提供给printHash
,并且还可以获得静态类型检查。这很好。
如果你想编写一个泛型数据结构呢?让我们编写一个简单的链表。在Go中,编写泛型数据结构的惯用方式是:
(这里只是开始部分)
type LinkedList struct {
value interface{}
next *LinkedList
}
func (oldNode *LinkedList) prepend(value interface{}) *LinkedList {
return &LinkedList{value, oldNode}
}
在Go中构建泛型数据结构的“正确”方式是将事物转换为顶级类型,然后将它们放入数据结构中。这就是Java在2004年左右的工作方式。然后人们意识到这完全违背了类型系统的目的。
当你有这样的数据结构时,你完全消除了类型系统提供的任何好处。例如,下面的代码是完全有效的:
node := tail(5).prepend("Hello").prepend([]byte{1,2,3,4})
因此,如果你想保留类型系统的好处,就必须使用一些代码生成来为特定类型生成样板代码。**gen
项目**就是这种方法的一个示例:
gen
在开发时使用命令行为你的类型生成代码。gen
不是一个导入包;生成的源代码成为你的项目的一部分,不依赖外部库。
2017年6月更新:Dave Cheney在他的文章“Simplicity Debt”和“Simplicity Debt Redux”中详细说明了Go的泛型意味着什么。
由于Go 2.0现在在核心团队层面上正在积极讨论,Dave指出了泛型涉及的内容:
- 错误处理:泛型将允许单子式错误处理,这意味着你需要理解单子以及其他内容:这使得处理计算流水线成为可能,而不是在每个函数调用后检查错误。但是:你需要理解单子!
- 集合:方便自定义集合类型,而无需使用interface{}进行封箱和类型断言。但这引出了一个问题,即如何处理内置的切片和映射类型。
- 切片:如果切片消失了,那么这将如何影响常见操作,比如处理对
io.Reader.Read
的调用结果?如果切片没有消失,那是否需要添加运算符重载,以便用户定义的集合类型可以实现切片运算符? - 向量:Go的类似Pascal的数组类型在编译时具有固定的大小。如何在不使用不安全的技巧的情况下实现可扩展的向量?
- 迭代器:你真正想要能做的是组合对数据库结果和网络请求的迭代器。简而言之,来自进程外部的数据——当数据在进程外部时,检索数据可能会失败。在这种情况下,你可以选择,你的
Iterable
接口返回一个值、一个值和一个错误,或者你可以选择使用选项类型。 - 不可变性:将函数参数标记为
const
的能力是不够的,因为它虽然限制了接收器对值的修改,但并不禁止调用者这样做,而这是我今天在Go程序中看到的大多数数据竞争。也许Go需要的不是不可变性,而是所有权语义。
正如Russ Cox在“我对2017年的Go的决心”中写道:
今天,还有一些新的尝试可以借鉴,包括Dart、Midori、Rust和Swift。
最新的讨论是Go问题15292,它还引用了“Go泛型讨论总结”。
英文:
Update Q1 2022
Generics are officially supported with Go 1.18
Go 1.18 includes an implementation of generic features as described by the Type Parameters Proposal.
This includes major - but fully backward-compatible - changes to the language.
See nwillc/genfuncs
for (lots of) examples:
// Keys returns a slice of all the keys in the map.
func Keys[K comparable, V any](m map[K]V) []K {
keys := make([]K, len(m))
var i int
for k, _ := range m {
keys[i] = k
i++
}
return keys
}
2014:
William B. Yager blog post reminds why the "generic" part present in Go is not enough:
> You can write generic functions easily enough.
Let's say you wanted to write a function that printed a hash code for objects that could be hashed. You can define an interface that allows you to do this with static type safety guarantees, like this:
<!-- language-all: go -->
type Hashable interface {
Hash() []byte
}
func printHash(item Hashable) {
fmt.Println(item.Hash())
}
> Now, you can supply any Hashable
object to printHash
, and you also get static type checking. This is good.
> What if you wanted to write a generic data structure?
Let's write a simple Linked List. The idiomatic way to write a generic data structure in Go is:
(here is just the start)
type LinkedList struct {
value interface{}
next *LinkedList
}
func (oldNode *LinkedList) prepend(value interface{}) *LinkedList {
return &LinkedList{value, oldNode}
}
> The "correct" way to build generic data structures in Go is to cast things to the top type and then put them in the data structure. This is how Java used to work, circa 2004. Then people realized that this completely defeated the purpose of type systems.
> When you have data structures like this, you completely eliminate any of the benefits that a type system provides. For example, this is perfectly valid code:
node := tail(5).prepend("Hello").prepend([]byte{1,2,3,4})
So that is why, if you want to retain the benefit of type system, you have to use some code generation, to generate the boileplate code for your specific type.
The gen
project is an example of that approach:
> gen
generates code for your types, at development time, using the command line.
gen
is not an import; the generated source becomes part of your project and takes no external dependencies.
Update June 2017: Dave Cheney detailed what Generics for Go would mean in his articles "Simplicity Debt" and "Simplicity Debt Redux".
Since Go 2.0 is now actively discussed at the core team level, Dave points out what Generics involve, and that is:
- Error handling: Generic would allow a monadic Error handling, meaning you need to understand monad on top of the rest: that enable handling computational pipeline instead of checking errors after each function call.
But: you need to understand monad! - Collections: facilitate custom collection types without the need for interface{} boxing and type assertions.
But that leaves the question of what to do with the built in slice and map types. - Slicing: Does it go away, if so, how would that impact common operations like handling the result a call to
io.Reader.Read
?
If slicing doesn’t go away, would that require the addition of operator overloading so that user defined collection types can implement a slice operator? - Vector: Go’s Pascal-like array type has a fixed size known at compile time. How could you implement a growable vector without resorting to unsafe hacks?
- Iterator: what you really want to be able to do is compose iterators over database results and network requests.
In short, data from outside your process—and when data is outside your process, retrieving it might fail.
In that case you have a choice, does yourIterable
interface return a value, a value and an error, or perhaps you go down the option type route. - Immutability: The ability to mark a function parameter as
const
is insufficient, because while it restricts the receiver from mutating the value, it does not prohibit the caller from doing so, which is the majority of the data races I see in Go programs today.
Perhaps what Go needs is not immutability, but ownership semantics.
As Russ Cox writes in "My Go Resolutions for 2017":
> Today, there are newer attempts to learn from as well, including Dart, Midori, Rust, and Swift.
The latest discussion is Go issue 15292: it also references "Summary of Go Generics Discussions".
答案3
得分: 1
Go 1.18引入了泛型(beta版本)
现在你不必再猜测了。泛型已经成为Go语言的现实。Go 1.18的草案发布说明正式宣布以向后兼容的方式引入了类型参数。
目前的类型参数规范可以在由Ian Lance Taylor和Robert Griesemer撰写的类型参数提案中找到。
这些规范已经在tip版本中实现,你可以立即在Gotip Playground上运行它。
非常高级的概述
(来源)
- 函数可以有一个额外的类型参数列表,使用方括号,但在其他方面看起来像普通的参数列表:
func F[T any](p T) { ... }
。 - 这些类型参数可以被常规参数和函数体使用。
- 类型也可以有一个类型参数列表:
type M[T any] []T
。 - 每个类型参数都有一个类型约束,就像每个普通参数都有一个类型:
func F[T Constraint](p T) { ... }
。 - 类型约束是接口类型。
- 新的预声明名称
any
是一个允许任何类型的类型约束。 - 用作类型约束的接口类型可以嵌入其他元素以限制满足约束的类型参数集合:
- 任意类型
T
限制为该类型 - 近似元素
~T
限制为底层类型为T
的所有类型 - 联合元素
T1 | T2 | ...
限制为列出的任何元素
- 任意类型
- 泛型函数只能使用约束允许的所有类型支持的操作。
- 使用泛型函数或类型需要传递类型参数。
- 类型推断允许在常见情况下省略函数调用的类型参数。
泛型函数的示例
func echo[T any](v T) T {
return v
}
注意,any
是interface{}
的类型别名。
泛型结构体类型的示例
type Foo[T any] struct {
val T
}
带有类型集和近似类型的接口约束示例
type FloatingPoint interface {
~float32 | ~float64
}
接口约束也可以是参数化的,例如将任何类型限制为其指针对应类型的约束:
type Ptr[T any] interface {
*T
}
英文:
Go 1.18 introduced generics (beta release)
You don't have to be left wondering now. Generics are a reality in Go. The draft release notes of Go 1.18 officially announce the introduction of type parameters into the language in a backward-compatible way.
The current set of specifications for type parameters can be found in the type parameters proposal authored by Ian Lance Taylor and Robert Griesemer.
These specs are already implemented into the language at tip, which you can run right now on the Gotip Playground.
A very high level overview
(source)
> - Functions can have an additional type parameter list that uses square
> brackets but otherwise looks like an ordinary parameter list: func F[T
.
> any](p T) { ... }
> - These type parameters can be used by the regular
> parameters and in the function body.
> - Types can also have a type
> parameter list: type M[T any] []T
.
> - Each type parameter has a type constraint, just as each ordinary parameter has a type: func F[T
.
> Constraint](p T) { ... }
> - Type constraints are interface types.
> - The new predeclared name any
is a type constraint that permits any type.
> - Interface types used as type constraints can embed additional elements
> to restrict the set of type arguments that satisfy the constraint:
> 1. an arbitrary type T
restricts to that type
> 2. an approximation element ~T
restricts to all types whose underlying type is T
> 3. a union element T1 |
restricts to any of the listed elements
> T2 | ...
> - Generic functions may
> only use operations supported by all the types permitted by the
> constraint.
> - Using a generic function or type requires passing type
> arguments.
> - Type inference permits omitting the type arguments of a
> function call in common cases.
Example of a generic function
func echo[T any](v T) T {
return v
}
Note that any
is a type alias of interface{}
Example of a generic struct type
type Foo[T any] struct {
val T
}
Example of an interface constraint with type set and approximation types
type FloatingPoint interface {
~float32 | ~float64
}
Interface constraints can also be parametrized, for example a constraint that restricts any type to its pointer counterpart:
type Ptr[T any] interface {
*T
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论