'Do Go generics allow for a LINQ to Objects equivalent?
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 (
"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)
}
Solution 1:[1]
(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<T> and pass derived container classes; or in Java the typical Predicate<? super T> 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: 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), "-")[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{"foo-bar", "hello-world", "bar-baz"}
fmt.Println(Remove(strs, "hello"))
// 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("%T %v", 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.
Solution 2:[2]
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)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | Roger Johansson |
