Do Go generics allow for a LINQ to Objects equivalent?

Issue

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

(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.

Answered By – blackgreen

Answer Checked By – Cary Denson (GoLangFix Admin)

Leave a Reply

Your email address will not be published.