Go泛型:自引用接口约束

huangapple go评论77阅读模式
英文:

Go generics: self-referring interface constraint

问题

我有几种自定义类型需要以相同的方式处理。泛型似乎是一个完美的选择。在处理过程中,我需要调用类型实例的方法,这些方法返回相同类型的不同实例,然后我需要调用返回实例的方法,但我无法使其正常工作。为了说明问题,我制作了一个简化的类型集合和一个展示问题的过程的示例。

以下是一个没有使用泛型的工作示例,展示了类型(CircleSquare)以及我将尝试抽象为泛型函数的过程(.Bigger().Smaller())(在线演示):

package main

import (
	"fmt"
)

type Circle struct{ r float64 }

func NewCircle(r float64) *Circle  { return &Circle{r: r} }
func (c *Circle) Radius() float64  { return c.r }
func (c *Circle) Bigger() *Circle  { return &Circle{r: c.r + 1} }
func (c *Circle) Smaller() *Circle { return &Circle{r: c.r - 1} }

type Square struct{ s float64 }

func NewSquare(s float64) *Square   { return &Square{s: s} }
func (s *Square) Side() float64     { return s.s }
func (s1 *Square) Bigger() *Square  { return &Square{s: s1.s + 1} }
func (s1 *Square) Smaller() *Square { return &Square{s: s1.s - 1} }

func main() {
	fmt.Println(NewCircle(3).Bigger().Smaller().Radius()) // 输出 3
	fmt.Println(NewSquare(6).Bigger().Smaller().Side())   // 输出 6
}

首先,我需要定义一个类型约束:

type ShapeType interface {
    *Circle | *Square
}

我将向 process 方法传递一个 ShapeType,并且我需要能够调用 ShapeType 实例的方法,因此我需要定义另一个类型约束,指定可以在 ShapeType 上调用的方法:

type Shape[ST ShapeType] interface {
    Bigger() ST
    Smaller() ST
}

有了这些,我可以编写一个 process 方法(在线演示):

func process[ST ShapeType](s Shape[ST]) ST {
    return s.Bigger().Smaller()
}

然而,这无法编译,因为 s.Bigger() 的返回值是 ST,而不是 Shape[ST],因此 Go 不知道它可以在 s.Bigger() 的返回值上调用 Smaller()。按照 Go 的说法:

> s.Bigger().Smaller undefined (type ST has no field or method Smaller)

如果 Bigger()Smaller() 不返回其接收者类型的实例,我可以这样写:

type Shape interface {
    *Circle | *Square
    Bigger()
    Smaller()
}

func process[S Shape](x S) S {
    x.Bigger().Smaller()
    return x // 为了示例,我猜我们甚至不需要返回 x
}

但是,我需要这样写:

type Shape interface {
    *Circle | *Square
    Bigger() Shape
    Smaller() Shape
}

而且似乎 Go 不喜欢自引用的类型约束。

如果可以将具体类型断言/转换为其符合的接口类型,那么我可以使其工作,但似乎不可能做到这一点(在线演示):

func process[ST ShapeType](s Shape[ST]) ST {
	s1 := s.Bigger()
	s2 := s1.(Shape[ST]) // 这里 Go 不开心
	return s2.Smaller()
}

对此,Go 会报错:

> cannot use type assertion on type parameter value s1 (variable of type ST constrained by ShapeType)

我不知道还能尝试什么。

是否可能使用泛型处理这些类型?如果可以,应该如何处理?

英文:

I have a couple custom types that I need to process in the same way. It seems like a perfect use for generics. In the process, I need to call methods on instances of the types, and those methods return different instances of the same types, and then I need to call methods on those returned instances, which I can't get to work. For the purpose of this question, I've fabricated a much simpler set of types and a process that exemplifies the problem I'm running in to.

Here's a working example without generics that shows the types (Circle and Square), and a process (.Bigger().Smaller()) I'll be trying to abstract into a generic function later (online demo):

package main

import (
	"fmt"
)

type Circle struct{ r float64 }

func NewCircle(r float64) *Circle  { return &Circle{r: r} }
func (c *Circle) Radius() float64  { return c.r }
func (c *Circle) Bigger() *Circle  { return &Circle{r: c.r + 1} }
func (c *Circle) Smaller() *Circle { return &Circle{r: c.r - 1} }

type Square struct{ s float64 }

func NewSquare(s float64) *Square   { return &Square{s: s} }
func (s *Square) Side() float64     { return s.s }
func (s1 *Square) Bigger() *Square  { return &Square{s: s1.s + 1} }
func (s1 *Square) Smaller() *Square { return &Square{s: s1.s - 1} }

func main() {
	fmt.Println(NewCircle(3).Bigger().Smaller().Radius()) // prints 3
	fmt.Println(NewSquare(6).Bigger().Smaller().Side())   // prints 6
}

The first thing I do to make a generic function is to define a type constraint:

type ShapeType interface {
    *Circle | *Square
}

I'll be passing a ShapeType to a process method, and I need to be able to call methods on the ShapeType instance, so I need to define another type constraint which specifies the methods that can be called on a ShapeType:

type Shape[ST ShapeType] interface {
    Bigger() ST
    Smaller() ST
}

With these, I can write a process method (online demo):

func process[ST ShapeType](s Shape[ST]) ST {
    return s.Bigger().Smaller()
}

This fails to compile however, as the return value of s.Bigger() is an ST, not a Shape[ST], so go doesn't know that it can then call Smaller() on the return value of s.Bigger(). In go's words:

> s.Bigger().Smaller undefined (type ST has no field or method Smaller)

If Bigger() and Smaller() didn't return instances of their receiver types, I could write:

type Shape interface {
    *Circle | *Square
    Bigger()
    Smaller()
}

func process[S Shape](x S) S {
    x.Bigger().Smaller()
    return x // I guess we wouldn't even have to return x, but just for example's sake
}

Instead I would need to write:

type Shape interface {
    *Circle | *Square
    Bigger() Shape
    Smaller() Shape
}

and it appears go doesn't like self-referential type constraints.

If it were possible to assert/convert a concrete type to an interface it conforms to, then I could make it work, but it doesn't appear to be possible to do that (online demo):

func process[ST ShapeType](s Shape[ST]) ST {
	s1 := s.Bigger()
	s2 := s1.(Shape[ST]) // go is not happy here
	return s2.Smaller()
}

For this, go says:

> cannot use type assertion on type parameter value s1 (variable of type ST constrained by ShapeType)

I don't know what else to try.

Is it possible to work with these kinds of types with generics? If so, how?

答案1

得分: 1

将你尝试的两个接口合并在一起:

type Shape[ST any] interface {
	*Circle | *Square
	Bigger() ST
	Smaller() ST
}

然后使用类型参数本身来实例化process的约束:

func process[ST Shape[ST]](s ST) ST {
	return s.Bigger().Smaller()
}
  • *Circle | *Square添加到Shape[ST any]中意味着只有这两种类型才能实现该接口。
  • 然后在方法签名中使用类型参数,比如Bigger() ST,意味着传递的任何类型都有一个返回自身的方法。

如果你想将ShapeType保持为一个独立的接口,你可以将Shape写成:

type Shape[ST any] interface {
	ShapeType
	Bigger() ST
	Smaller() ST
}

你也可以使用类型推断来使用process方法,没有任何问题:

func main() {
	c1 := NewCircle(3)
	c2 := process(c1) 
	fmt.Println(c2.Radius()) // 正如预期的那样打印3
    fmt.Printf("%T\n", c2) // *main.Circle

	s1 := NewSquare(6)
	s2 := process(s1)
	fmt.Println(s2.Side()) // 正如预期的那样打印6
    fmt.Printf("%T\n", s2) // *main.Square
}

最终的 playground:https://go.dev/play/p/_mR4wkxXupH

英文:

Combine your two attempted interfaces together:

type Shape[ST any] interface {
	*Circle | *Square
	Bigger() ST
	Smaller() ST
}

And then instantiate the constraint of process with the type parameter itself:

func process[ST Shape[ST]](s ST) ST {
	return s.Bigger().Smaller()
}
  • Adding the union element *Circle | *Square into Shape[ST any] means that only those two types will be able to implement the interface
  • Then using the type parameter in the method signature, like Bigger() ST, means that whichever type is passed has a method that returns itself.

If you want to keep ShapeType as a separated interface, you can write Shape as:

type Shape[ST any] interface {
	ShapeType
	Bigger() ST
	Smaller() ST
}

You can also use process method with type inference, without any issue:

func main() {
	c1 := NewCircle(3)
	c2 := process(c1) 
	fmt.Println(c2.Radius()) // prints 3 as expected
    fmt.Printf("%T\n", c2) // *main.Circle

	s1 := NewSquare(6)
	s2 := process(s1)
	fmt.Println(s2.Side()) // prints 6 as expected
    fmt.Printf("%T\n", s2) // *main.Square
}

Final playground: https://go.dev/play/p/_mR4wkxXupH

huangapple
  • 本文由 发表于 2022年9月25日 21:37:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/73844929.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定