Go泛型是否允许使用与LINQ to Objects等效的功能?

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

Do Go generics allow for a LINQ to Objects equivalent?

问题

在Go 1.18中引入了泛型之后,现在是否有可能实现类似于C#的LINQ to Objects?

或者说,与C#泛型相比,Go的泛型在原则上是否存在某些缺失,使得这种实现变得困难或不可能?

例如,原始的101个LINQ示例中的第一个示例("LowNumbers")现在可以使用泛型在Go中实现,大致如下:

package main

import (
	"fmt"
)

type collection[T comparable] []T

func (input collection[T]) where(pred func(T) bool) collection[T] {
	result := collection[T]{}
	for _, j := range input {
		if pred(j) {
			result = append(result, j)
		}
	}
	return result
}

func main() {
	numbers := collection[int]{5, 4, 1, 3, 9, 8, 6, 7, 2, 0}
	lowNums := numbers.where(func(i int) bool { return i < 5 })
	fmt.Println("Numbers < 5:")
	fmt.Println(lowNums)
}
英文:

With the addition of generics in Go 1.18, would it now be possible to come up with an equivalent of C#'s LINQ to Objects?

Or are Go's generics lacking something in principle, compared to C# generics, that will make that difficult or impossible?

For example, the first of the original 101 LINQ samples ("LowNumbers") could now be implemented in Go with generics roughly like this:

package main

import (
	&quot;fmt&quot;
)

type collection[T comparable] []T

func (input collection[T]) where(pred func(T) bool) collection[T] {
	result := collection[T]{}
	for _, j := range input {
		if pred(j) {
			result = append(result, j)
		}
	}
	return result
}

func main() {
	numbers := collection[int]{5, 4, 1, 3, 9, 8, 6, 7, 2, 0}
	lowNums := numbers.where(func(i int) bool { return i &lt; 5 })
	fmt.Println(&quot;Numbers &lt; 5:&quot;)
	fmt.Println(lowNums)
}

答案1

得分: 5

是和不是。

你几乎可以使用链式API实现这一点。
这对于许多标准的LINQ方法有效,例如SkipTakeWhereFirstLast等。

不起作用的是,在流/流中需要切换到另一个泛型类型时。

Go泛型不允许方法具有与其定义的接口/结构不同的其他类型参数。
例如,你不能有一个结构Foo[T any],然后有一个方法Bar[O any]
这对于像Select这样的方法是必需的,其中输入类型和输出类型不同。

然而,如果你不使用链式调用,而只使用普通函数,那么在功能上你可以接近。

我在这里做到了:https://github.com/asynkron/gofun

这是一个通过模拟协程来实现的完全惰性的可枚举实现。

这里不起作用的是像Zip这样需要同时枚举两个可枚举对象的函数。(虽然有一些方法可以绕过这个问题,但不太美观)

英文:

Yes and no.

You can almost get there using a chained APIs.
This works for many of the standard LINQ methods, such as Skip, Take, Where, First, Last etc.

What doesn't work, is when you need to switch to another generic type within a flow/stream.

Go Generics does not allow methods to have other type argument than the interface/struct for which they are defined.
e.g. you cannot have a struct Foo[T any] that then has a method Bar[O any]
This is needed for methods like Select where you have one type for the input and another type for the output.

However, if you don't use chaining and just go for plain functions. then you can get pretty close functionality-wise.

I've done that here: https://github.com/asynkron/gofun

This is a fully lazy enumerable implementation by simulating co-routines.

What doesn't work here is functions like Zip which needs to enumerate two enumerables at the same time. (although there are ways to hack that. but nothing pretty)

答案2

得分: 1

Go的参数化多态性与C#或Java中泛型的实现之间一个显著的区别是,Go(至今)没有针对类型参数的协变/逆变的语法。

例如,在C#中,你可以编写实现IComparer<T>接口的代码,并传递派生的容器类;或者在Java中,在流API中使用典型的Predicate<? super T>。在Go中,类型必须完全匹配,使用不同类型参数实例化泛型类型会产生不同的命名类型,它们无法相互赋值。参见:https://stackoverflow.com/questions/71399641/why-does-go-not-allow-assigning-one-generic-to-another/

此外,Go不是面向对象的,因此没有继承的概念。你可以有实现接口的类型,甚至是参数化的接口。以下是一个人为的示例:

type Equaler[T any] interface {
	Equals(T) bool
}

type Vector []int32

func (v Vector) Equals(other Vector) bool {
	// 一些逻辑
}

因此,使用此代码,Vector实现了Equaler特定实例,即Equaler[Vector]。为了明确起见,以下变量声明是合法的:

var _ Equaler[Vector] = Vector{}

因此,你可以编写在T上泛型的函数,并使用T实例化Equaler,然后可以传递任何实现该特定实例的Equaler的内容:

func Remove[E Equaler[T], T any](es []E, v T) []E {
	for i, e := range es {
		if e.Equals(v) {
			return append(es[:i], es[i+1:]...)
		}
	}
	return es
}

你可以使用任何T调用此函数,因此可以使用具有Equals(T)方法的任何T

// 实现了Equaler[string]的其他随机类型
type MyString string

func (s MyString) Equals(other string) bool {
	return strings.Split(string(s), "-")[0] == other
}

func main() {
	vecs := []Vector{{1, 2}, {3, 4, 5}, {6, 7}, {8}}
	fmt.Println(Remove(vecs, Vector{6, 7})) 
	// 输出[[1 2] [3 4 5] [8]]

	strs := []MyString{"foo-bar", "hello-world", "bar-baz"}
	fmt.Println(Remove(strs, "hello"))
	// 输出[foo-bar bar-baz]
}

唯一的问题是只有定义的类型才能有方法,因此这种方法已经排除了所有复合非命名类型。

然而,Go具有高阶函数,因此使用它和非命名类型编写类似流的API并不是不可能的,例如:

func Where[C ~[]T, T any](collection C, predicate func(T) bool) (out C) {
    for _, v := range collection {
        if predicate(v) {
            out = append(out, v)
        }
    }
    return 
}

func main() {
    // 先前声明的vecs
	filtered := Where(vecs, func(v Vector) bool { return v[0] == 3})
	fmt.Printf("%T %v", filtered, filtered)
	// 输出[]main.Vector [[3 4 5]]
}

特别是在这里,你使用了一个命名的类型参数C ~[]T,而不仅仅是定义collection []T,以便你可以将其与命名和非命名类型一起使用。

代码在playground中可用:https://gotipplay.golang.org/p/mCM2TJ9qb3F

(选择参数化接口还是高阶函数可能取决于许多因素,其中之一是是否想要链式方法,但在Go中,方法链并不常见。)

结论:是否足以模拟类似LINQ或Stream的API,并/或启用大型泛型库,只有实践才能告诉我们。现有的功能非常强大,并且在Go 1.19中,在语言设计师获得有关泛型的实际使用经验后,这些功能可能变得更加强大。

英文:

(Disclaimer: I'm not a C# expert)

A conspicuous difference between Go's parametric polymorphism and the implementation of generics in C# or Java is that Go (still) has no syntax for co-/contra-variance over type parameters.

For example in C# you can have code that implements IComparer&lt;T&gt; and pass derived container classes; or in Java the typical Predicate&lt;? super T&gt; in the stream API. In Go, types must match exactly, and instantiating a generic type with different type parameters yields different named types that just can't be assigned to each other. See also: https://stackoverflow.com/questions/71399641/why-does-go-not-allow-assigning-one-generic-to-another/

Also Go is not OO, so there's no concept of inheritance. You may have types that implement interfaces, and even parametrized interfaces. A contrived example:

type Equaler[T any] interface {
	Equals(T) bool
}

type Vector []int32

func (v Vector) Equals(other Vector) bool {
	// some logic
}

So with this code, Vector implements a specific instance of Equaler which is Equaler[Vector]. To be clear, the following var declaration compiles:

var _ Equaler[Vector] = Vector{}

So with this, you can write functions that are generic in T and use T to instantiate Equaler, and you will be able to pass anything that does implement that specific instance of Equaler:

func Remove[E Equaler[T], T any](es []E, v T) []E {
	for i, e := range es {
		if e.Equals(v) {
			return append(es[:i], es[i+1:]...)
		}
	}
	return es
}

And you can call this function with any T, and therefore with any T that has an Equals(T) method:

// some other random type that implements Equaler[T]
type MyString string

// implements Equaler[string]
func (s MyString) Equals(other string) bool {
	return strings.Split(string(s), &quot;-&quot;)[0] == other
}

func main() {
	vecs := []Vector{{1, 2}, {3, 4, 5}, {6, 7}, {8}}
	fmt.Println(Remove(vecs, Vector{6, 7})) 
	// prints [[1 2] [3 4 5] [8]]

	strs := []MyString{&quot;foo-bar&quot;, &quot;hello-world&quot;, &quot;bar-baz&quot;}
	fmt.Println(Remove(strs, &quot;hello&quot;))
	// prints [foo-bar bar-baz]
}

The only problem is that only defined types can have methods, so this approach already excludes all composite non-named types.

However, to a partial rescue, Go has higher-order functions, so writing a stream-like API with that and non-named types is not impossible, e.g.:

func Where[C ~[]T, T any](collection C, predicate func(T) bool) (out C) {
    for _, v := range collection {
        if predicate(v) {
            out = append(out, v)
        }
    }
    return 
}

func main() {
    // vecs declared earlier
	filtered := Where(vecs, func(v Vector) bool { return v[0] == 3})
	fmt.Printf(&quot;%T %v&quot;, filtered, filtered)
	// prints []main.Vector [[3 4 5]]
}

In particular here you use a named type parameter C ~[]T instead of just defining collection []T so that you can use it with both named and non-named types.

Code available in the playground: https://gotipplay.golang.org/p/mCM2TJ9qb3F

(Choosing the parametrized interfaces vs. higher-order functions probably depends on, among others, if you want to chain methods, but method chaining in Go isn't very common to begin with.)

Conclusion: whether that is enough to mimic LINQ- or Stream-like APIs, and/or enable large generic libraries, only practice will tell. The existing facilities are pretty powerful, and could become even more so in Go 1.19 after the language designers gain additional experience with real-world usage of generics.

答案3

得分: 0

Go泛型目前不支持泛型方法。但是你可以编写泛型函数。

// Filter函数选择f()返回true的项
func Filter[Src any](l []Src, f func(Src) bool) (result []Src) {
	result = make([]Src, 0, len(l)/2)
	for _, x := range l {
		if f(x) {
			result = append(result, x)
		}
	}
	return
}

func ExampleFilter() {
	lst := []int{1, 2, 3, 4, 5, 6}
	isEven := func(n int) bool { return n%2 == 0 } // 期望过滤后的偶数数组

	evens := Filter(lst, isEven)
	fmt.Println(evens)
	// 输出: [2 4 6]
}

你可以在我的代码库中获取Filter()和许多其他有用的工具函数:https://github.com/szmcdull/glinq

英文:

Go generics does not support generic method at the time. But you can write generic functions.

// Filter selects items that f() returns true
func Filter[Src any](l []Src, f func(Src) bool) (result []Src) {
	result = make([]Src, 0, len(l)/2)
	for _, x := range l {
		if f(x) {
			result = append(result, x)
		}
	}
	return
}

func ExampleFilter() {
	lst := []int{1, 2, 3, 4, 5, 6}
	isEven := func(n int) bool { return n%2 == 0 } // expected filtered array of even numbers

	evens := Filter(lst, isEven)
	fmt.Println(evens)
	// Output: [2 4 6]
}

You can get Filter() and many other useful tool functions at my repository: https://github.com/szmcdull/glinq

huangapple
  • 本文由 发表于 2022年3月14日 09:09:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/71462116.html
匿名

发表评论

匿名网友

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

确定