如何在Go中实现一个通用的Either类型?

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

How to implement a generic Either type in Go?

问题

在Go 1.18中引入的新泛型特性,我认为可以创建一个'Either[A,B]'类型,用于表示某个值可以是类型A或类型B之一。

你可能会在函数可能返回两个可能值之一的情况下使用这种类型(例如,一个用于'正常'结果,一个用于错误)。

我知道在Go中处理错误的惯用方式是同时返回'正常'值和错误值,对于错误或值,其中一个为nil。但是...我有点困扰的是,我们实际上是在类型中说'这返回A B',而我们真正想要表达的是'这返回A B'。

所以我想也许我们可以在这里做得更好,我认为这也是一个很好的练习,可以测试我们在使用这些新泛型时能做到什么程度。

不幸的是,尽管我尝试了很多次,但到目前为止我还没有能够解决这个问题并使其工作/编译。从我失败的尝试中,这是一个我想要以某种方式实现的接口:

//类型`Either[A,B]`的值保存一个可以是类型A或类型B的值。
type Either[A any, B any] interface {

    //根据值是A还是B调用两个函数中的一个,并返回结果。
	Switch[R any](
		onA func(a A) R,
		onB func(b B) R,
	) R
}

不幸的是,这很快失败了,因为Go不允许声明这个接口。显然是因为'接口方法不能有类型参数'。

我们如何绕过这个限制?或者在Go中是否根本没有办法创建一个准确表达'这个东西是/返回A或B'的'type'(而不是A和B的元组)?

英文:

With the new generics in Go 1.18, I thought it might be possible to create a 'Either[A,B]' type that can be used to express that something could be either of type A or type B.

A situation where you might use this is in situations where a function might return one of two possible values as a result (e.g. one for 'normal' result and one for an error).

I know the 'idiomatic' Go for errors would be to return both a 'normal' value and an error value, returning a nil for either the error or the value. But... it sort of bothers me that we are essentially saying 'this returns A and B' in the type, where what we really mean to say is 'this returns A or B'.

So I thought maybe we can do better here, and I thought this might also be a good exercise to see/test the boundaries of what we can do with these new generics.

Sadly,try as I might, so far I have not been able solve the exercise and get anything working/compiling. From one of my failed attempts, here is an interface I'd like to implement somehow:

//A value of type `Either[A,B]` holds one value which can be either of type A or type B.
type Either[A any, B any] interface {

    // Call either one of two functions depending on whether the value is an A or B
    // and return the result.
	Switch[R any]( // <=== ERROR: interface methods must have no type parameters
		onA func(a A) R),
		onB func(b B) R),
	) R
}

Unfortunately, this fails rather quickly because declaring this interface isn't allowed by Go. Apparantly because 'interface methods must have no type parameters'.

How do we work around this restriction? Or is there simply no way to create a 'type' in Go that accurately expresses the idea that 'this thing is/returns either A or B' (as opposed to a tuple of both A and B).

答案1

得分: 3

如果我要做这个,我会查找一个函数式编程语言(比如OCaml)并模仿它们的either类型的解决方案。

package main

import (
	"errors"
	"fmt"
	"os"
)

type Optional[T any] interface {
	get() (T, error)
}

type None[T any] struct {
}

func (None[T]) get() (T, error) {
	var data T
	return data, errors.New("None中没有数据")
}

type Some[T any] struct {
	data T
}

func (s Some[T]) get() (T, error) {
	return s.data, nil
}

func CreateNone[T any]() Optional[T] {
	return None[T]{}
}

func CreateSome[T any](data T) Optional[T] {
	return Some[T]{data}
}

type Either[A, B any] interface {
	is_left() bool
	is_right() bool
	find_left() Optional[A]
	find_right() Optional[B]
}

type Left[A, B any] struct {
	data A
}

func (l Left[A, B]) is_left() bool {
	return true
}

func (l Left[A, B]) is_right() bool {
	return false
}

func left[A, B any](data A) Either[A, B] {
	return Left[A, B]{data}
}

func (l Left[A, B]) find_left() Optional[A] {
	return CreateSome(l.data)
}

func (l Left[A, B]) find_right() Optional[B] {
	return CreateNone[B]()
}

type Right[A, B any] struct {
	data B
}

func (r Right[A, B]) is_left() bool {
	return false
}

func (r Right[A, B]) is_right() bool {
	return true
}

func right[A, B any](data B) Either[A, B] {
	return Right[A, B]{data}
}

func (r Right[A, B]) find_left() Optional[A] {
	return CreateNone[A]()
}

func (r Right[A, B]) find_right() Optional[B] {
	return CreateSome(r.data)
}

func main() {
	var e1 Either[int, string] = left[int, string](4143)
	var e2 Either[int, string] = right[int, string]("G4143")
	fmt.Println(e1)
	fmt.Println(e2)
	if e1.is_left() {
		if l, err := e1.find_left().get(); err == nil {
			fmt.Printf("The int is: %d\n", l)
		} else {
			fmt.Fprintln(os.Stderr, err)
		}
	}
	if e2.is_right() {
		if r, err := e2.find_right().get(); err == nil {
			fmt.Printf("The string is: %s\n", r)
		} else {
			fmt.Fprintln(os.Stderr, err)
		}
	}
}

以上是代码的翻译部分。

英文:

If I had to do this, I would look up a functional programming language(like OCaml) and knock-off their solution of the either type..

package main
import (
"errors"
"fmt"
"os"
)
type Optional[T any] interface {
get() (T, error)
}
type None[T any] struct {
}
func (None[T]) get() (T, error) {
var data T
return data, errors.New("No data present in None")
}
type Some[T any] struct {
data T
}
func (s Some[T]) get() (T, error) {
return s.data, nil
}
func CreateNone[T any]() Optional[T] {
return None[T]{}
}
func CreateSome[T any](data T) Optional[T] {
return Some[T]{data}
}
type Either[A, B any] interface {
is_left() bool
is_right() bool
find_left() Optional[A]
find_right() Optional[B]
}
type Left[A, B any] struct {
data A
}
func (l Left[A, B]) is_left() bool {
return true
}
func (l Left[A, B]) is_right() bool {
return false
}
func left[A, B any](data A) Either[A, B] {
return Left[A, B]{data}
}
func (l Left[A, B]) find_left() Optional[A] {
return CreateSome(l.data)
}
func (l Left[A, B]) find_right() Optional[B] {
return CreateNone[B]()
}
type Right[A, B any] struct {
data B
}
func (r Right[A, B]) is_left() bool {
return false
}
func (r Right[A, B]) is_right() bool {
return true
}
func right[A, B any](data B) Either[A, B] {
return Right[A, B]{data}
}
func (r Right[A, B]) find_left() Optional[A] {
return CreateNone[A]()
}
func (r Right[A, B]) find_right() Optional[B] {
return CreateSome(r.data)
}
func main() {
var e1 Either[int, string] = left[int, string](4143)
var e2 Either[int, string] = right[int, string]("G4143")
fmt.Println(e1)
fmt.Println(e2)
if e1.is_left() {
if l, err := e1.find_left().get(); err == nil {
fmt.Printf("The int is: %d\n", l)
} else {
fmt.Fprintln(os.Stderr, err)
}
}
if e2.is_right() {
if r, err := e2.find_right().get(); err == nil {
fmt.Printf("The string is: %s\n", r)
} else {
fmt.Fprintln(os.Stderr, err)
}
}
}

答案2

得分: 2

Either可以被建模为一个具有一个未导出字段类型为any/interface{}的结构类型。类型参数将用于确保在编译时具有一定程度的类型安全性:

type Either[A, B any] struct {
    value any
}

func (e *Either[A,B]) SetA(a A) {
    e.value = a
}

func (e *Either[A,B]) SetB(b B) {
    e.value = b
}

func (e *Either[A,B]) IsA() bool {
    _, ok := e.value.(A)
    return ok
}

func (e *Either[A,B]) IsB() bool {
    _, ok := e.value.(B)
    return ok
}

如果Switch必须声明为一个方法,它不能在R中自己进行参数化。额外的类型参数必须在类型定义上声明,然而这可能会使使用有点麻烦,因为在实例化时必须选择R

一个独立的函数似乎更好-在同一个包中,以访问未导出的字段:

func Switch[A,B,R any](e *Either[A,B], onA func(A) R, onB func(B) R) R {
    switch v := e.value.(type) {
        case A:
            return onA(v)
        case B:
            return onB(v)
    }
}

一个带有一些代码和用法的playground:https://go.dev/play/p/g-NmE4KZVq2

英文:

The Either could be modeled as a struct type with one unexported field of type any/interface{}. The type parameters would be used to ensure some degree of compile-time type safety:

type Either[A, B any] struct {
value any
}
func (e *Either[A,B]) SetA(a A) {
e.value = a
}
func (e *Either[A,B]) SetB(b B) {
e.value = b
}
func (e *Either[A,B]) IsA() bool {
_, ok := e.value.(A)
return ok
}
func (e *Either[A,B]) IsB() bool {
_, ok := e.value.(B)
return ok
}

If Switch has to be declared as a method, it can't be parametrized in R by itself. The additional type parameter must be declared on the type definition, however this might make usage a bit cumbersome because then R must be chosen upon instantiation.

A standalone function seems better — in the same package, to access the unexported field:

func Switch[A,B,R any](e *Either[A,B], onA func(A) R, onB func(B) R) R {
switch v := e.value.(type) {
case A:
return onA(v)
case B:
return onB(v)
}
}

A playground with some code and usage: https://go.dev/play/p/g-NmE4KZVq2

答案3

得分: 1

你可以使用https://github.com/samber/mo库(免责声明:我是该项目的作者)。

其中一个签名是:

type Either[L any, R any] struct {}

一些示例:

import "github.com/samber/mo"

left := lo.Left[string, int]("hello")

left.LeftOrElse("world")
// hello

left.RightOrElse(1234)
// 1234

left.IsLeft()
// true

left.IsRight()
// false

你关于Switch模式的问题可以这样实现:

import "github.com/samber/mo"

left := lo.Left[string, int]("hello")

result := left.Match(
	func(s string) Either[string, int] {
        // <-- 应该进入这里
		return lo.Right[string, int](1234)
	},
	func(i int) Either[string, int] {
        // <-- 不应该进入这里
		return lo.Right[string, int](i * 42)
	},
)

result.LeftOrElse("world")
// world

result.RightOrElse(42)
// 1234
英文:

You can use the https://github.com/samber/mo library (disclaimer: I'm the project author).

Either signature is:

type Either[L any, R any] struct {}

Some examples:

import &quot;github.com/samber/mo&quot;

left := lo.Left[string, int](&quot;hello&quot;)

left.LeftOrElse(&quot;world&quot;)
// hello

left.RightOrElse(1234)
// 1234

left.IsLeft()
// true

left.IsRight()
// false

Your question about a Switch pattern can be implemented this way:

import &quot;github.com/samber/mo&quot;

left := lo.Left[string, int](&quot;hello&quot;)

result := left.Match(
	func(s string) Either[string, int] {
        // &lt;-- should enter here
		return lo.Right[string, int](1234)
	},
	func(i int) Either[string, int] {
        // &lt;-- should not enter here
		return lo.Right[string, int](i * 42)
	},
)

result.LeftOrElse(&quot;world&quot;)
// world

result.RightOrElse(42)
// 1234

答案4

得分: 0

我终于找到了一个解决方案。关键是将'Either'类型定义为'struct'而不是接口。

type Either[A any, B any] struct {
	isA bool
	a   A
	b   B
}

func Switch[A any, B any, R any](either Either[A, B],
	onA func(a A) R,
	onB func(b B) R,
) R {
	if either.isA {
		return onA(either.a)
	} else {
		return onB(either.b)
	}
}

func MakeA[A any, B any](a A) Either[A, B] {
	var result Either[A, B]
	result.isA = true
	result.a = a
	return result
}

func MakeB[A any, B any](b B) Either[A, B] {
  ... 类似于MakeA的实现...
}

这样做是可行的,但是以牺牲使用一个'tuple-like'实现的代价,我们在内部存储了一个A 一个B,但通过公共API确保只能使用其中一个。

考虑到Go对我们的限制,我怀疑这是我们能做到的最好的方法。

如果有人有一个不基本上使用'tuples'来表示'union'的'解决方法',我会认为那是一个更好的答案。

英文:

A solution finally came to me. The key was defining the 'Either' type as a 'struct' instead of an interface.

type Either[A any, B any] struct {
isA bool
a   A
b   B
}
func Switch[A any, B any, R any](either Either[A, B],
onA func(a A) R,
onB func(b B) R,
) R {
if either.isA {
return onA(either.a)
} else {
return onB(either.b)
}
}
func MakeA[A any, B any](a A) Either[A, B] {
var result Either[A, B]
result.isA = true
result.a = a
return result
}
func MakeB[A any, B any](b B) Either[A, B] {
... similar to MakeA...
}

That works, but at the 'price' of really still using a 'tuple-like' implementation under the hood were we store both an A and a B but ensure it is only possible to use one of them via the public API.

I suspect this is the best we can do given the restrictions Go puts on us.

If someone has a 'workaround' that doesn't essentially use 'tuples' to represent 'unions'. I would consider that a better answer.

huangapple
  • 本文由 发表于 2022年4月10日 01:33:15
  • 转载请务必保留本文链接:https://go.coder-hub.com/71810399.html
匿名

发表评论

匿名网友

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

确定