英文:
go generics: how to declare a type parameter compatible with another type parameter
问题
我正在寻找一种在Go泛型约束中声明类型参数之间的兼容性的方法。
更具体地说,我需要表达某个类型T
与另一个类型U
兼容。例如,T
是一个指向实现接口U
的结构体的指针。
下面是我想要实现的具体示例:
注意:请不要用替代方法来实现"数组前置"。我只是用它作为我要解决的问题的具体应用。专注于具体示例会偏离讨论的主题。
func Prepend[T any](array []T, values ...T) []T {
if len(values) < 1 { return array }
result := make([]T, len(values) + len(array))
copy(result, values)
copy(result[len(values):], array)
return result
}
可以调用上述函数将给定类型T
的元素追加到相同类型的数组中,因此下面的代码可以正常工作:
type Foo struct{ x int }
func (self *Foo) String() string { return fmt.Sprintf("foo#%d", self.x) }
func grow(array []*Foo) []*Foo {
return Prepend(array, &Foo{x: len(array)})
}
如果数组类型与要添加的元素的类型不同(例如,由元素类型实现的接口),则代码将无法编译通过(如预期的那样),并显示错误信息type *Foo of &Foo{…} does not match inferred type Base for T
:
type Base interface { fmt.Stringer }
type Foo struct{ x int }
func (self *Foo) String() string { return fmt.Sprintf("foo#%d", self.x) }
func grow(array []Base) []Base {
return Prepend(array, &Foo{x: len(array)})
}
直观的解决方法是更改Prepend
的类型参数,使array
和values
具有不同但兼容的类型。这是我不知道如何在Go中表达的部分。
例如,下面的代码不起作用(如预期的那样),因为array
和values
的类型彼此独立。类似的代码在C++模板中可以工作,因为兼容性在模板实例化之后进行验证(类似于鸭子类型)。Go编译器会报错,显示invalid argument: arguments to copy result (variable of type []A) and values (variable of type []T) have different element types A and T
:
func Prepend[A any, T any](array []A, values ...T) []A {
if len(values) < 1 { return array }
result := make([]A, len(values) + len(array))
copy(result, values)
copy(result[len(values):], array)
return result
}
我尝试过使用约束~A
使类型T
与A
兼容,但Go不允许将类型参数用作约束的类型,会报错type in term ~A cannot be a type parameter
:
func Prepend[A any, T ~A](array []A, values ...T) []A {
在不使用反射的情况下,如何正确声明这种类型兼容性作为泛型约束的方法是什么?
英文:
I'm looking for a way to declare type compatibility between type parameters in Go generics constraints.
More specifically, I need to say some type T
is compatible with another type U
. For instance, T
is a pointer to a struct that implements the interface U
.
Below is a concrete example of what I want to accomplish:
NOTE: Please, do not answer with alternative ways to implement "array prepend". I've only used it as a concrete application of the problem I'm looking to solve. Focusing on the specific example digresses the conversation.
func Prepend[T any](array []T, values ...T) []T {
if len(values) < 1 { return array }
result := make([]T, len(values) + len(array))
copy(result, values)
copy(result[len(values):], array)
return result
}
The above function can be called to append elements of a given type T
to an array of the same type, so the code below works just fine:
type Foo struct{ x int }
func (self *Foo) String() string { return fmt.Sprintf("foo#%d", self.x) }
func grow(array []*Foo) []*Foo {
return Prepend(array, &Foo{x: len(array)})
}
If the array type is different than the elements being added (say, an interface implemented by the elements' type), the code fails to compile (as expected) with type *Foo of &Foo{…} does not match inferred type Base for T
:
type Base interface { fmt.Stringer }
type Foo struct{ x int }
func (self *Foo) String() string { return fmt.Sprintf("foo#%d", self.x) }
func grow(array []Base) []Base {
return Prepend(array, &Foo{x: len(array)})
}
The intuitive solution to that is to change the type parameters for Prepend
so that array
and values
have different, but compatible types. That's the part I don't know how to express in Go.
For instance, the code below doesn't work (as expected) because the types of array
and values
are independent of each other. Similar code would work with C++ templates since the compatibility is validated after template instantiation (similar to duck typing). The Go compiler gives out the error invalid argument: arguments to copy result (variable of type []A) and values (variable of type []T) have different element types A and T
:
func Prepend[A any, T any](array []A, values ...T) []A {
if len(values) < 1 { return array }
result := make([]A, len(values) + len(array))
copy(result, values)
copy(result[len(values):], array)
return result
}
I've tried making the type T
compatible with A
with the constraint ~A
, but Go doesn't like a type parameter used as type of a constraint, giving out the error type in term ~A cannot be a type parameter
:
func Prepend[A any, T ~A](array []A, values ...T) []A {
What's the proper way to declare this type compatibility as generics constraints without resorting to reflection?
答案1
得分: 2
这是Go语言类型参数推断的一个限制,它是一种尝试在你没有显式定义类型参数的情况下自动插入类型参数的系统。尝试显式添加类型参数,你会发现它可以工作。例如:
// 这样可以工作。
func grow(array []Base) []Base {
return Prepend[Base](array, &Foo{x: len(array)})
}
你也可以尝试将*Foo
值显式转换为Base
接口。例如:
// 这样也可以工作。
func grow(array []Base) []Base {
return Prepend(array, Base(&Foo{x: len(array)}))
}
解释
首先,你应该记住,"正确"使用类型参数的方式是始终显式地包含它们。省略类型参数列表的选项被认为是"好用的",但并不打算覆盖所有的用例。
来自博客文章《泛型简介》:
> ##### 实践中的类型推断
>
> 关于类型推断的具体细节很复杂,但使用起来并不复杂:类型推断要么成功,要么失败。如果成功,类型参数可以省略,调用泛型函数看起来与调用普通函数没有区别。如果类型推断失败,编译器会给出错误消息,在这些情况下,我们只需提供必要的类型参数即可。
>
> 在将类型推断添加到语言中时,我们试图在推断能力和复杂性之间取得平衡。我们希望确保当编译器推断类型时,这些类型永远不会令人惊讶。我们试图小心翼翼地在推断类型的失败方面而不是推断错误类型的方面上进行。我们可能没有完全做到,我们可能会在未来的版本中继续完善它。效果将是更多的程序可以在不显式指定类型参数的情况下编写。今天不需要类型参数的程序明天也不需要。
换句话说,类型推断可能会随着时间的推移而改进,但你应该预期它的限制。
在这种情况下:
// 这样可以工作。
func grow(array []*Foo) []*Foo {
return Prepend(array, &Foo{x: len(array)})
}
编译器相对简单地匹配了[]*Foo
和*Foo
的参数类型与[]T
和...T
的模式,通过替换T = *Foo
来实现匹配。
那么为什么你最初给出的简单解决方案不起作用呢?
// 为什么这个不起作用?
func grow(array []Base) []Base {
return Prepend(array, &Foo{x: len(array)})
}
将[]Base
和*Foo
替换为[]T
和...T
的模式,仅仅替换T = *Foo
或T = Base
并没有明显的匹配。你必须应用规则,即*Foo
可赋值给Base
类型,才能看到T = Base
可以工作。显然,推断系统没有额外的努力去尝试弄清楚这一点,所以在这里失败了。
英文:
This is a limitation of Go's type parameter inference, which is the system that tries to automatically insert type parameters in cases where you don't define them explicitly. Try adding in the type parameter explicitly, and you'll see that it works. For example:
// This works.
func grow(array []Base) []Base {
return Prepend[Base](array, &Foo{x: len(array)})
}
You can also try explicitly converting the *Foo
value to a Base
interface. For example:
// This works too.
func grow(array []Base) []Base {
return Prepend(array, Base(&Foo{x: len(array)}))
}
Explanation
First, you should bear in mind that the "proper" use of type parameters is to always include them explicitly. The option to omit the type parameter list is considered a "nice to have", but not intended to cover all use cases.
From the blog post An Introduction To Generics:
> ##### Type inference in practice
>
> The exact details of how type inference works are complicated, but using it is not: type inference either succeeds or fails. If it succeeds, type arguments can be omitted, and calling generic functions looks no different than calling ordinary functions. If type inference fails, the compiler will give an error message, and in those cases we can just provide the necessary type arguments.
>
> In adding type inference to the language we’ve tried to strike a balance between inference power and complexity. We want to ensure that when the compiler infers types, those types are never surprising. We’ve tried to be careful to err on the side of failing to infer a type rather than on the side of inferring the wrong type. We probably have not gotten it entirely right, and we may continue to refine it in future releases. The effect will be that more programs can be written without explicit type arguments. Programs that don’t need type arguments today won’t need them tomorrow either.
In other words, type inference may improve over time, but you should expect it to be limited.
In this case:
// This works.
func grow(array []*Foo) []*Foo {
return Prepend(array, &Foo{x: len(array)})
}
It is relatively simple for the compiler to match that the argument types of []*Foo
and *Foo
match the pattern []T
and ...T
by substitutingT = *Foo
.
So why does the plain solution you gave first not work?
// Why does this not work?
func grow(array []Base) []Base {
return Prepend(array, &Foo{x: len(array)})
}
To make []Base
and *Foo
match the pattern []T
and ...T
, just substituting T = *Foo
or T = Base
provides no apparent match. You have to apply the rule that *Foo
is assignable to the type Base
to see that T = Base
works. Apparently the inference system doesn't go the extra mile to try to figure that out, so it fails here.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论