Experimenting with generics in Go
On 16th of June the Go team has published an update on generics in Go. Alongside a very thorough design draft, that ditches “contracts” and adopts more conventional approach to typed parameters, came the ability to play with generics yourself online, using the new Go Playground.
Let’s see how to work with the new proposal, and what it promises for the Go language.
For the sake of the exercise we’ll implement a simple list of generics elements, that should have some basic higher order methods as well, such as Filter
, Foreach
and Map
We’ll start with the definition of our generic data structure:
type List(type T) struct {
elems []T
}
As you can see, we now are able to declare types after struct or interface name. Most languages use <>
to denote a type, although Scala, for example, uses []
. Go uses (type)
for the same purpose.
Having declared the type, we now can use it inside our struct to define slice of elements.
Translating this to Java, we would have something along those lines:
class MyList<T> {
private List<T> elements = new ArrayList<>();
}
Having defined our generic data structure, let’s implement Size
, Get
and Add
on it.
Size
is pretty trivial:
func (this *List(T)) Size() int {
return len(this.elems)
}
Note that we need to define that our method receiver is a generic type: List(T)
, and not List
, as we would do before.
Add
is more interesting:
func (this *List(T)) Add(e T) *List(T) {
this.elems = append(this.elems, e)
return this
}
Here we return the struct we operate on to allow chaining:
list.Add("A").Add("B").Add("C")
Note that this is still type-safe:
var listOfNumbers List(int)
list.Add(1).Add(2).Add("A") // cannot convert "A" to int
Get
returns element in a type-safe manner. No more casting and runtime panics.
func (this *List(T)) Get(i int) T {
return this.elems[i]
}
Here I omitted the range check entirely, but that’s for brevity.
Now, to the more interesting stuff.
Let’s check if we could have Filter
, Foreach
and Map
in “Go2”.
Foreach
is the simplest one:
func (this *List(T)) Foreach(f func(T)) {
for i := 0; i < this.Size(); i++ {
f(this.Get(i))
}
}
The function we invoke on each element of our List doesn’t return anything, and neither does our method.
Function we receive as an argument is also typed: f func(T)
Filter
will return a new list, without mutation the original one:
func (this *List(T)) Filter(f func(T) bool) *List(T) {
var res List(T) for i := 0; i < this.Size(); i++ {
e := this.Get(i)
if f(e) {
res.Add(e)
}
} return &res
}
We’re using Size
, Get
and Add
methods we defined earlier.
Finally, let’s implement Map
. That’s were things are a bit more tricky:
func Map(type F, T)(list List(F), f func(F) T) *List(T) {
var res List(T)
for i := 0; i < list.Size(); i++ {
e := list.Get(i)
res.Add(f(e))
}
return &res
}
Wait, what happened? Well, in Java, we would define map()
such as:
class MyList<T> {
public <R> MyList<R> map(Function<T, R> mapper) {
...
}
}
Note: I’m dropping the extends
, super
and wildcards here
Notice the <R>
, generic that defined only on method, not on class.
But Go doesn’t support generics on methods at the moment. So, Map
will have to stay a function with two generic parameters, and not a method.
Let’s see how it all comes together now:
func main() {
var listOfNumbers List(int) for i := 1; i <= 10; i++ {
listOfNumbers.Add(i)
} // Now we can have nice Filter and Foreach in Go!
listOfNumbers.Filter(func(i int) bool {
return i%2 == 0
}).Foreach(func(i int) {
fmt.Println(i)
}) // And Map is also not that bad
listOfStrings := Map(listOfNumbers, func(i int) string {
s := strconv.Itoa(i)
return s + s
}) // The most important part - it's generic!
listOfStrings.Foreach(func(s string) {
fmt.Println(s)
})
}
Conclusions
Must admit, that I’m quite happy with the current proposal.
It is certainly not the most powerful type system in the world (Scala can rest in peace for another millennia at the very least), but it’s powerful enough for 80% of the use cases, and it’s familiar enough, unlike previous contracts proposal.
So, if you’re working with Go anyway, that’s definitely something to be looking forward to.
Full example is available here: https://go2goplay.golang.org/p/o6okRdyuJ-3