How to create generic method in Go? (method must have no type parameters)
Asked Answered
H

2

40

Golang 1.18beta supports generic, I want to add an extension method on a generic slice. e.g. a map function is defined as this:

func Map[E, V any](slice *[]E, iteratee func(E) V) *[]V {
    result := []V{}
    for _, item := range *slice {
        result = append(result, iteratee(item))
    }

    return &result
}

Then I want to make this method as an extension method of slice, something like this, but cannot compile successfully:

func (slice *[]E) Map[E, V any](iteratee func(E) V) *[]V {
    result := []V{}
    for _, item := range *slice {
        result = append(result, iteratee(item))
    }

    return &result
}

go build with Go 1.18 gives the error:

main.go: method must have no type parameters

What is the correct way to implement the second code block?

I want to use like this:

slice := []string{"a", "b", "c"}
newSlice := slice.Map(func(s string) string {
        return s + "122"
    })
Herschelherself answered 11/1, 2022 at 14:10 Comment(6)
What do you want in the 2nd part of your question? You want to add a Map method to what type?Amiss
I updated on my post.Herschelherself
go.googlesource.com/proposal/+/refs/heads/master/design/…Wesle
In your second code block, you are attempting to add type parameters to the method. It's worth reading this short FAQ from the official Go project: Why does Go not support methods with type parameters?, which explains why that is not allowed, and also includes the advice: "Instead of methods with type parameters, use top-level functions with type parameters, or add the type parameters to the receiver type."Cremona
Also, the question here is a simple case, but for more complex cases where someone might be tempted to add type parameters to methods, it can be worth reading "Generics facilitators in Go" by JBD (@rakyll), which describes an additional solution.Cremona
The generics proposal that got accepted does explain why this is not allowed, but it's a sorry excuse for an explanation. Parametrised methods exist pretty much everywhere because they're not simply useful, but very useful. There's no reason why they couldn't have been added and -while no better solution came around- just don't count for interfaces. But this is entirely in line with go's aversion to implementing features. Before there were generics, generics were just 'not needed', or their absence could be worked around.Morley
A
37

You have to declare the slice type with type parameters you want to use for the Map() (or for any other) method:

type slice[E, V any] []E

And you have to add the type parameters to the receiver, without the constraints (those will be the same as defined at the type declaration), as if you'd instantiate the generic slice type:

func (s *slice[E, V]) Map(iteratee func(E) V) *[]V {
    result := []V{}
    for _, item := range *s {
        result = append(result, iteratee(item))
    }

    return &result
}

Testing it:

s := slice[int, string]{1, 2, 3}
m := s.Map(func(i int) string { return fmt.Sprint("x", i) })
fmt.Println(m)

Which will output (try it on the Go Playground):

&[x1 x2 x3]

Also note that slices (slice headers) already contain a pointer to an underlying array, so it's rare that you need to use a pointer to slice. Instead declare the method with non-pointer receiver, and return a non-pointer (try this one on the Go Playground):

func (s slice[E, V]) Map(iteratee func(E) V) []V {
    result := []V{}
    for _, item := range s {
        result = append(result, iteratee(item))
    }

    return result
}

Relevant section from the (tip) spec: Spec: Method declarations:

If the receiver base type is a parameterized type, the receiver specification must declare corresponding type parameters for the method to use. This makes the receiver type parameters available to the method.

Syntactically, this type parameter declaration looks like an instantiation of the receiver base type, except that the type arguments are the type parameters being declared, one for each type parameter of the receiver base type. The type parameter names do not need to match their corresponding parameter names in the receiver base type definition, and all non-blank parameter names must be unique in the receiver parameter section and the method signature. The receiver type parameter constraints are implied by the receiver base type definition: corresponding type parameters have corresponding constraints.

Amiss answered 11/1, 2022 at 14:34 Comment(0)
A
21

Methods are not permitted to have type params not specified on the type of the receiver.

The reason is that interfaces in Go are implemented implicitly, so it is yet unclear how a generic method would implement interfaces.

In order to use the second type param V, it must be defined on the type declaration. For example:

// type def: must specify V here
type Slice[T, V any] []T

func (s Slice[T,V]) Map(iteratee func(T) V) []V {
    result := make([]V, 0)
    for _, item := range s {
        result = append(result, iteratee(item))
    }

    return result
}

However you might also consider using a generic top-level function, that has the advantage of permitting type inference, thus allowing you to have less verbose code and omit explicit type params during the instantiation.

Note that the receiver must repeat the type parameter declaration, so that type parameter names are in scope for the method:

func (s Slice[T,V]) Map(...)

If some method does not use a type parameter at all, you can signal that with the blank identifier _ (underscore):

func (s Slice[T,_]) Foo( /* not using V herein */ )

However what your question was actually about is defining a generic method on an unnamed type.

You can't do this with or without generics. You can only declare methods on defined types (i.e. types with identifiers), and in your second snippet, E is not a defined type, it's a type parameter.

Unlike Javascript where all arrays inherit the Array prototype, a slice type, in itself, in Go isn't defined either. It's formalized in the language specs as:

SliceType = "[" "]" ElementType .

As you can see there's no identifier. So you can't declare a method on any and all slices, because there isn't a catch-all type that can be the receiver for such method.

Playground showing method vs func: https://gotipplay.golang.org/p/R7uAq0SBUjj

Abet answered 11/1, 2022 at 14:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.